MediaWiki REL1_35
QueryPage.php
Go to the documentation of this file.
1<?php
29
39abstract class QueryPage extends SpecialPage {
41 protected $listoutput = false;
42
44 protected $offset = 0;
45
47 protected $limit = 0;
48
56 protected $numRows;
57
61 protected $cachedTimestamp = null;
62
66 protected $shownavigation = true;
67
77 public static function getPages() {
78 static $qp = null;
79
80 if ( $qp === null ) {
81 // QueryPage subclass, Special page name
82 $qp = [
83 [ SpecialAncientPages::class, 'Ancientpages' ],
84 [ SpecialBrokenRedirects::class, 'BrokenRedirects' ],
85 [ SpecialDeadendPages::class, 'Deadendpages' ],
86 [ SpecialDoubleRedirects::class, 'DoubleRedirects' ],
87 [ SpecialFileDuplicateSearch::class, 'FileDuplicateSearch' ],
88 [ SpecialListDuplicatedFiles::class, 'ListDuplicatedFiles' ],
89 [ SpecialLinkSearch::class, 'LinkSearch' ],
90 [ SpecialListRedirects::class, 'Listredirects' ],
91 [ SpecialLonelyPages::class, 'Lonelypages' ],
92 [ SpecialLongPages::class, 'Longpages' ],
93 [ SpecialMediaStatistics::class, 'MediaStatistics' ],
94 [ SpecialMIMESearch::class, 'MIMEsearch' ],
95 [ SpecialMostCategories::class, 'Mostcategories' ],
96 [ MostimagesPage::class, 'Mostimages' ],
97 [ SpecialMostInterwikis::class, 'Mostinterwikis' ],
98 [ SpecialMostLinkedCategories::class, 'Mostlinkedcategories' ],
99 [ SpecialMostLinkedTemplates::class, 'Mostlinkedtemplates' ],
100 [ SpecialMostLinked::class, 'Mostlinked' ],
101 [ SpecialMostRevisions::class, 'Mostrevisions' ],
102 [ SpecialFewestRevisions::class, 'Fewestrevisions' ],
103 [ SpecialShortPages::class, 'Shortpages' ],
104 [ SpecialUncategorizedCategories::class, 'Uncategorizedcategories' ],
105 [ SpecialUncategorizedPages::class, 'Uncategorizedpages' ],
106 [ SpecialUncategorizedImages::class, 'Uncategorizedimages' ],
107 [ SpecialUncategorizedTemplates::class, 'Uncategorizedtemplates' ],
108 [ SpecialUnusedCategories::class, 'Unusedcategories' ],
109 [ SpecialUnusedImages::class, 'Unusedimages' ],
110 [ SpecialWantedCategories::class, 'Wantedcategories' ],
111 [ WantedFilesPage::class, 'Wantedfiles' ],
112 [ WantedPagesPage::class, 'Wantedpages' ],
113 [ SpecialWantedTemplates::class, 'Wantedtemplates' ],
114 [ SpecialUnwatchedPages::class, 'Unwatchedpages' ],
115 [ SpecialUnusedTemplates::class, 'Unusedtemplates' ],
116 [ SpecialWithoutInterwiki::class, 'Withoutinterwiki' ],
117 ];
118 Hooks::runner()->onWgQueryPages( $qp );
119 }
120
121 return $qp;
122 }
123
129 public static function getDisabledQueryPages( Config $config ) {
130 $disableQueryPageUpdate = $config->get( 'DisableQueryPageUpdate' );
131
132 if ( !is_array( $disableQueryPageUpdate ) ) {
133 return [];
134 }
135
136 $pages = [];
137 foreach ( $disableQueryPageUpdate as $name => $runMode ) {
138 if ( is_int( $name ) ) {
139 // The run mode may be omitted
140 $pages[$runMode] = 'disabled';
141 } else {
142 $pages[$name] = $runMode;
143 }
144 }
145 return $pages;
146 }
147
153 protected function setListoutput( $bool ) {
154 $this->listoutput = $bool;
155 }
156
184 public function getQueryInfo() {
185 return null;
186 }
187
194 protected function getSQL() {
195 /* Implement getQueryInfo() instead */
196 throw new MWException( "Bug in a QueryPage: doesn't implement getQueryInfo() nor "
197 . "getQuery() properly" );
198 }
199
208 protected function getOrderFields() {
209 return [ 'value' ];
210 }
211
223 public function usesTimestamps() {
224 return false;
225 }
226
233 protected function sortDescending() {
234 return true;
235 }
236
245 public function isExpensive() {
246 return $this->getConfig()->get( 'DisableQueryPages' );
247 }
248
257 public function isCacheable() {
258 return true;
259 }
260
268 public function isCached() {
269 return $this->isExpensive() && $this->getConfig()->get( 'MiserMode' );
270 }
271
278 public function isSyndicated() {
279 return true;
280 }
281
291 abstract protected function formatResult( $skin, $result );
292
299 protected function getPageHeader() {
300 return '';
301 }
302
310 protected function showEmptyText() {
311 $this->getOutput()->addWikiMsg( 'specialpage-empty' );
312 }
313
322 protected function linkParameters() {
323 return [];
324 }
325
336 public function recache( $limit, $ignoreErrors = true ) {
337 if ( !$this->isCacheable() ) {
338 return 0;
339 }
340
341 $fname = static::class . '::recache';
342 $dbw = wfGetDB( DB_MASTER );
343 if ( !$dbw ) {
344 return false;
345 }
346
347 try {
348 # Do query
349 $res = $this->reallyDoQuery( $limit, false );
350 $num = false;
351 if ( $res ) {
352 $num = $res->numRows();
353 # Fetch results
354 $vals = [];
355 foreach ( $res as $i => $row ) {
356 if ( isset( $row->value ) ) {
357 if ( $this->usesTimestamps() ) {
358 $value = wfTimestamp( TS_UNIX,
359 $row->value );
360 } else {
361 $value = intval( $row->value ); // T16414
362 }
363 } else {
364 $value = $i;
365 }
366
367 $vals[] = [
368 'qc_type' => $this->getName(),
369 'qc_namespace' => $row->namespace,
370 'qc_title' => $row->title,
371 'qc_value' => $value
372 ];
373 }
374
375 $dbw->doAtomicSection(
376 __METHOD__,
377 function ( IDatabase $dbw, $fname ) use ( $vals ) {
378 # Clear out any old cached data
379 $dbw->delete( 'querycache',
380 [ 'qc_type' => $this->getName() ],
381 $fname
382 );
383 # Save results into the querycache table on the master
384 if ( count( $vals ) ) {
385 $dbw->insert( 'querycache', $vals, $fname );
386 }
387 # Update the querycache_info record for the page
388 $dbw->delete( 'querycache_info',
389 [ 'qci_type' => $this->getName() ],
390 $fname
391 );
392 $dbw->insert( 'querycache_info',
393 [ 'qci_type' => $this->getName(),
394 'qci_timestamp' => $dbw->timestamp() ],
395 $fname
396 );
397 }
398 );
399 }
400 } catch ( DBError $e ) {
401 if ( !$ignoreErrors ) {
402 throw $e; // report query error
403 }
404 $num = false; // set result to false to indicate error
405 }
406
407 return $num;
408 }
409
415 protected function getRecacheDB() {
416 return wfGetDB( DB_REPLICA, [ $this->getName(), 'QueryPage::recache', 'vslow' ] );
417 }
418
425 public function delete( LinkTarget $title ) {
426 if ( $this->isCached() ) {
427 $dbw = wfGetDB( DB_MASTER );
428 $dbw->delete( 'querycache', [
429 'qc_type' => $this->getName(),
430 'qc_namespace' => $title->getNamespace(),
431 'qc_title' => $title->getDBkey(),
432 ], __METHOD__ );
433 }
434 }
435
444 public function reallyDoQuery( $limit, $offset = false ) {
445 $fname = static::class . '::reallyDoQuery';
446 $dbr = $this->getRecacheDB();
447 $query = $this->getQueryInfo();
448 $order = $this->getOrderFields();
449
450 if ( $this->sortDescending() ) {
451 foreach ( $order as &$field ) {
452 $field .= ' DESC';
453 }
454 }
455
456 if ( is_array( $query ) ) {
457 $tables = isset( $query['tables'] ) ? (array)$query['tables'] : [];
458 $fields = isset( $query['fields'] ) ? (array)$query['fields'] : [];
459 $conds = isset( $query['conds'] ) ? (array)$query['conds'] : [];
460 $options = isset( $query['options'] ) ? (array)$query['options'] : [];
461 $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : [];
462
463 if ( $order ) {
464 $options['ORDER BY'] = $order;
465 }
466
467 if ( $limit !== false ) {
468 $options['LIMIT'] = intval( $limit );
469 }
470
471 if ( $offset !== false ) {
472 $options['OFFSET'] = intval( $offset );
473 }
474
475 $res = $dbr->select( $tables, $fields, $conds, $fname,
476 $options, $join_conds
477 );
478 } else {
479 // Old-fashioned raw SQL style, deprecated
480 $sql = $this->getSQL();
481 $sql .= ' ORDER BY ' . implode( ', ', $order );
482 $sql = $dbr->limitResult( $sql, $limit, $offset );
483 $res = $dbr->query( $sql, $fname );
484 }
485
486 return $res;
487 }
488
495 public function doQuery( $offset = false, $limit = false ) {
496 if ( $this->isCached() && $this->isCacheable() ) {
497 return $this->fetchFromCache( $limit, $offset );
498 } else {
499 return $this->reallyDoQuery( $limit, $offset );
500 }
501 }
502
512 public function fetchFromCache( $limit, $offset = false ) {
514 $options = [];
515
516 if ( $limit !== false ) {
517 $options['LIMIT'] = intval( $limit );
518 }
519
520 if ( $offset !== false ) {
521 $options['OFFSET'] = intval( $offset );
522 }
523
524 $order = $this->getCacheOrderFields();
525 if ( $this->sortDescending() ) {
526 foreach ( $order as &$field ) {
527 $field .= " DESC";
528 }
529 }
530 if ( $order ) {
531 $options['ORDER BY'] = $order;
532 }
533
534 return $dbr->select( 'querycache',
535 [ 'qc_type',
536 'namespace' => 'qc_namespace',
537 'title' => 'qc_title',
538 'value' => 'qc_value' ],
539 [ 'qc_type' => $this->getName() ],
540 __METHOD__,
541 $options
542 );
543 }
544
552 protected function getCacheOrderFields() {
553 return [ 'value' ];
554 }
555
559 public function getCachedTimestamp() {
560 if ( $this->cachedTimestamp === null ) {
562 $fname = static::class . '::getCachedTimestamp';
563 $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
564 [ 'qci_type' => $this->getName() ], $fname );
565 }
567 }
568
581 protected function getLimitOffset() {
582 list( $limit, $offset ) = $this->getRequest()
583 ->getLimitOffsetForUser( $this->getUser() );
584 if ( $this->getConfig()->get( 'MiserMode' ) ) {
585 $maxResults = $this->getMaxResults();
586 // Can't display more than max results on a page
587 $limit = min( $limit, $maxResults );
588 // Can't skip over more than the end of $maxResults
589 $offset = min( $offset, $maxResults + 1 );
590 }
591 return [ $limit, $offset ];
592 }
593
602 protected function getDBLimit( $uiLimit, $uiOffset ) {
603 $maxResults = $this->getMaxResults();
604 if ( $this->getConfig()->get( 'MiserMode' ) ) {
605 $limit = min( $uiLimit + 1, $maxResults - $uiOffset );
606 return max( $limit, 0 );
607 } else {
608 return $uiLimit + 1;
609 }
610 }
611
622 protected function getMaxResults() {
623 // Max of 10000, unless we store more than 10000 in query cache.
624 return max( $this->getConfig()->get( 'QueryCacheLimit' ), 10000 );
625 }
626
633 public function execute( $par ) {
634 $this->checkPermissions();
635
636 $this->setHeaders();
637 $this->outputHeader();
638
639 $out = $this->getOutput();
640
641 if ( $this->isCached() && !$this->isCacheable() ) {
642 $out->addWikiMsg( 'querypage-disabled' );
643 return;
644 }
645
646 $out->setSyndicated( $this->isSyndicated() );
647
648 if ( $this->limit == 0 && $this->offset == 0 ) {
649 list( $this->limit, $this->offset ) = $this->getLimitOffset();
650 }
651 $dbLimit = $this->getDBLimit( $this->limit, $this->offset );
652 // @todo Use doQuery()
653 if ( !$this->isCached() ) {
654 # select one extra row for navigation
655 $res = $this->reallyDoQuery( $dbLimit, $this->offset );
656 } else {
657 # Get the cached result, select one extra row for navigation
658 $res = $this->fetchFromCache( $dbLimit, $this->offset );
659 if ( !$this->listoutput ) {
660 # Fetch the timestamp of this update
661 $ts = $this->getCachedTimestamp();
662 $lang = $this->getLanguage();
663 $maxResults = $lang->formatNum( $this->getConfig()->get( 'QueryCacheLimit' ) );
664
665 if ( $ts ) {
666 $user = $this->getUser();
667 $updated = $lang->userTimeAndDate( $ts, $user );
668 $updateddate = $lang->userDate( $ts, $user );
669 $updatedtime = $lang->userTime( $ts, $user );
670 $out->addMeta( 'Data-Cache-Time', $ts );
671 $out->addJsConfigVars( 'dataCacheTime', $ts );
672 $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
673 } else {
674 $out->addWikiMsg( 'perfcached', $maxResults );
675 }
676
677 # If updates on this page have been disabled, let the user know
678 # that the data set won't be refreshed for now
679 $disabledQueryPages = self::getDisabledQueryPages( $this->getConfig() );
680 if ( isset( $disabledQueryPages[$this->getName()] ) ) {
681 $runMode = $disabledQueryPages[$this->getName()];
682 if ( $runMode === 'disabled' ) {
683 $out->wrapWikiMsg(
684 "<div class=\"mw-querypage-no-updates\">\n$1\n</div>",
685 'querypage-no-updates'
686 );
687 } else {
688 // Messages used here: querypage-updates-periodical
689 $out->wrapWikiMsg(
690 "<div class=\"mw-querypage-updates-" . $runMode . "\">\n$1\n</div>",
691 'querypage-updates-' . $runMode
692 );
693 }
694 }
695 }
696 }
697
698 $this->numRows = $res->numRows();
699
700 $dbr = $this->getRecacheDB();
701 $this->preprocessResults( $dbr, $res );
702
703 $out->addHTML( Xml::openElement( 'div', [ 'class' => 'mw-spcontent' ] ) );
704
705 # Top header and navigation
706 if ( $this->shownavigation ) {
707 $out->addHTML( $this->getPageHeader() );
708 if ( $this->numRows > 0 ) {
709 $out->addHTML( $this->msg( 'showingresultsinrange' )->numParams(
710 min( $this->numRows, $this->limit ), # do not show the one extra row, if exist
711 $this->offset + 1, ( min( $this->numRows, $this->limit ) + $this->offset ) )->parseAsBlock() );
712 # Disable the "next" link when we reach the end
713 $miserMaxResults = $this->getConfig()->get( 'MiserMode' )
714 && ( $this->offset + $this->limit >= $this->getMaxResults() );
715 $atEnd = ( $this->numRows <= $this->limit ) || $miserMaxResults;
716 $paging = $this->buildPrevNextNavigation( $this->offset,
717 $this->limit, $this->linkParameters(), $atEnd, $par );
718 $out->addHTML( '<p>' . $paging . '</p>' );
719 } else {
720 # No results to show, so don't bother with "showing X of Y" etc.
721 # -- just let the user know and give up now
722 $this->showEmptyText();
723 $out->addHTML( Xml::closeElement( 'div' ) );
724 return;
725 }
726 }
727
728 # The actual results; specialist subclasses will want to handle this
729 # with more than a straight list, so we hand them the info, plus
730 # an OutputPage, and let them get on with it
731 $this->outputResults( $out,
732 $this->getSkin(),
733 $dbr, # Should use IResultWrapper for this
734 $res,
735 min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
736 $this->offset );
737
738 # Repeat the paging links at the bottom
739 if ( $this->shownavigation ) {
740 $out->addHTML( '<p>' . $paging . '</p>' );
741 }
742
743 $out->addHTML( Xml::closeElement( 'div' ) );
744 }
745
759 protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
760 if ( $num > 0 ) {
761 $html = [];
762 if ( !$this->listoutput ) {
763 $html[] = $this->openList( $offset );
764 }
765
766 # $res might contain the whole 1,000 rows, so we read up to
767 # $num [should update this to use a Pager]
768 for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
769 $line = $this->formatResult( $skin, $row );
770 if ( $line ) {
771 $html[] = $this->listoutput
772 ? $line
773 : "<li>{$line}</li>\n";
774 }
775 }
776
777 if ( !$this->listoutput ) {
778 $html[] = $this->closeList();
779 }
780
781 $html = $this->listoutput
782 ? MediaWikiServices::getInstance()->getContentLanguage()->listToText( $html )
783 : implode( '', $html );
784
785 $out->addHTML( $html );
786 }
787 }
788
793 protected function openList( $offset ) {
794 return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
795 }
796
800 protected function closeList() {
801 return "</ol>\n";
802 }
803
810 protected function preprocessResults( $db, $res ) {
811 }
812
823 protected function executeLBFromResultWrapper( IResultWrapper $res, $ns = null ) {
824 if ( !$res->numRows() ) {
825 return;
826 }
827
828 $batch = new LinkBatch;
829 foreach ( $res as $row ) {
830 $batch->add( $ns ?? $row->namespace, $row->title );
831 }
832 $batch->execute();
833
834 $res->seek( 0 );
835 }
836}
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition LinkBatch.php:35
add( $ns, $dbkey)
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:39
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.
fetchFromCache( $limit, $offset=false)
Fetch the query results from the query cache Stable to override.
int $offset
The offset and limit in use, as passed to the query() function.
Definition QueryPage.php:44
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.
formatResult( $skin, $result)
Formats the results of the query for display.
isSyndicated()
Sometime we don't want to build rss / atom feeds.
getCachedTimestamp()
string null $cachedTimestamp
Definition QueryPage.php:61
static getPages()
Get a list of query page classes and their associated special pages, for periodic updates.
Definition QueryPage.php:77
bool $shownavigation
Whether to show prev/next links.
Definition QueryPage.php:66
reallyDoQuery( $limit, $offset=false)
Run the query and return the result Stable to override.
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.
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:56
preprocessResults( $db, $res)
Do any necessary preprocessing of the result object.
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:41
execute( $par)
This is the actual workhorse.
getLimitOffset()
Returns limit and offset, as returned by $this->getRequest()->getLimitOffsetForUser().
getRecacheDB()
Get a DB connection to be used for slow recache queries Stable to override.
Parent class for all special pages.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
getName()
Get the name of this Special Page.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!...
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
buildPrevNextNavigation( $offset, $limit, array $query=[], $atend=false, $subpage=false)
Generate (prev x| next x) (20|50|100...) type links for paging.
getSkin()
Shortcut to get the skin being used for this instance.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
getLanguage()
Shortcut to get user's language.
Database error base class @newable Stable to extend.
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.
Result wrapper for grabbing data queried from an IDatabase object.
$line
Definition mcc.php:119
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:29
if(!isset( $args[0])) $lang