Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 115 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
FeaturedFeeds | |
0.00% |
0 / 115 |
|
0.00% |
0 / 15 |
1980 | |
0.00% |
0 / 1 |
getFeeds | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
getCacheKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFeedDefinitions | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
42 | |||
allInContentLanguage | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
onBeforePageDisplay | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
onSidebarBeforeOutput | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
onPageSaveComplete | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
getFeedsQuick | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getFeedsInternal | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getFeedsFromCached | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
todaysStart | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
startOfThisWeek | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
startOfDay | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getTimezone | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getMaxAge | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\FeaturedFeeds; |
4 | |
5 | use DateTime; |
6 | use DateTimeZone; |
7 | use MediaWiki\Extension\FeaturedFeeds\Hooks\HookRunner; |
8 | use MediaWiki\Hook\SidebarBeforeOutputHook; |
9 | use MediaWiki\MainConfigNames; |
10 | use MediaWiki\MediaWikiServices; |
11 | use MediaWiki\Output\Hook\BeforePageDisplayHook; |
12 | use MediaWiki\Revision\RevisionRecord; |
13 | use MediaWiki\Storage\EditResult; |
14 | use MediaWiki\Storage\Hook\PageSaveCompleteHook; |
15 | use MediaWiki\Title\Title; |
16 | use MediaWiki\User\UserIdentity; |
17 | use MediaWiki\Utils\MWTimestamp; |
18 | use Skin; |
19 | use Wikimedia\ObjectCache\WANObjectCache; |
20 | use WikiPage; |
21 | |
22 | class FeaturedFeeds implements |
23 | BeforePageDisplayHook, |
24 | PageSaveCompleteHook, |
25 | SidebarBeforeOutputHook |
26 | { |
27 | /** @var bool|null */ |
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]; |
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 | $timeZone = new DateTimeZone( $wgLocaltimezone ); |
305 | } |
306 | return $timeZone; |
307 | } |
308 | |
309 | /** |
310 | * Returns the number of seconds a feed should stay in cache |
311 | * |
312 | * @return int Time in seconds |
313 | */ |
314 | public static function getMaxAge() { |
315 | $ts = new MWTimestamp(); |
316 | // add 10 seconds to cater for time deviation between servers |
317 | $expiry = self::todaysStart() + 24 * 3600 - (int)$ts->getTimestamp() + 10; |
318 | return min( $expiry, 3600 ); |
319 | } |
320 | } |