Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 459 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
0.00% |
0 / 459 |
|
0.00% |
0 / 4 |
19182 | |
0.00% |
0 / 1 |
onParserFirstCallInit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
renderDynamicPageList | |
0.00% |
0 / 424 |
|
0.00% |
0 / 1 |
17292 | |||
processQuery | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
30 | |||
onParserTestGlobals | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\DynamicPageList; |
4 | |
5 | use ImageGalleryBase; |
6 | use MediaWiki\Hook\ParserFirstCallInitHook; |
7 | use MediaWiki\Hook\ParserTestGlobalsHook; |
8 | use MediaWiki\MediaWikiServices; |
9 | use MediaWiki\Parser\DateFormatter; |
10 | use MediaWiki\Parser\Parser; |
11 | use MediaWiki\Parser\ParserOptions; |
12 | use MediaWiki\PoolCounter\PoolCounterWorkViaCallback; |
13 | use MediaWiki\Registration\ExtensionRegistry; |
14 | use MediaWiki\Title\Title; |
15 | use MediaWiki\WikiMap\WikiMap; |
16 | use PageImages\PageImages; |
17 | use UnexpectedValueException; |
18 | use Wikimedia\ObjectCache\WANObjectCache; |
19 | use Wikimedia\Rdbms\Database; |
20 | use Wikimedia\Rdbms\IExpression; |
21 | use Wikimedia\Rdbms\IReadableDatabase; |
22 | use Wikimedia\Rdbms\LikeValue; |
23 | use Wikimedia\Rdbms\SelectQueryBuilder; |
24 | |
25 | class Hooks implements |
26 | ParserFirstCallInitHook, |
27 | ParserTestGlobalsHook |
28 | { |
29 | |
30 | /** |
31 | * Set up the <DynamicPageList> tag. |
32 | * @param Parser $parser |
33 | */ |
34 | public function onParserFirstCallInit( $parser ) { |
35 | $parser->setHook( 'DynamicPageList', [ self::class, 'renderDynamicPageList' ] ); |
36 | } |
37 | |
38 | /** |
39 | * The callback function for converting the input text to HTML output |
40 | * @param string $input |
41 | * @param array $args |
42 | * @param Parser $mwParser |
43 | * @return string |
44 | */ |
45 | public static function renderDynamicPageList( $input, $args, $mwParser ) { |
46 | global $wgDLPmaxCategories, $wgDLPMaxResultCount, $wgDLPMaxCacheTime, |
47 | $wgDLPAllowUnlimitedResults, $wgDLPAllowUnlimitedCategories; |
48 | |
49 | if ( $wgDLPMaxCacheTime !== false ) { |
50 | $mwParser->getOutput()->updateCacheExpiry( $wgDLPMaxCacheTime ); |
51 | } |
52 | $mwParser->addTrackingCategory( 'intersection-category' ); |
53 | |
54 | $countSet = false; |
55 | $count = 0; |
56 | |
57 | $startList = '<ul>'; |
58 | $endList = '</ul>'; |
59 | $startItem = '<li>'; |
60 | $endItem = '</li>'; |
61 | $inlineMode = false; |
62 | |
63 | $useGallery = false; |
64 | $pageImagesEnabled = ExtensionRegistry::getInstance()->isLoaded( 'PageImages' ); |
65 | $galleryFileSize = false; |
66 | $galleryFileName = true; |
67 | $galleryImageHeight = 0; |
68 | $galleryImageWidth = 0; |
69 | $galleryNumbRows = 0; |
70 | $galleryCaption = ''; |
71 | $gallery = null; |
72 | |
73 | $orderMethod = 'categoryadd'; |
74 | $order = 'descending'; |
75 | $redirects = 'exclude'; |
76 | $stable = 'include'; |
77 | $quality = 'include'; |
78 | $flaggedRevs = false; |
79 | |
80 | $namespaceFiltering = false; |
81 | $namespaceIndex = 0; |
82 | |
83 | $offset = 0; |
84 | |
85 | $googleHack = false; |
86 | |
87 | $suppressErrors = false; |
88 | $suppressPCErrors = false; |
89 | $showNamespace = true; |
90 | $addFirstCategoryDate = false; |
91 | $ignoreSubpages = false; |
92 | $dateFormat = ''; |
93 | $stripYear = false; |
94 | |
95 | $linkOptions = []; |
96 | $categories = []; |
97 | $excludeCategories = []; |
98 | |
99 | $services = MediaWikiServices::getInstance(); |
100 | $parser = $services->getParserFactory()->create(); |
101 | $parser->setPage( $mwParser->getPage() ); |
102 | $poptions = new ParserOptions( $mwParser->getUserIdentity() ); |
103 | |
104 | $contLang = $services->getContentLanguage(); |
105 | |
106 | $parameters = explode( "\n", $input ); |
107 | foreach ( $parameters as $parameter ) { |
108 | $paramField = explode( '=', $parameter, 2 ); |
109 | if ( count( $paramField ) < 2 ) { |
110 | continue; |
111 | } |
112 | $type = trim( $paramField[0] ); |
113 | $arg = trim( $paramField[1] ); |
114 | switch ( $type ) { |
115 | case 'category': |
116 | $title = Title::makeTitleSafe( |
117 | NS_CATEGORY, |
118 | $parser->transformMsg( $arg, $poptions, $mwParser->getTitle() ) |
119 | ); |
120 | if ( $title !== null ) { |
121 | $categories[] = $title; |
122 | } |
123 | break; |
124 | case 'notcategory': |
125 | $title = Title::makeTitleSafe( |
126 | NS_CATEGORY, |
127 | $parser->transformMsg( $arg, $poptions, $mwParser->getTitle() ) |
128 | ); |
129 | if ( $title !== null ) { |
130 | $excludeCategories[] = $title; |
131 | } |
132 | break; |
133 | case 'namespace': |
134 | $ns = $contLang->getNsIndex( $arg ); |
135 | if ( $ns !== null ) { |
136 | $namespaceIndex = $ns; |
137 | $namespaceFiltering = true; |
138 | } else { |
139 | // Note, since intval("some string") = 0 |
140 | // this considers pretty much anything |
141 | // invalid here as the main namespace. |
142 | // This was probably originally a bug, |
143 | // but is now depended upon by people |
144 | // writing things like namespace=main |
145 | // so be careful when changing this code. |
146 | $namespaceIndex = intval( $arg ); |
147 | $namespaceFiltering = ( $namespaceIndex >= 0 ); |
148 | } |
149 | break; |
150 | case 'count': |
151 | // ensure that $count is a number; |
152 | $count = intval( $arg ); |
153 | $countSet = true; |
154 | break; |
155 | case 'offset': |
156 | $offset = intval( $arg ); |
157 | break; |
158 | case 'imagewidth': |
159 | $galleryImageWidth = intval( $arg ); |
160 | break; |
161 | case 'imageheight': |
162 | $galleryImageHeight = intval( $arg ); |
163 | break; |
164 | case 'imagesperrow': |
165 | $galleryNumbRows = intval( $arg ); |
166 | break; |
167 | case 'mode': |
168 | switch ( $arg ) { |
169 | case 'gallery': |
170 | $useGallery = true; |
171 | $gallery = ImageGalleryBase::factory(); |
172 | $gallery->setParser( $mwParser ); |
173 | $startList = ''; |
174 | $endList = ''; |
175 | $startItem = ''; |
176 | $endItem = ''; |
177 | break; |
178 | case 'none': |
179 | $startList = ''; |
180 | $endList = ''; |
181 | $startItem = ''; |
182 | $endItem = '<br />'; |
183 | $inlineMode = false; |
184 | break; |
185 | case 'ordered': |
186 | $startList = '<ol>'; |
187 | $endList = '</ol>'; |
188 | $startItem = '<li>'; |
189 | $endItem = '</li>'; |
190 | $inlineMode = false; |
191 | break; |
192 | case 'inline': |
193 | // aka comma separated list |
194 | $startList = ''; |
195 | $endList = ''; |
196 | $startItem = ''; |
197 | $endItem = ''; |
198 | $inlineMode = true; |
199 | break; |
200 | case 'unordered': |
201 | default: |
202 | $startList = '<ul>'; |
203 | $endList = '</ul>'; |
204 | $startItem = '<li>'; |
205 | $endItem = '</li>'; |
206 | $inlineMode = false; |
207 | break; |
208 | } |
209 | break; |
210 | case 'gallerycaption': |
211 | // Should perhaps actually parse caption instead |
212 | // as links and what not in caption might be useful. |
213 | $galleryCaption = $parser->transformMsg( $arg, $poptions, $mwParser->getTitle() ); |
214 | break; |
215 | case 'galleryshowfilesize': |
216 | if ( $arg == 'no' || $arg == 'false' ) { |
217 | $galleryFileSize = false; |
218 | } else { |
219 | $galleryFileSize = true; |
220 | } |
221 | break; |
222 | case 'galleryshowfilename': |
223 | if ( $arg == 'no' || $arg == 'false' ) { |
224 | $galleryFileName = false; |
225 | } else { |
226 | $galleryFileName = true; |
227 | } |
228 | break; |
229 | case 'order': |
230 | if ( $arg == 'ascending' ) { |
231 | $order = 'ascending'; |
232 | } else { |
233 | $order = 'descending'; |
234 | } |
235 | break; |
236 | case 'ordermethod': |
237 | switch ( $arg ) { |
238 | case 'lastedit': |
239 | $orderMethod = 'lastedit'; |
240 | break; |
241 | case 'length': |
242 | $orderMethod = 'length'; |
243 | break; |
244 | case 'created': |
245 | $orderMethod = 'created'; |
246 | break; |
247 | case 'sortkey': |
248 | case 'categorysortkey': |
249 | $orderMethod = 'categorysortkey'; |
250 | break; |
251 | case 'popularity': |
252 | $orderMethod = 'categoryadd'; // no HitCounters since MW1.25 |
253 | break; |
254 | case 'categoryadd': |
255 | default: |
256 | $orderMethod = 'categoryadd'; |
257 | break; |
258 | } |
259 | break; |
260 | case 'redirects': |
261 | switch ( $arg ) { |
262 | case 'include': |
263 | $redirects = 'include'; |
264 | break; |
265 | case 'only': |
266 | $redirects = 'only'; |
267 | break; |
268 | case 'exclude': |
269 | default: |
270 | $redirects = 'exclude'; |
271 | break; |
272 | } |
273 | break; |
274 | case 'stablepages': |
275 | switch ( $arg ) { |
276 | case 'include': |
277 | $stable = 'include'; |
278 | break; |
279 | case 'only': |
280 | $flaggedRevs = true; |
281 | $stable = 'only'; |
282 | break; |
283 | case 'exclude': |
284 | default: |
285 | $flaggedRevs = true; |
286 | $stable = 'exclude'; |
287 | break; |
288 | } |
289 | break; |
290 | case 'qualitypages': |
291 | switch ( $arg ) { |
292 | case 'include': |
293 | $quality = 'include'; |
294 | break; |
295 | case 'only': |
296 | $flaggedRevs = true; |
297 | $quality = 'only'; |
298 | break; |
299 | case 'exclude': |
300 | default: |
301 | $flaggedRevs = true; |
302 | $quality = 'exclude'; |
303 | break; |
304 | } |
305 | break; |
306 | case 'suppresserrors': |
307 | if ( $arg == 'true' || $arg === 'all' ) { |
308 | $suppressErrors = true; |
309 | if ( $arg === 'all' ) { |
310 | $suppressPCErrors = true; |
311 | } |
312 | } else { |
313 | $suppressErrors = false; |
314 | } |
315 | break; |
316 | case 'addfirstcategorydate': |
317 | if ( $arg === 'true' ) { |
318 | $addFirstCategoryDate = true; |
319 | } elseif ( preg_match( '/^(?:[ymd]{2,3}|ISO 8601)$/', $arg ) ) { |
320 | // if it more or less is valid dateformat. |
321 | $addFirstCategoryDate = true; |
322 | $dateFormat = $arg; |
323 | if ( strlen( $dateFormat ) == 2 ) { |
324 | $dateFormat .= 'y'; # DateFormatter does not support no year. work around |
325 | $stripYear = true; |
326 | } |
327 | } else { |
328 | $addFirstCategoryDate = false; |
329 | } |
330 | break; |
331 | case 'shownamespace': |
332 | $showNamespace = $arg !== 'false'; |
333 | break; |
334 | case 'ignoresubpages': |
335 | $ignoreSubpages = ( $arg === 'true' ); |
336 | break; |
337 | case 'googlehack': |
338 | $googleHack = $arg !== 'false'; |
339 | break; |
340 | case 'nofollow': # bug 6658 |
341 | if ( $arg !== 'false' ) { |
342 | $linkOptions['rel'] = 'nofollow'; |
343 | } |
344 | break; |
345 | } // end main switch() |
346 | } // end foreach() |
347 | |
348 | $catCount = count( $categories ); |
349 | $excludeCatCount = count( $excludeCategories ); |
350 | $totalCatCount = $catCount + $excludeCatCount; |
351 | |
352 | if ( $catCount < 1 && !$namespaceFiltering ) { |
353 | if ( $suppressErrors ) { |
354 | return ''; |
355 | } |
356 | |
357 | // "!!no included categories!!" |
358 | return wfMessage( 'intersection_noincludecats' )->inContentLanguage()->escaped(); |
359 | } |
360 | |
361 | if ( $totalCatCount > $wgDLPmaxCategories && !$wgDLPAllowUnlimitedCategories ) { |
362 | if ( $suppressErrors ) { |
363 | return ''; |
364 | } |
365 | |
366 | // "!!too many categories!!" |
367 | return wfMessage( 'intersection_toomanycats' )->inContentLanguage()->escaped(); |
368 | } |
369 | |
370 | if ( $countSet ) { |
371 | if ( $count < 1 ) { |
372 | $count = 1; |
373 | } |
374 | if ( $count > $wgDLPMaxResultCount ) { |
375 | $count = $wgDLPMaxResultCount; |
376 | } |
377 | } elseif ( !$wgDLPAllowUnlimitedResults ) { |
378 | $count = $wgDLPMaxResultCount; |
379 | $countSet = true; |
380 | } |
381 | |
382 | // disallow showing date if the query doesn't have an inclusion category parameter |
383 | if ( $catCount < 1 ) { |
384 | $addFirstCategoryDate = false; |
385 | // don't sort by fields relating to categories if there are no categories. |
386 | if ( $orderMethod === 'categoryadd' || $orderMethod === 'categorysortkey' ) { |
387 | $orderMethod = 'created'; |
388 | } |
389 | } |
390 | |
391 | // build the SQL query |
392 | $dbr = $services->getConnectionProvider()->getReplicaDatabase( false, 'vslow' ); |
393 | $queryBuilder = $dbr->newSelectQueryBuilder() |
394 | ->select( [ 'page_namespace', 'page_title' ] ) |
395 | ->from( 'page' ); |
396 | |
397 | if ( $googleHack ) { |
398 | $queryBuilder->field( 'page_id' ); |
399 | } |
400 | |
401 | if ( $addFirstCategoryDate ) { |
402 | $queryBuilder->field( 'c1.cl_timestamp' ); |
403 | } |
404 | |
405 | if ( $namespaceFiltering ) { |
406 | $queryBuilder->where( [ 'page_namespace' => $namespaceIndex ] ); |
407 | } |
408 | |
409 | // Bug 14943 - Allow filtering based on FlaggedRevs stability. |
410 | // Check if the extension actually exists before changing the query... |
411 | if ( $flaggedRevs && ExtensionRegistry::getInstance()->isLoaded( 'FlaggedRevs' ) ) { |
412 | $queryBuilder->leftJoin( 'flaggedpages', null, 'page_id = fp_page_id' ); |
413 | |
414 | if ( $stable == 'only' ) { |
415 | $queryBuilder->where( $dbr->expr( 'fp_stable', '!=', null ) ); |
416 | } elseif ( $stable == 'exclude' ) { |
417 | $queryBuilder->where( [ 'fp_stable' => null ] ); |
418 | } |
419 | |
420 | if ( $quality == 'only' ) { |
421 | $queryBuilder->where( $dbr->expr( 'fp_quality', '>=', 1 ) ); |
422 | } elseif ( $quality == 'exclude' ) { |
423 | $queryBuilder->where( $dbr->expr( 'fp_quality', '=', 0 )->or( 'fp_quality', '=', null ) ); |
424 | } |
425 | } |
426 | |
427 | if ( $redirects == 'only' ) { |
428 | $queryBuilder->where( [ 'page_is_redirect' => 1 ] ); |
429 | } elseif ( $redirects == 'exclude' ) { |
430 | $queryBuilder->where( [ 'page_is_redirect' => 0 ] ); |
431 | } |
432 | |
433 | if ( $ignoreSubpages ) { |
434 | $queryBuilder->where( $dbr->expr( 'page_title', IExpression::NOT_LIKE, |
435 | new LikeValue( $dbr->anyString(), '/', $dbr->anyString() ) ) ); |
436 | } |
437 | |
438 | if ( $useGallery && $pageImagesEnabled ) { |
439 | $queryBuilder->leftJoin( 'page_props', 'pp1', [ |
440 | 'pp1.pp_propname' => PageImages::PROP_NAME_FREE, |
441 | 'page_id = pp1.pp_page' |
442 | ] ); |
443 | $queryBuilder->field( 'pp1.pp_value', 'pageimage_free' ); |
444 | |
445 | $queryBuilder->leftJoin( 'page_props', 'pp2', [ |
446 | 'pp2.pp_propname' => PageImages::PROP_NAME, |
447 | 'page_id = pp2.pp_page' |
448 | ] ); |
449 | $queryBuilder->field( 'pp2.pp_value', 'pageimage_nonfree' ); |
450 | } |
451 | |
452 | // Alias each category as c1, c2, etc. |
453 | $currentTableNumber = 1; |
454 | foreach ( $categories as $cat ) { |
455 | $queryBuilder->join( 'categorylinks', "c$currentTableNumber", [ |
456 | "page_id = c{$currentTableNumber}.cl_from", |
457 | "c{$currentTableNumber}.cl_to" => $cat->getDBKey(), |
458 | ] ); |
459 | $currentTableNumber++; |
460 | } |
461 | |
462 | foreach ( $excludeCategories as $cat ) { |
463 | $queryBuilder->leftJoin( 'categorylinks', "c$currentTableNumber", [ |
464 | "page_id = c{$currentTableNumber}.cl_from", |
465 | "c{$currentTableNumber}.cl_to" => $cat->getDBKey(), |
466 | ] ); |
467 | $queryBuilder->where( [ "c{$currentTableNumber}.cl_to" => null ] ); |
468 | $currentTableNumber++; |
469 | } |
470 | |
471 | if ( $order === 'descending' ) { |
472 | $sqlOrder = SelectQueryBuilder::SORT_DESC; |
473 | } else { |
474 | $sqlOrder = SelectQueryBuilder::SORT_ASC; |
475 | } |
476 | |
477 | switch ( $orderMethod ) { |
478 | case 'lastedit': |
479 | $queryBuilder->orderBy( 'page_touched', $sqlOrder ); |
480 | break; |
481 | case 'length': |
482 | $queryBuilder->orderBy( 'page_len', $sqlOrder ); |
483 | break; |
484 | case 'created': |
485 | $queryBuilder->orderBy( 'page_id', $sqlOrder ); // Since they're never reused and increasing |
486 | break; |
487 | case 'categorysortkey': |
488 | $queryBuilder->orderBy( [ 'c1.cl_type', 'c1.cl_sortkey' ], $sqlOrder ); |
489 | break; |
490 | case 'categoryadd': |
491 | $queryBuilder->orderBy( 'c1.cl_timestamp', $sqlOrder ); |
492 | break; |
493 | default: |
494 | // Should never reach here |
495 | throw new UnexpectedValueException( "Invalid ordermethod $orderMethod" ); |
496 | } |
497 | |
498 | if ( $countSet ) { |
499 | $queryBuilder->limit( $count ); |
500 | } |
501 | if ( $offset > 0 ) { |
502 | $queryBuilder->offset( $offset ); |
503 | } |
504 | |
505 | // To track down what page offending queries are on. |
506 | // For some reason, $fname doesn't get escaped by our code?! |
507 | $pageName = str_replace( [ '*', '/' ], '-', $mwParser->getTitle()->getPrefixedDBkey() ); |
508 | $rows = self::processQuery( $pageName, $dbr, $queryBuilder ); |
509 | if ( $rows === false ) { |
510 | // This error path is very fast (We exit immediately if poolcounter is full) |
511 | // Thus it should be safe to try again in ~5 minutes. |
512 | $mwParser->getOutput()->updateCacheExpiry( 4 * 60 + mt_rand( 0, 120 ) ); |
513 | // Pool counter all threads in use. |
514 | if ( $suppressPCErrors ) { |
515 | return ''; |
516 | } |
517 | return wfMessage( 'intersection_pcerror' )->inContentLanguage()->escaped(); |
518 | } |
519 | if ( count( $rows ) == 0 ) { |
520 | if ( $suppressErrors ) { |
521 | return ''; |
522 | } |
523 | |
524 | return wfMessage( 'intersection_noresults' )->inContentLanguage()->escaped(); |
525 | } |
526 | |
527 | $df = null; |
528 | if ( $dateFormat !== '' && $addFirstCategoryDate ) { |
529 | $df = DateFormatter::getInstance(); |
530 | } |
531 | |
532 | // process results of query, outputing equivalent of <li>[[Article]]</li> |
533 | // for each result, or something similar if the list uses other |
534 | // startlist/endlist |
535 | $articleList = []; |
536 | $linkRenderer = $services->getLinkRenderer(); |
537 | foreach ( $rows as $row ) { |
538 | $title = Title::makeTitle( $row->page_namespace, $row->page_title ); |
539 | $categoryDate = ''; |
540 | if ( $addFirstCategoryDate ) { |
541 | if ( $dateFormat !== '' ) { |
542 | // this is a tad ugly |
543 | // use DateFormatter, and support discarding year. |
544 | $categoryDate = wfTimestamp( TS_ISO_8601, $row->cl_timestamp ); |
545 | if ( $stripYear ) { |
546 | $categoryDate = $contLang->getMonthName( (int)substr( $categoryDate, 5, 2 ) ) |
547 | . ' ' . substr( $categoryDate, 8, 2 ); |
548 | } else { |
549 | $categoryDate = substr( $categoryDate, 0, 10 ); |
550 | } |
551 | $categoryDate = $df->reformat( $dateFormat, $categoryDate, [ 'match-whole' ] ); |
552 | } else { |
553 | $categoryDate = $contLang->date( wfTimestamp( TS_MW, $row->cl_timestamp ) ); |
554 | } |
555 | if ( $useGallery ) { |
556 | $categoryDate .= ' '; |
557 | } else { |
558 | $categoryDate .= wfMessage( 'colon-separator' )->text(); |
559 | } |
560 | } |
561 | |
562 | $query = []; |
563 | if ( $googleHack ) { |
564 | $query['dpl_id'] = intval( $row->page_id ); |
565 | } |
566 | |
567 | if ( $showNamespace ) { |
568 | $titleText = $title->getPrefixedText(); |
569 | } else { |
570 | $titleText = $title->getText(); |
571 | } |
572 | |
573 | if ( $useGallery ) { |
574 | $link = ''; |
575 | if ( $galleryFileName ) { |
576 | $link = $linkRenderer->makeKnownLink( |
577 | $title, |
578 | $titleText, |
579 | [ 'class' => 'galleryfilename galleryfilename-truncate' ] |
580 | ) . "\n"; |
581 | } |
582 | |
583 | $file = null; |
584 | if ( $title->getNamespace() !== NS_FILE && $pageImagesEnabled ) { |
585 | $file = $row->pageimage_free ?: $row->pageimage_nonfree; |
586 | } |
587 | |
588 | // Note, $categoryDate is treated as raw html |
589 | // this is safe since the only html present |
590 | // would come from the dateformatter <span>. |
591 | if ( $file !== null ) { |
592 | $gallery->add( |
593 | Title::makeTitle( NS_FILE, $file ), |
594 | $link . $categoryDate, |
595 | $file, |
596 | $title->getLinkURL() |
597 | ); |
598 | } else { |
599 | $gallery->add( |
600 | $title, |
601 | $link . $categoryDate, |
602 | $title->getText() |
603 | ); |
604 | } |
605 | } else { |
606 | // FIXME: per T17739 and T22818, forcearticlepath |
607 | // was used, this may be unnecessary nowadays |
608 | // depending on a full rollout of GoogleNewsSitemap |
609 | $articleList[] = htmlspecialchars( $categoryDate ) . |
610 | MediaWikiServices::getInstance()->getLinkRendererFactory() |
611 | ->createFromLegacyOptions( [ 'forcearticlepath' ] )->makeKnownLink( |
612 | $title, |
613 | $titleText, |
614 | $linkOptions, |
615 | $query |
616 | ); |
617 | } |
618 | } |
619 | |
620 | if ( $useGallery ) { |
621 | $gallery->setHideBadImages(); |
622 | $gallery->setShowFilename( false ); |
623 | $gallery->setShowBytes( $galleryFileSize ); |
624 | if ( $galleryImageHeight > 0 ) { |
625 | $gallery->setHeights( (string)$galleryImageHeight ); |
626 | } |
627 | if ( $galleryImageWidth > 0 ) { |
628 | $gallery->setWidths( (string)$galleryImageWidth ); |
629 | } |
630 | if ( $galleryNumbRows > 0 ) { |
631 | $gallery->setPerRow( $galleryNumbRows ); |
632 | } |
633 | if ( $galleryCaption !== '' ) { |
634 | $gallery->setCaption( $galleryCaption ); // gallery class escapes string |
635 | } |
636 | return $gallery->toHtml(); |
637 | } |
638 | |
639 | // start unordered list |
640 | $output = $startList . "\n" . $startItem; |
641 | if ( $inlineMode ) { |
642 | $output .= $contLang->commaList( $articleList ); |
643 | } else { |
644 | $output .= implode( "$endItem \n$startItem", $articleList ); |
645 | } |
646 | $output .= $endItem . $endList . "\n"; |
647 | // end unordered list |
648 | |
649 | return $output; |
650 | } |
651 | |
652 | /** |
653 | * @param string $pageName Name of page (for logging purposes) |
654 | * @param IReadableDatabase $dbr |
655 | * @param SelectQueryBuilder $queryBuilder |
656 | * @return array|bool List of stdObj's or false on poolcounter being full |
657 | */ |
658 | public static function processQuery( |
659 | string $pageName, |
660 | IReadableDatabase $dbr, |
661 | SelectQueryBuilder $queryBuilder |
662 | ) { |
663 | global $wgDLPQueryCacheTime, $wgDLPMaxQueryTime; |
664 | $qname = __METHOD__ . ' - ' . $pageName; |
665 | if ( $wgDLPMaxQueryTime ) { |
666 | $queryBuilder->setMaxExecutionTime( $wgDLPMaxQueryTime ); |
667 | } |
668 | |
669 | $doQuery = static function () use ( $qname, $queryBuilder ) { |
670 | $res = $queryBuilder->caller( $qname )->fetchResultSet(); |
671 | // Serializing a ResultWrapper doesn't work. |
672 | return iterator_to_array( $res ); |
673 | }; |
674 | |
675 | // We're probably already inside a pool-counter lock due to parse, so nowait. |
676 | $poolCounterKey = "nowait:dpl-query:" . WikiMap::getCurrentWikiId(); |
677 | // The goal here is to have an emergency shutoff break to prevent a query |
678 | // pile-up if a large number of slow DPL queries are run at once. |
679 | // This is meant to be in total across the wiki. The WANObjectCache stuff below this |
680 | // is meant to make the somewhat common case of the same DPL query being run multiple |
681 | // times due to template usage fast, where this is not meant to speed things up, but |
682 | // to have an emergency stop before things get out of hand. |
683 | // Recommended config is probably something like 15 workers normally and |
684 | // 5 workers if DB seems to have excessive load. |
685 | $worker = new PoolCounterWorkViaCallback( 'DPL', $poolCounterKey, [ |
686 | 'doWork' => $doQuery, |
687 | ] ); |
688 | |
689 | if ( $wgDLPQueryCacheTime <= 0 ) { |
690 | return $worker->execute(); |
691 | } |
692 | |
693 | // This is meant to guard against the case where a lot of pages get parsed at once |
694 | // all with the same query. See T262240. This should be a short cache, e.g. 120 seconds. |
695 | $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); |
696 | // Don't use actual input as hash, because some per-page parsing can happen to options. |
697 | $query = ( clone $queryBuilder )->getSQL(); |
698 | return $cache->getWithSetCallback( |
699 | $cache->makeKey( "DPLQuery", hash( "sha256", $query ) ), |
700 | $wgDLPQueryCacheTime, |
701 | static function ( $oldVal, &$ttl, &$setOpts ) use ( $worker, $dbr ){ |
702 | // TODO: Maybe could do something like check max(cl_timestamp) in |
703 | // category and the count in category.cat_pages, and invalidate if |
704 | // it appears like someone added or removed something from the category. |
705 | $setOpts += Database::getCacheSetOptions( $dbr ); |
706 | $res = $worker->execute(); |
707 | if ( $res === false ) { |
708 | // Do not cache errors. |
709 | $ttl = WANObjectCache::TTL_UNCACHEABLE; |
710 | // If we have oldVal, prefer it to error |
711 | if ( is_array( $oldVal ) ) { |
712 | return $oldVal; |
713 | } |
714 | } |
715 | return $res; |
716 | }, |
717 | [ |
718 | 'lowTTL' => min( $cache::TTL_MINUTE, floor( $wgDLPQueryCacheTime * 0.75 ) ), |
719 | 'pcTTL' => min( $cache::TTL_PROC_LONG, $wgDLPQueryCacheTime ) |
720 | ] |
721 | ); |
722 | } |
723 | |
724 | /** |
725 | * Use legacy gallery syntax in tests. |
726 | * FIXME the tests should be updated instead |
727 | * @param array &$globals |
728 | */ |
729 | public function onParserTestGlobals( &$globals ) { |
730 | $globals['wgParserEnableLegacyMediaDOM'] = true; |
731 | } |
732 | } |