Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 459
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 459
0.00% covered (danger)
0.00%
0 / 4
19182
0.00% covered (danger)
0.00%
0 / 1
 onParserFirstCallInit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 renderDynamicPageList
0.00% covered (danger)
0.00%
0 / 424
0.00% covered (danger)
0.00%
0 / 1
17292
 processQuery
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
30
 onParserTestGlobals
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\DynamicPageList;
4
5use DateFormatter;
6use ExtensionRegistry;
7use ImageGalleryBase;
8use MediaWiki\Hook\ParserFirstCallInitHook;
9use MediaWiki\Hook\ParserTestGlobalsHook;
10use MediaWiki\MediaWikiServices;
11use MediaWiki\PoolCounter\PoolCounterWorkViaCallback;
12use MediaWiki\Title\Title;
13use MediaWiki\WikiMap\WikiMap;
14use PageImages\PageImages;
15use Parser;
16use ParserOptions;
17use UnexpectedValueException;
18use WANObjectCache;
19use Wikimedia\Rdbms\Database;
20use Wikimedia\Rdbms\IExpression;
21use Wikimedia\Rdbms\IReadableDatabase;
22use Wikimedia\Rdbms\LikeValue;
23use Wikimedia\Rdbms\SelectQueryBuilder;
24
25class 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}