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