MediaWiki REL1_37
QueryPage.php
Go to the documentation of this file.
1<?php
31
41abstract class QueryPage extends SpecialPage {
43 protected $listoutput = false;
44
46 protected $offset = 0;
47
49 protected $limit = 0;
50
58 protected $numRows;
59
63 protected $cachedTimestamp = null;
64
68 protected $shownavigation = true;
69
71 private $loadBalancer = null;
72
74 private $linkBatchFactory = null;
75
85 public static function getPages() {
86 static $qp = null;
87
88 if ( $qp === null ) {
89 // QueryPage subclass, Special page name
90 $qp = [
91 [ SpecialAncientPages::class, 'Ancientpages' ],
92 [ SpecialBrokenRedirects::class, 'BrokenRedirects' ],
93 [ SpecialDeadendPages::class, 'Deadendpages' ],
94 [ SpecialDoubleRedirects::class, 'DoubleRedirects' ],
95 [ SpecialListDuplicatedFiles::class, 'ListDuplicatedFiles' ],
96 [ SpecialLinkSearch::class, 'LinkSearch' ],
97 [ SpecialListRedirects::class, 'Listredirects' ],
98 [ SpecialLonelyPages::class, 'Lonelypages' ],
99 [ SpecialLongPages::class, 'Longpages' ],
100 [ SpecialMediaStatistics::class, 'MediaStatistics' ],
101 [ SpecialMIMESearch::class, 'MIMEsearch' ],
102 [ SpecialMostCategories::class, 'Mostcategories' ],
103 [ MostimagesPage::class, 'Mostimages' ],
104 [ SpecialMostInterwikis::class, 'Mostinterwikis' ],
105 [ SpecialMostLinkedCategories::class, 'Mostlinkedcategories' ],
106 [ SpecialMostLinkedTemplates::class, 'Mostlinkedtemplates' ],
107 [ SpecialMostLinked::class, 'Mostlinked' ],
108 [ SpecialMostRevisions::class, 'Mostrevisions' ],
109 [ SpecialFewestRevisions::class, 'Fewestrevisions' ],
110 [ SpecialShortPages::class, 'Shortpages' ],
111 [ SpecialUncategorizedCategories::class, 'Uncategorizedcategories' ],
112 [ SpecialUncategorizedPages::class, 'Uncategorizedpages' ],
113 [ SpecialUncategorizedImages::class, 'Uncategorizedimages' ],
114 [ SpecialUncategorizedTemplates::class, 'Uncategorizedtemplates' ],
115 [ SpecialUnusedCategories::class, 'Unusedcategories' ],
116 [ SpecialUnusedImages::class, 'Unusedimages' ],
117 [ SpecialWantedCategories::class, 'Wantedcategories' ],
118 [ WantedFilesPage::class, 'Wantedfiles' ],
119 [ WantedPagesPage::class, 'Wantedpages' ],
120 [ SpecialWantedTemplates::class, 'Wantedtemplates' ],
121 [ SpecialUnwatchedPages::class, 'Unwatchedpages' ],
122 [ SpecialUnusedTemplates::class, 'Unusedtemplates' ],
123 [ SpecialWithoutInterwiki::class, 'Withoutinterwiki' ],
124 ];
125 Hooks::runner()->onWgQueryPages( $qp );
126 }
127
128 return $qp;
129 }
130
136 $this->linkBatchFactory = $linkBatchFactory;
137 }
138
143 final protected function getLinkBatchFactory(): LinkBatchFactory {
144 if ( $this->linkBatchFactory === null ) {
145 // Fallback if not provided
146 // TODO Change to wfWarn in a future release
147 $this->linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
148 }
150 }
151
157 public static function getDisabledQueryPages( Config $config ) {
158 $disableQueryPageUpdate = $config->get( 'DisableQueryPageUpdate' );
159
160 if ( !is_array( $disableQueryPageUpdate ) ) {
161 return [];
162 }
163
164 $pages = [];
165 foreach ( $disableQueryPageUpdate as $name => $runMode ) {
166 if ( is_int( $name ) ) {
167 // The run mode may be omitted
168 $pages[$runMode] = 'disabled';
169 } else {
170 $pages[$name] = $runMode;
171 }
172 }
173 return $pages;
174 }
175
181 protected function setListoutput( $bool ) {
182 $this->listoutput = $bool;
183 }
184
212 public function getQueryInfo() {
213 return null;
214 }
215
223 protected function getSQL() {
224 /* Implement getQueryInfo() instead */
225 throw new MWException( "Bug in a QueryPage: doesn't implement getQueryInfo() nor "
226 . "getQuery() properly" );
227 }
228
237 protected function getOrderFields() {
238 return [ 'value' ];
239 }
240
252 public function usesTimestamps() {
253 return false;
254 }
255
262 protected function sortDescending() {
263 return true;
264 }
265
274 public function isExpensive() {
275 return $this->getConfig()->get( 'DisableQueryPages' );
276 }
277
286 public function isCacheable() {
287 return true;
288 }
289
297 public function isCached() {
298 return $this->isExpensive() && $this->getConfig()->get( 'MiserMode' );
299 }
300
307 public function isSyndicated() {
308 return true;
309 }
310
320 abstract protected function formatResult( $skin, $result );
321
328 protected function getPageHeader() {
329 return '';
330 }
331
339 protected function showEmptyText() {
340 $this->getOutput()->addWikiMsg( 'specialpage-empty' );
341 }
342
351 protected function linkParameters() {
352 return [];
353 }
354
365 public function recache( $limit, $ignoreErrors = true ) {
366 if ( !$this->isCacheable() ) {
367 return 0;
368 }
369
370 $fname = static::class . '::recache';
371 $dbw = $this->getDBLoadBalancer()->getConnectionRef( ILoadBalancer::DB_PRIMARY );
372
373 try {
374 // Do query
375 $res = $this->reallyDoQuery( $limit, false );
376 $num = false;
377 if ( $res ) {
378 $num = $res->numRows();
379 // Fetch results
380 $vals = [];
381 foreach ( $res as $i => $row ) {
382 if ( isset( $row->value ) ) {
383 if ( $this->usesTimestamps() ) {
384 $value = wfTimestamp( TS_UNIX,
385 $row->value );
386 } else {
387 $value = intval( $row->value ); // T16414
388 }
389 } else {
390 $value = $i;
391 }
392
393 $vals[] = [
394 'qc_type' => $this->getName(),
395 'qc_namespace' => $row->namespace,
396 'qc_title' => $row->title,
397 'qc_value' => $value
398 ];
399 }
400
401 $dbw->doAtomicSection(
402 __METHOD__,
403 function ( IDatabase $dbw, $fname ) use ( $vals ) {
404 // Clear out any old cached data
405 $dbw->delete( 'querycache',
406 [ 'qc_type' => $this->getName() ],
407 $fname
408 );
409 // Save results into the querycache table on the primary DB
410 if ( count( $vals ) ) {
411 $dbw->insert( 'querycache', $vals, $fname );
412 }
413 // Update the querycache_info record for the page
414 $dbw->delete( 'querycache_info',
415 [ 'qci_type' => $this->getName() ],
416 $fname
417 );
418 $dbw->insert( 'querycache_info',
419 [ 'qci_type' => $this->getName(),
420 'qci_timestamp' => $dbw->timestamp() ],
421 $fname
422 );
423 }
424 );
425 }
426 } catch ( DBError $e ) {
427 if ( !$ignoreErrors ) {
428 throw $e; // report query error
429 }
430 $num = false; // set result to false to indicate error
431 }
432
433 return $num;
434 }
435
441 protected function getRecacheDB() {
442 return $this->getDBLoadBalancer()
443 ->getConnectionRef( ILoadBalancer::DB_REPLICA, [ $this->getName(), 'QueryPage::recache', 'vslow' ] );
444 }
445
452 public function delete( LinkTarget $title ) {
453 if ( $this->isCached() ) {
454 $dbw = $this->getDBLoadBalancer()->getConnectionRef( ILoadBalancer::DB_PRIMARY );
455 $dbw->delete( 'querycache', [
456 'qc_type' => $this->getName(),
457 'qc_namespace' => $title->getNamespace(),
458 'qc_title' => $title->getDBkey(),
459 ], __METHOD__ );
460 }
461 }
462
468 public function deleteAllCachedData() {
469 $fname = static::class . '::' . __FUNCTION__;
470 $dbw = $this->getDBLoadBalancer()->getConnectionRef( ILoadBalancer::DB_PRIMARY );
471 $dbw->delete( 'querycache',
472 [ 'qc_type' => $this->getName() ],
473 $fname
474 );
475 $dbw->delete( 'querycachetwo',
476 [ 'qcc_type' => $this->getName() ],
477 $fname
478 );
479 $dbw->delete( 'querycache_info',
480 [ 'qci_type' => $this->getName() ],
481 $fname
482 );
483 }
484
493 public function reallyDoQuery( $limit, $offset = false ) {
494 $fname = static::class . '::reallyDoQuery';
495 $dbr = $this->getRecacheDB();
496 $query = $this->getQueryInfo();
497 $order = $this->getOrderFields();
498
499 if ( $this->sortDescending() ) {
500 foreach ( $order as &$field ) {
501 $field .= ' DESC';
502 }
503 }
504
505 if ( is_array( $query ) ) {
506 $tables = isset( $query['tables'] ) ? (array)$query['tables'] : [];
507 $fields = isset( $query['fields'] ) ? (array)$query['fields'] : [];
508 $conds = isset( $query['conds'] ) ? (array)$query['conds'] : [];
509 $options = isset( $query['options'] ) ? (array)$query['options'] : [];
510 $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : [];
511
512 if ( $order ) {
513 $options['ORDER BY'] = $order;
514 }
515
516 if ( $limit !== false ) {
517 $options['LIMIT'] = intval( $limit );
518 }
519
520 if ( $offset !== false ) {
521 $options['OFFSET'] = intval( $offset );
522 }
523
524 $res = $dbr->select( $tables, $fields, $conds, $fname,
525 $options, $join_conds
526 );
527 } else {
528 // Old-fashioned raw SQL style, deprecated
529 $sql = $this->getSQL();
530 $sql .= ' ORDER BY ' . implode( ', ', $order );
531 $sql = $dbr->limitResult( $sql, $limit, $offset );
532 $res = $dbr->query( $sql, $fname );
533 }
534
535 return $res;
536 }
537
544 public function doQuery( $offset = false, $limit = false ) {
545 if ( $this->isCached() && $this->isCacheable() ) {
546 return $this->fetchFromCache( $limit, $offset );
547 } else {
548 return $this->reallyDoQuery( $limit, $offset );
549 }
550 }
551
561 public function fetchFromCache( $limit, $offset = false ) {
562 $dbr = $this->getDBLoadBalancer()->getConnectionRef( ILoadBalancer::DB_REPLICA );
563 $options = [];
564
565 if ( $limit !== false ) {
566 $options['LIMIT'] = intval( $limit );
567 }
568
569 if ( $offset !== false ) {
570 $options['OFFSET'] = intval( $offset );
571 }
572
573 $order = $this->getCacheOrderFields();
574 if ( $this->sortDescending() ) {
575 foreach ( $order as &$field ) {
576 $field .= " DESC";
577 }
578 }
579 if ( $order ) {
580 $options['ORDER BY'] = $order;
581 }
582
583 return $dbr->select( 'querycache',
584 [ 'qc_type',
585 'namespace' => 'qc_namespace',
586 'title' => 'qc_title',
587 'value' => 'qc_value' ],
588 [ 'qc_type' => $this->getName() ],
589 __METHOD__,
590 $options
591 );
592 }
593
601 protected function getCacheOrderFields() {
602 return [ 'value' ];
603 }
604
608 public function getCachedTimestamp() {
609 if ( $this->cachedTimestamp === null ) {
610 $dbr = $this->getDBLoadBalancer()->getConnectionRef( ILoadBalancer::DB_REPLICA );
611 $fname = static::class . '::getCachedTimestamp';
612 $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
613 [ 'qci_type' => $this->getName() ], $fname );
614 }
615 return $this->cachedTimestamp;
616 }
617
630 protected function getLimitOffset() {
631 list( $limit, $offset ) = $this->getRequest()
632 ->getLimitOffsetForUser( $this->getUser() );
633 if ( $this->getConfig()->get( 'MiserMode' ) ) {
634 $maxResults = $this->getMaxResults();
635 // Can't display more than max results on a page
636 $limit = min( $limit, $maxResults );
637 // Can't skip over more than the end of $maxResults
638 $offset = min( $offset, $maxResults + 1 );
639 }
640 return [ $limit, $offset ];
641 }
642
651 protected function getDBLimit( $uiLimit, $uiOffset ) {
652 $maxResults = $this->getMaxResults();
653 if ( $this->getConfig()->get( 'MiserMode' ) ) {
654 $limit = min( $uiLimit + 1, $maxResults - $uiOffset );
655 return max( $limit, 0 );
656 } else {
657 return $uiLimit + 1;
658 }
659 }
660
671 protected function getMaxResults() {
672 // Max of 10000, unless we store more than 10000 in query cache.
673 return max( $this->getConfig()->get( 'QueryCacheLimit' ), 10000 );
674 }
675
682 public function execute( $par ) {
683 $this->checkPermissions();
684
685 $this->setHeaders();
686 $this->outputHeader();
687
688 $out = $this->getOutput();
689
690 if ( $this->isCached() && !$this->isCacheable() ) {
691 $out->addWikiMsg( 'querypage-disabled' );
692 return;
693 }
694
695 $out->setSyndicated( $this->isSyndicated() );
696
697 if ( $this->limit == 0 && $this->offset == 0 ) {
698 list( $this->limit, $this->offset ) = $this->getLimitOffset();
699 }
700 $dbLimit = $this->getDBLimit( $this->limit, $this->offset );
701 // @todo Use doQuery()
702 if ( !$this->isCached() ) {
703 // select one extra row for navigation
704 $res = $this->reallyDoQuery( $dbLimit, $this->offset );
705 } else {
706 // Get the cached result, select one extra row for navigation
707 $res = $this->fetchFromCache( $dbLimit, $this->offset );
708 if ( !$this->listoutput ) {
709 // Fetch the timestamp of this update
710 $ts = $this->getCachedTimestamp();
711 $lang = $this->getLanguage();
712 $maxResults = $lang->formatNum( $this->getConfig()->get( 'QueryCacheLimit' ) );
713
714 if ( $ts ) {
715 $user = $this->getUser();
716 $updated = $lang->userTimeAndDate( $ts, $user );
717 $updateddate = $lang->userDate( $ts, $user );
718 $updatedtime = $lang->userTime( $ts, $user );
719 $out->addMeta( 'Data-Cache-Time', $ts );
720 $out->addJsConfigVars( 'dataCacheTime', $ts );
721 $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
722 } else {
723 $out->addWikiMsg( 'perfcached', $maxResults );
724 }
725
726 // If updates on this page have been disabled, let the user know
727 // that the data set won't be refreshed for now
728 $disabledQueryPages = self::getDisabledQueryPages( $this->getConfig() );
729 if ( isset( $disabledQueryPages[$this->getName()] ) ) {
730 $runMode = $disabledQueryPages[$this->getName()];
731 if ( $runMode === 'disabled' ) {
732 $out->wrapWikiMsg(
733 "<div class=\"mw-querypage-no-updates\">\n$1\n</div>",
734 'querypage-no-updates'
735 );
736 } else {
737 // Messages used here: querypage-updates-periodical
738 $out->wrapWikiMsg(
739 "<div class=\"mw-querypage-updates-" . $runMode . "\">\n$1\n</div>",
740 'querypage-updates-' . $runMode
741 );
742 }
743 }
744 }
745 }
746
747 $this->numRows = $res->numRows();
748
749 $dbr = $this->getRecacheDB();
750 $this->preprocessResults( $dbr, $res );
751
752 $out->addHTML( Xml::openElement( 'div', [ 'class' => 'mw-spcontent' ] ) );
753
754 // Top header and navigation
755 if ( $this->shownavigation ) {
756 $out->addHTML( $this->getPageHeader() );
757 if ( $this->numRows > 0 ) {
758 $out->addHTML( $this->msg( 'showingresultsinrange' )->numParams(
759 min( $this->numRows, $this->limit ), // do not show the one extra row, if exist
760 $this->offset + 1, ( min( $this->numRows, $this->limit ) + $this->offset ) )->parseAsBlock() );
761 // Disable the "next" link when we reach the end
762 $miserMaxResults = $this->getConfig()->get( 'MiserMode' )
763 && ( $this->offset + $this->limit >= $this->getMaxResults() );
764 $atEnd = ( $this->numRows <= $this->limit ) || $miserMaxResults;
765 $paging = $this->buildPrevNextNavigation( $this->offset,
766 $this->limit, $this->linkParameters(), $atEnd, $par );
767 $out->addHTML( '<p>' . $paging . '</p>' );
768 } else {
769 // No results to show, so don't bother with "showing X of Y" etc.
770 // -- just let the user know and give up now
771 $this->showEmptyText();
772 $out->addHTML( Xml::closeElement( 'div' ) );
773 return;
774 }
775 }
776
777 // The actual results; specialist subclasses will want to handle this
778 // with more than a straight list, so we hand them the info, plus
779 // an OutputPage, and let them get on with it
780 $this->outputResults( $out,
781 $this->getSkin(),
782 $dbr, // Should use IResultWrapper for this
783 $res,
784 min( $this->numRows, $this->limit ), // do not format the one extra row, if exist
785 $this->offset );
786
787 // Repeat the paging links at the bottom
788 if ( $this->shownavigation ) {
789 $out->addHTML( '<p>' . $paging . '</p>' );
790 }
791
792 $out->addHTML( Xml::closeElement( 'div' ) );
793 }
794
808 protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
809 if ( $num > 0 ) {
810 $html = [];
811 if ( !$this->listoutput ) {
812 $html[] = $this->openList( $offset );
813 }
814
815 // $res might contain the whole 1,000 rows, so we read up to
816 // $num [should update this to use a Pager]
817 for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
818 $line = $this->formatResult( $skin, $row );
819 if ( $line ) {
820 $html[] = $this->listoutput
821 ? $line
822 : "<li>{$line}</li>\n";
823 }
824 }
825
826 if ( !$this->listoutput ) {
827 $html[] = $this->closeList();
828 }
829
830 $html = $this->listoutput
831 ? $this->getContentLanguage()->listToText( $html )
832 : implode( '', $html );
833
834 $out->addHTML( $html );
835 }
836 }
837
842 protected function openList( $offset ) {
843 return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
844 }
845
849 protected function closeList() {
850 return "</ol>\n";
851 }
852
859 protected function preprocessResults( $db, $res ) {
860 }
861
874 protected function executeLBFromResultWrapper( IResultWrapper $res, $ns = null ) {
875 if ( !$res->numRows() ) {
876 return;
877 }
878
879 $batch = $this->getLinkBatchFactory()->newLinkBatch();
880 foreach ( $res as $row ) {
881 $batch->add( $ns ?? $row->namespace, $row->title );
882 }
883 $batch->execute();
884
885 $res->seek( 0 );
886 }
887
892 final protected function setDBLoadBalancer( ILoadBalancer $loadBalancer ) {
893 $this->loadBalancer = $loadBalancer;
894 }
895
900 final protected function getDBLoadBalancer(): ILoadBalancer {
901 if ( $this->loadBalancer === null ) {
902 // Fallback if not provided
903 // TODO Change to wfWarn in a future release
904 $this->loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
905 }
906 return $this->loadBalancer;
907 }
908}
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:88
MediaWiki exception.
MediaWikiServices is the service locator for the application scope of MediaWiki.
This is a class for doing query pages; since they're almost all the same, we factor out some of the f...
Definition QueryPage.php:41
isExpensive()
Is this query expensive (for some definition of expensive)? Then we don't let it run in miser mode.
linkParameters()
If using extra form wheely-dealies, return a set of parameters here as an associative array.
getMaxResults()
Get max number of results we can return in miser mode.
doQuery( $offset=false, $limit=false)
Somewhat deprecated, you probably want to be using execute()
executeLBFromResultWrapper(IResultWrapper $res, $ns=null)
Creates a new LinkBatch object, adds all pages from the passed result wrapper (MUST include title and...
setListoutput( $bool)
A mutator for $this->listoutput;.
static getDisabledQueryPages(Config $config)
Get a list of query pages disabled and with it's run mode.
recache( $limit, $ignoreErrors=true)
Clear the cache and save new results.
setDBLoadBalancer(ILoadBalancer $loadBalancer)
LinkBatchFactory null $linkBatchFactory
Definition QueryPage.php:74
fetchFromCache( $limit, $offset=false)
Fetch the query results from the query cache.
int $offset
The offset and limit in use, as passed to the query() function.
Definition QueryPage.php:46
outputResults( $out, $skin, $dbr, $res, $num, $offset)
Format and output report results using the given information plus OutputPage.
isCached()
Whether or not the output of the page in question is retrieved from the database cache.
sortDescending()
Override to sort by increasing values.
deleteAllCachedData()
Remove all cached value This is needed when the page is no longer using the cache.
formatResult( $skin, $result)
Formats the results of the query for display.
isSyndicated()
Sometime we don't want to build rss / atom feeds.
getCachedTimestamp()
static getPages()
Get a list of query page classes and their associated special pages, for periodic updates.
Definition QueryPage.php:85
bool $shownavigation
Whether to show prev/next links.
Definition QueryPage.php:68
reallyDoQuery( $limit, $offset=false)
Run the query and return the result.
isCacheable()
Is the output of this query cacheable? Non-cacheable expensive pages will be disabled in miser mode a...
getOrderFields()
Subclasses return an array of fields to order by here.
getLinkBatchFactory()
showEmptyText()
Outputs some kind of an informative message (via OutputPage) to let the user know that the query retu...
usesTimestamps()
Does this query return timestamps rather than integers in its 'value' field? If true,...
getCacheOrderFields()
Return the order fields for fetchFromCache.
getQueryInfo()
Subclasses return an SQL query here, formatted as an array with the following keys: tables => Table(s...
openList( $offset)
getDBLimit( $uiLimit, $uiOffset)
What is limit to fetch from DB.
int $numRows
The number of rows returned by the query.
Definition QueryPage.php:58
preprocessResults( $db, $res)
Do any necessary preprocessing of the result object.
setLinkBatchFactory(LinkBatchFactory $linkBatchFactory)
getSQL()
For back-compat, subclasses may return a raw SQL query here, as a string.
getPageHeader()
The content returned by this function will be output before any result.
bool $listoutput
Whether or not we want plain listoutput rather than an ordered list.
Definition QueryPage.php:43
execute( $par)
This is the actual workhorse.
string null false $cachedTimestamp
Definition QueryPage.php:63
getDBLoadBalancer()
ILoadBalancer null $loadBalancer
Definition QueryPage.php:71
getLimitOffset()
Returns limit and offset, as returned by $this->getRequest()->getLimitOffsetForUser().
getRecacheDB()
Get a DB connection to be used for slow recache queries.
Parent class for all special pages.
Database error base class @newable.
Definition DBError.php:32
Interface for configuration instances.
Definition Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
delete( $table, $conds, $fname=__METHOD__)
Delete all rows in a table that match a condition.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
insert( $table, $rows, $fname=__METHOD__, $options=[])
Insert the given row(s) into a table.
Database cluster connection, tracking, load balancing, and transaction manager interface.
Result wrapper for grabbing data queried from an IDatabase object.
$line
Definition mcc.php:119
if(!isset( $args[0])) $lang