Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 120
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
FeaturedFeeds
0.00% covered (danger)
0.00%
0 / 120
0.00% covered (danger)
0.00%
0 / 15
2070
0.00% covered (danger)
0.00%
0 / 1
 getFeeds
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 getCacheKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFeedDefinitions
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 allInContentLanguage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 onSidebarBeforeOutput
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 onPageSaveComplete
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 getFeedsQuick
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getFeedsInternal
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getFeedsFromCached
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 todaysStart
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 startOfThisWeek
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 startOfDay
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getTimezone
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getMaxAge
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\FeaturedFeeds;
4
5use DateTime;
6use DateTimeZone;
7use MediaWiki\Extension\FeaturedFeeds\Hooks\HookRunner;
8use MediaWiki\Hook\BeforePageDisplayHook;
9use MediaWiki\Hook\SidebarBeforeOutputHook;
10use MediaWiki\MainConfigNames;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Revision\RevisionRecord;
13use MediaWiki\Storage\EditResult;
14use MediaWiki\Storage\Hook\PageSaveCompleteHook;
15use MediaWiki\Title\Title;
16use MediaWiki\User\UserIdentity;
17use MediaWiki\Utils\MWTimestamp;
18use Skin;
19use WANObjectCache;
20use Wikimedia\AtEase\AtEase;
21use WikiPage;
22
23class FeaturedFeeds implements
24    BeforePageDisplayHook,
25    PageSaveCompleteHook,
26    SidebarBeforeOutputHook
27{
28    private static $allInContLang = null;
29
30    /**
31     * Returns the list of feeds
32     *
33     * @param string|bool $langCode Code of language to use or false if default
34     * @return FeaturedFeedChannel[] Feeds in format of ('name' => FeedItem)
35     */
36    public static function getFeeds( $langCode ) {
37        global $wgLanguageCode;
38
39        if (
40            !$langCode ||
41            self::allInContentLanguage() ||
42            !MediaWikiServices::getInstance()->getLanguageNameUtils()->isValidBuiltInCode( $langCode )
43        ) {
44            $langCode = $wgLanguageCode;
45        }
46
47        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
48
49        $feeds = $cache->getWithSetCallback(
50            self::getCacheKey( $cache, $langCode ),
51            self::getMaxAge(),
52            function () use ( $langCode ) {
53                return self::getFeedsInternal( $langCode );
54            },
55            [
56                // The "*" key is touched whenever a relevant message changes.
57                // This avoids a slow explicit delete() of ~360 keys.
58                'checkKeys' => [ self::getCacheKey( $cache, '*' ) ],
59                // Avoid I/O from repeated access
60                'pcGroup' => 'FeaturedFeeds:100',
61                'pcTTL' => $cache::TTL_PROC_LONG
62            ]
63        );
64
65        return self::getFeedsFromCached( $feeds );
66    }
67
68    /**
69     * Returns cache key for a given language
70     * @param WANObjectCache $cache
71     * @param string $langCode Feed language code
72     * @return string
73     */
74    private static function getCacheKey( WANObjectCache $cache, $langCode ) {
75        return $cache->makeKey( 'featured-feeds', FeaturedFeedChannel::VERSION, $langCode );
76    }
77
78    /**
79     * Returns fully prepared feed definitions
80     * @return array[]
81     */
82    private static function getFeedDefinitions() {
83        global $wgFeaturedFeeds, $wgFeaturedFeedsDefaults;
84        static $feedDefs = false;
85        if ( $feedDefs === false ) {
86            $feedDefs = $wgFeaturedFeeds;
87            ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
88                ->onFeaturedFeeds__getFeeds( $feedDefs );
89
90            // fill defaults
91            self::$allInContLang = true;
92            foreach ( $feedDefs as $name => $opts ) {
93                foreach ( $wgFeaturedFeedsDefaults as $setting => $value ) {
94                    if ( !isset( $opts[$setting] ) ) {
95                        $feedDefs[$name][$setting] = $value;
96                    }
97                }
98                self::$allInContLang = self::$allInContLang && !$feedDefs[$name]['inUserLanguage'];
99            }
100        }
101        return $feedDefs;
102    }
103
104    /**
105     * Returns whether all feeds are in content language
106     * @return bool
107     */
108    public static function allInContentLanguage() {
109        if ( self::$allInContLang === null ) {
110            self::getFeedDefinitions();
111        }
112        return self::$allInContLang;
113    }
114
115    /**
116     * Adds feeds to the page header
117     *
118     * {@inheritDoc}
119     */
120    public function onBeforePageDisplay( $out, $skin ): void {
121        if ( $out->getTitle()->isMainPage() ) {
122            $feeds = self::getFeedsQuick( $out->getLanguage()->getCode() );
123            $advertisedFeedTypes = $out->getConfig()->get( MainConfigNames::AdvertisedFeedTypes );
124            /** @var FeaturedFeedChannel $feed */
125            foreach ( $feeds as $feed ) {
126                foreach ( $advertisedFeedTypes as $type ) {
127                    $out->addLink( [
128                        'rel' => 'alternate',
129                        'type' => "application/$type+xml",
130                        'title' => $feed->title,
131                        'href' => $feed->getURL( $type ),
132                    ] );
133                }
134            }
135        }
136    }
137
138    /**
139     * SidebarBeforeOutput hook handler
140     *
141     * @param Skin $skin
142     * @param array &$sidebar
143     */
144    public function onSidebarBeforeOutput( $skin, &$sidebar ): void {
145        global $wgDisplayFeedsInSidebar, $wgAdvertisedFeedTypes;
146
147        if ( !$skin->getTitle()->isMainPage() ) {
148            return;
149        }
150
151        $msgDisabled = $skin->msg( 'ffeed-enable-sidebar-links' )->inContentLanguage()->isDisabled();
152
153        if ( !$wgDisplayFeedsInSidebar || $msgDisabled ) {
154            return;
155        }
156
157        $feeds = self::getFeedsQuick( $skin->getLanguage()->getCode() );
158        $links = [];
159        $format = $wgAdvertisedFeedTypes[0]; // @fixme:
160        /** @var FeaturedFeedChannel $feed */
161        foreach ( $feeds as $feed ) {
162            $links[] = [
163                'href' => $feed->getURL( $format ),
164                'title' => $feed->title,
165                'text' => $feed->shortTitle,
166            ];
167        }
168
169        if ( count( $links ) ) {
170            $sidebar['ffeed-sidebar-section'] = $links;
171        }
172    }
173
174    /**
175     * Purges cache on message edit
176     *
177     * @param WikiPage $wikiPage
178     * @param UserIdentity $user
179     * @param string $summary
180     * @param int $flags
181     * @param RevisionRecord $revisionRecord
182     * @param EditResult $editResult
183     */
184    public function onPageSaveComplete( $wikiPage, $user, $summary, $flags, $revisionRecord, $editResult ) {
185        $title = $wikiPage->getTitle();
186        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
187        // Although message names are configurable and can be set not to start with 'Ffeed', we
188        // make a shortcut here to avoid running these checks on every NS_MEDIAWIKI edit
189        if ( $title->getNamespace() == NS_MEDIAWIKI && strpos( $title->getText(), 'Ffeed-' ) === 0 ) {
190            $baseTitle = Title::makeTitle( NS_MEDIAWIKI, $title->getBaseText() );
191            $messages  = [ 'page', 'title', 'short-title', 'description', 'entryName' ];
192            foreach ( self::getFeedDefinitions() as $feed ) {
193                foreach ( $messages as $msgType ) {
194                    $nt = Title::makeTitleSafe( NS_MEDIAWIKI, $feed[$msgType] );
195                    if ( $nt->equals( $baseTitle ) ) {
196                        wfDebug( "FeaturedFeeds-related page {$title->getFullText()} edited, purging cache\n" );
197                        $cache->touchCheckKey( self::getCacheKey( $cache, '*' ) );
198                        return;
199                    }
200                }
201            }
202        }
203    }
204
205    /**
206     * Get all the feed objects without loading the items
207     *
208     * @param string $langCode
209     * @return FeaturedFeedChannel[]
210     */
211    private static function getFeedsQuick( $langCode ) {
212        $feedDefs = self::getFeedDefinitions();
213
214        $feeds = [];
215        foreach ( $feedDefs as $name => $opts ) {
216            $feed = new FeaturedFeedChannel( $name, $opts, $langCode );
217            if ( !$feed->isOK() ) {
218                continue;
219            }
220            $feeds[$name] = $feed;
221        }
222
223        return $feeds;
224    }
225
226    /**
227     * @param string $langCode
228     * @return array[]
229     */
230    private static function getFeedsInternal( $langCode ) {
231        $feeds = self::getFeedsQuick( $langCode );
232        $toCache = [];
233
234        foreach ( $feeds as $name => $feed ) {
235            $feed->getFeedItems();
236            $toCache[$name] = $feed->toArray();
237        }
238
239        return $toCache;
240    }
241
242    /**
243     * @param array[] $cached
244     * @return FeaturedFeedChannel[]
245     */
246    private static function getFeedsFromCached( array $cached ): array {
247        $feeds = [];
248
249        foreach ( $cached as $name => $array ) {
250            $feeds[$name] = FeaturedFeedChannel::fromArray( $array );
251        }
252
253        return $feeds;
254    }
255
256    /**
257     * Returns the Unix timestamp of current day's first second
258     *
259     * @return int Timestamp
260     */
261    public static function todaysStart() {
262        static $time = false;
263        if ( !$time ) {
264            $time = self::startOfDay( time() );
265        }
266        return $time;
267    }
268
269    /**
270     * Returns the Unix timestamp of current week's first second
271     *
272     * @return int Timestamp
273     */
274    public static function startOfThisWeek() {
275        static $time = false;
276        if ( !$time ) {
277            $dt = new DateTime( 'this week', self::getTimezone() );
278            $dt->setTime( 0, 0, 0 );
279            $time = $dt->getTimestamp();
280        }
281        return $time;
282    }
283
284    /**
285     * Returns the Unix timestamp of current day's first second
286     *
287     * @param string|int $timestamp
288     * @return int Timestamp
289     */
290    public static function startOfDay( $timestamp ) {
291        $dt = new DateTime( "@$timestamp", self::getTimezone() );
292        $dt->setTime( 0, 0, 0 );
293        return $dt->getTimestamp();
294    }
295
296    /**
297     * @return DateTimeZone
298     */
299    private static function getTimezone() {
300        global $wgLocaltimezone;
301        static $timeZone;
302
303        if ( $timeZone === null ) {
304            if ( isset( $wgLocaltimezone ) ) {
305                $tz = $wgLocaltimezone;
306            } else {
307                AtEase::suppressWarnings();
308                $tz = date_default_timezone_get();
309                AtEase::restoreWarnings();
310            }
311            $timeZone = new DateTimeZone( $tz );
312        }
313        return $timeZone;
314    }
315
316    /**
317     * Returns the number of seconds a feed should stay in cache
318     *
319     * @return int Time in seconds
320     */
321    public static function getMaxAge() {
322        $ts = new MWTimestamp();
323        // add 10 seconds to cater for time deviation between servers
324        $expiry = self::todaysStart() + 24 * 3600 - (int)$ts->getTimestamp() + 10;
325        return min( $expiry, 3600 );
326    }
327}