MediaWiki REL1_34
QueryPage.php
Go to the documentation of this file.
1<?php
29
36abstract class QueryPage extends SpecialPage {
38 protected $listoutput = false;
39
41 protected $offset = 0;
42
44 protected $limit = 0;
45
53 protected $numRows;
54
58 protected $cachedTimestamp = null;
59
63 protected $shownavigation = true;
64
74 public static function getPages() {
75 static $qp = null;
76
77 if ( $qp === null ) {
78 // QueryPage subclass, Special page name
79 $qp = [
80 [ SpecialAncientPages::class, 'Ancientpages' ],
81 [ SpecialBrokenRedirects::class, 'BrokenRedirects' ],
82 [ SpecialDeadendPages::class, 'Deadendpages' ],
83 [ SpecialDoubleRedirects::class, 'DoubleRedirects' ],
84 [ SpecialFileDuplicateSearch::class, 'FileDuplicateSearch' ],
85 [ SpecialListDuplicatedFiles::class, 'ListDuplicatedFiles' ],
86 [ SpecialLinkSearch::class, 'LinkSearch' ],
87 [ SpecialListRedirects::class, 'Listredirects' ],
88 [ SpecialLonelyPages::class, 'Lonelypages' ],
89 [ SpecialLongPages::class, 'Longpages' ],
90 [ SpecialMediaStatistics::class, 'MediaStatistics' ],
91 [ SpecialMIMESearch::class, 'MIMEsearch' ],
92 [ SpecialMostCategories::class, 'Mostcategories' ],
93 [ MostimagesPage::class, 'Mostimages' ],
94 [ SpecialMostInterwikis::class, 'Mostinterwikis' ],
95 [ SpecialMostLinkedCategories::class, 'Mostlinkedcategories' ],
96 [ SpecialMostLinkedTemplates::class, 'Mostlinkedtemplates' ],
97 [ SpecialMostLinked::class, 'Mostlinked' ],
98 [ SpecialMostRevisions::class, 'Mostrevisions' ],
99 [ SpecialFewestRevisions::class, 'Fewestrevisions' ],
100 [ SpecialShortPages::class, 'Shortpages' ],
101 [ SpecialUncategorizedCategories::class, 'Uncategorizedcategories' ],
102 [ SpecialUncategorizedPages::class, 'Uncategorizedpages' ],
103 [ SpecialUncategorizedImages::class, 'Uncategorizedimages' ],
104 [ SpecialUncategorizedTemplates::class, 'Uncategorizedtemplates' ],
105 [ SpecialUnusedCategories::class, 'Unusedcategories' ],
106 [ SpecialUnusedImages::class, 'Unusedimages' ],
107 [ SpecialWantedCategories::class, 'Wantedcategories' ],
108 [ WantedFilesPage::class, 'Wantedfiles' ],
109 [ WantedPagesPage::class, 'Wantedpages' ],
110 [ SpecialWantedTemplates::class, 'Wantedtemplates' ],
111 [ SpecialUnwatchedPages::class, 'Unwatchedpages' ],
112 [ SpecialUnusedTemplates::class, 'Unusedtemplates' ],
113 [ SpecialWithoutInterwiki::class, 'Withoutinterwiki' ],
114 ];
115 Hooks::run( 'wgQueryPages', [ &$qp ] );
116 }
117
118 return $qp;
119 }
120
126 public static function getDisabledQueryPages( Config $config ) {
127 $disableQueryPageUpdate = $config->get( 'DisableQueryPageUpdate' );
128
129 if ( !is_array( $disableQueryPageUpdate ) ) {
130 return [];
131 }
132
133 $pages = [];
134 foreach ( $disableQueryPageUpdate as $name => $runMode ) {
135 if ( is_int( $name ) ) {
136 // The run mode may be omitted
137 $pages[$runMode] = 'disabled';
138 } else {
139 $pages[$name] = $runMode;
140 }
141 }
142 return $pages;
143 }
144
150 function setListoutput( $bool ) {
151 $this->listoutput = $bool;
152 }
153
180 public function getQueryInfo() {
181 return null;
182 }
183
190 function getSQL() {
191 /* Implement getQueryInfo() instead */
192 throw new MWException( "Bug in a QueryPage: doesn't implement getQueryInfo() nor "
193 . "getQuery() properly" );
194 }
195
203 function getOrderFields() {
204 return [ 'value' ];
205 }
206
217 public function usesTimestamps() {
218 return false;
219 }
220
226 function sortDescending() {
227 return true;
228 }
229
237 public function isExpensive() {
238 return $this->getConfig()->get( 'DisableQueryPages' );
239 }
240
248 public function isCacheable() {
249 return true;
250 }
251
258 public function isCached() {
259 return $this->isExpensive() && $this->getConfig()->get( 'MiserMode' );
260 }
261
267 function isSyndicated() {
268 return true;
269 }
270
280 abstract function formatResult( $skin, $result );
281
287 function getPageHeader() {
288 return '';
289 }
290
298 protected function showEmptyText() {
299 $this->getOutput()->addWikiMsg( 'specialpage-empty' );
300 }
301
309 function linkParameters() {
310 return [];
311 }
312
321 public function recache( $limit, $ignoreErrors = true ) {
322 if ( !$this->isCacheable() ) {
323 return 0;
324 }
325
326 $fname = static::class . '::recache';
327 $dbw = wfGetDB( DB_MASTER );
328 if ( !$dbw ) {
329 return false;
330 }
331
332 try {
333 # Do query
334 $res = $this->reallyDoQuery( $limit, false );
335 $num = false;
336 if ( $res ) {
337 $num = $res->numRows();
338 # Fetch results
339 $vals = [];
340 foreach ( $res as $i => $row ) {
341 if ( isset( $row->value ) ) {
342 if ( $this->usesTimestamps() ) {
343 $value = wfTimestamp( TS_UNIX,
344 $row->value );
345 } else {
346 $value = intval( $row->value ); // T16414
347 }
348 } else {
349 $value = $i;
350 }
351
352 $vals[] = [
353 'qc_type' => $this->getName(),
354 'qc_namespace' => $row->namespace,
355 'qc_title' => $row->title,
356 'qc_value' => $value
357 ];
358 }
359
360 $dbw->doAtomicSection(
361 __METHOD__,
362 function ( IDatabase $dbw, $fname ) use ( $vals ) {
363 # Clear out any old cached data
364 $dbw->delete( 'querycache',
365 [ 'qc_type' => $this->getName() ],
366 $fname
367 );
368 # Save results into the querycache table on the master
369 if ( count( $vals ) ) {
370 $dbw->insert( 'querycache', $vals, $fname );
371 }
372 # Update the querycache_info record for the page
373 $dbw->delete( 'querycache_info',
374 [ 'qci_type' => $this->getName() ],
375 $fname
376 );
377 $dbw->insert( 'querycache_info',
378 [ 'qci_type' => $this->getName(),
379 'qci_timestamp' => $dbw->timestamp() ],
380 $fname
381 );
382 }
383 );
384 }
385 } catch ( DBError $e ) {
386 if ( !$ignoreErrors ) {
387 throw $e; // report query error
388 }
389 $num = false; // set result to false to indicate error
390 }
391
392 return $num;
393 }
394
399 function getRecacheDB() {
400 return wfGetDB( DB_REPLICA, [ $this->getName(), 'QueryPage::recache', 'vslow' ] );
401 }
402
409 public function delete( LinkTarget $title ) {
410 if ( $this->isCached() ) {
411 $dbw = wfGetDB( DB_MASTER );
412 $dbw->delete( 'querycache', [
413 'qc_type' => $this->getName(),
414 'qc_namespace' => $title->getNamespace(),
415 'qc_title' => $title->getDBkey(),
416 ], __METHOD__ );
417 }
418 }
419
427 public function reallyDoQuery( $limit, $offset = false ) {
428 $fname = static::class . '::reallyDoQuery';
429 $dbr = $this->getRecacheDB();
430 $query = $this->getQueryInfo();
431 $order = $this->getOrderFields();
432
433 if ( $this->sortDescending() ) {
434 foreach ( $order as &$field ) {
435 $field .= ' DESC';
436 }
437 }
438
439 if ( is_array( $query ) ) {
440 $tables = isset( $query['tables'] ) ? (array)$query['tables'] : [];
441 $fields = isset( $query['fields'] ) ? (array)$query['fields'] : [];
442 $conds = isset( $query['conds'] ) ? (array)$query['conds'] : [];
443 $options = isset( $query['options'] ) ? (array)$query['options'] : [];
444 $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : [];
445
446 if ( $order ) {
447 $options['ORDER BY'] = $order;
448 }
449
450 if ( $limit !== false ) {
451 $options['LIMIT'] = intval( $limit );
452 }
453
454 if ( $offset !== false ) {
455 $options['OFFSET'] = intval( $offset );
456 }
457
458 $res = $dbr->select( $tables, $fields, $conds, $fname,
459 $options, $join_conds
460 );
461 } else {
462 // Old-fashioned raw SQL style, deprecated
463 $sql = $this->getSQL();
464 $sql .= ' ORDER BY ' . implode( ', ', $order );
465 $sql = $dbr->limitResult( $sql, $limit, $offset );
466 $res = $dbr->query( $sql, $fname );
467 }
468
469 return $res;
470 }
471
478 public function doQuery( $offset = false, $limit = false ) {
479 if ( $this->isCached() && $this->isCacheable() ) {
480 return $this->fetchFromCache( $limit, $offset );
481 } else {
482 return $this->reallyDoQuery( $limit, $offset );
483 }
484 }
485
493 public function fetchFromCache( $limit, $offset = false ) {
495 $options = [];
496
497 if ( $limit !== false ) {
498 $options['LIMIT'] = intval( $limit );
499 }
500
501 if ( $offset !== false ) {
502 $options['OFFSET'] = intval( $offset );
503 }
504
505 $order = $this->getCacheOrderFields();
506 if ( $this->sortDescending() ) {
507 foreach ( $order as &$field ) {
508 $field .= " DESC";
509 }
510 }
511 if ( $order ) {
512 $options['ORDER BY'] = $order;
513 }
514
515 return $dbr->select( 'querycache',
516 [ 'qc_type',
517 'namespace' => 'qc_namespace',
518 'title' => 'qc_title',
519 'value' => 'qc_value' ],
520 [ 'qc_type' => $this->getName() ],
521 __METHOD__,
522 $options
523 );
524 }
525
533 return [ 'value' ];
534 }
535
539 public function getCachedTimestamp() {
540 if ( is_null( $this->cachedTimestamp ) ) {
542 $fname = static::class . '::getCachedTimestamp';
543 $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
544 [ 'qci_type' => $this->getName() ], $fname );
545 }
547 }
548
561 protected function getLimitOffset() {
562 list( $limit, $offset ) = $this->getRequest()->getLimitOffset();
563 if ( $this->getConfig()->get( 'MiserMode' ) ) {
564 $maxResults = $this->getMaxResults();
565 // Can't display more than max results on a page
566 $limit = min( $limit, $maxResults );
567 // Can't skip over more than the end of $maxResults
568 $offset = min( $offset, $maxResults + 1 );
569 }
570 return [ $limit, $offset ];
571 }
572
581 protected function getDBLimit( $uiLimit, $uiOffset ) {
582 $maxResults = $this->getMaxResults();
583 if ( $this->getConfig()->get( 'MiserMode' ) ) {
584 $limit = min( $uiLimit + 1, $maxResults - $uiOffset );
585 return max( $limit, 0 );
586 } else {
587 return $uiLimit + 1;
588 }
589 }
590
600 protected function getMaxResults() {
601 // Max of 10000, unless we store more than 10000 in query cache.
602 return max( $this->getConfig()->get( 'QueryCacheLimit' ), 10000 );
603 }
604
610 public function execute( $par ) {
611 $user = $this->getUser();
612 if ( !$this->userCanExecute( $user ) ) {
614 return;
615 }
616
617 $this->setHeaders();
618 $this->outputHeader();
619
620 $out = $this->getOutput();
621
622 if ( $this->isCached() && !$this->isCacheable() ) {
623 $out->addWikiMsg( 'querypage-disabled' );
624 return;
625 }
626
627 $out->setSyndicated( $this->isSyndicated() );
628
629 if ( $this->limit == 0 && $this->offset == 0 ) {
630 list( $this->limit, $this->offset ) = $this->getLimitOffset();
631 }
632 $dbLimit = $this->getDBLimit( $this->limit, $this->offset );
633 // @todo Use doQuery()
634 if ( !$this->isCached() ) {
635 # select one extra row for navigation
636 $res = $this->reallyDoQuery( $dbLimit, $this->offset );
637 } else {
638 # Get the cached result, select one extra row for navigation
639 $res = $this->fetchFromCache( $dbLimit, $this->offset );
640 if ( !$this->listoutput ) {
641 # Fetch the timestamp of this update
642 $ts = $this->getCachedTimestamp();
643 $lang = $this->getLanguage();
644 $maxResults = $lang->formatNum( $this->getConfig()->get( 'QueryCacheLimit' ) );
645
646 if ( $ts ) {
647 $updated = $lang->userTimeAndDate( $ts, $user );
648 $updateddate = $lang->userDate( $ts, $user );
649 $updatedtime = $lang->userTime( $ts, $user );
650 $out->addMeta( 'Data-Cache-Time', $ts );
651 $out->addJsConfigVars( 'dataCacheTime', $ts );
652 $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
653 } else {
654 $out->addWikiMsg( 'perfcached', $maxResults );
655 }
656
657 # If updates on this page have been disabled, let the user know
658 # that the data set won't be refreshed for now
659 $disabledQueryPages = self::getDisabledQueryPages( $this->getConfig() );
660 if ( isset( $disabledQueryPages[$this->getName()] ) ) {
661 $runMode = $disabledQueryPages[$this->getName()];
662 if ( $runMode === 'disabled' ) {
663 $out->wrapWikiMsg(
664 "<div class=\"mw-querypage-no-updates\">\n$1\n</div>",
665 'querypage-no-updates'
666 );
667 } else {
668 // Messages used here: querypage-updates-periodical
669 $out->wrapWikiMsg(
670 "<div class=\"mw-querypage-updates-" . $runMode . "\">\n$1\n</div>",
671 'querypage-updates-' . $runMode
672 );
673 }
674 }
675 }
676 }
677
678 $this->numRows = $res->numRows();
679
680 $dbr = $this->getRecacheDB();
681 $this->preprocessResults( $dbr, $res );
682
683 $out->addHTML( Xml::openElement( 'div', [ 'class' => 'mw-spcontent' ] ) );
684
685 # Top header and navigation
686 if ( $this->shownavigation ) {
687 $out->addHTML( $this->getPageHeader() );
688 if ( $this->numRows > 0 ) {
689 $out->addHTML( $this->msg( 'showingresultsinrange' )->numParams(
690 min( $this->numRows, $this->limit ), # do not show the one extra row, if exist
691 $this->offset + 1, ( min( $this->numRows, $this->limit ) + $this->offset ) )->parseAsBlock() );
692 # Disable the "next" link when we reach the end
693 $miserMaxResults = $this->getConfig()->get( 'MiserMode' )
694 && ( $this->offset + $this->limit >= $this->getMaxResults() );
695 $atEnd = ( $this->numRows <= $this->limit ) || $miserMaxResults;
696 $paging = $this->buildPrevNextNavigation( $this->offset,
697 $this->limit, $this->linkParameters(), $atEnd, $par );
698 $out->addHTML( '<p>' . $paging . '</p>' );
699 } else {
700 # No results to show, so don't bother with "showing X of Y" etc.
701 # -- just let the user know and give up now
702 $this->showEmptyText();
703 $out->addHTML( Xml::closeElement( 'div' ) );
704 return;
705 }
706 }
707
708 # The actual results; specialist subclasses will want to handle this
709 # with more than a straight list, so we hand them the info, plus
710 # an OutputPage, and let them get on with it
711 $this->outputResults( $out,
712 $this->getSkin(),
713 $dbr, # Should use IResultWrapper for this
714 $res,
715 min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
716 $this->offset );
717
718 # Repeat the paging links at the bottom
719 if ( $this->shownavigation ) {
720 $out->addHTML( '<p>' . $paging . '</p>' );
721 }
722
723 $out->addHTML( Xml::closeElement( 'div' ) );
724 }
725
737 protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
738 if ( $num > 0 ) {
739 $html = [];
740 if ( !$this->listoutput ) {
741 $html[] = $this->openList( $offset );
742 }
743
744 # $res might contain the whole 1,000 rows, so we read up to
745 # $num [should update this to use a Pager]
746 for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
747 $line = $this->formatResult( $skin, $row );
748 if ( $line ) {
749 $html[] = $this->listoutput
750 ? $line
751 : "<li>{$line}</li>\n";
752 }
753 }
754
755 if ( !$this->listoutput ) {
756 $html[] = $this->closeList();
757 }
758
759 $html = $this->listoutput
760 ? MediaWikiServices::getInstance()->getContentLanguage()->listToText( $html )
761 : implode( '', $html );
762
763 $out->addHTML( $html );
764 }
765 }
766
771 function openList( $offset ) {
772 return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
773 }
774
778 function closeList() {
779 return "</ol>\n";
780 }
781
787 function preprocessResults( $db, $res ) {
788 }
789
800 protected function executeLBFromResultWrapper( IResultWrapper $res, $ns = null ) {
801 if ( !$res->numRows() ) {
802 return;
803 }
804
805 $batch = new LinkBatch;
806 foreach ( $res as $row ) {
807 $batch->add( $ns ?? $row->namespace, $row->title );
808 }
809 $batch->execute();
810
811 $res->seek( 0 );
812 }
813}
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
$line
Definition cdb.php:59
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition LinkBatch.php:34
add( $ns, $dbkey)
Definition LinkBatch.php:83
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:36
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.
int $offset
The offset and limit in use, as passed to the query() function.
Definition QueryPage.php:41
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:58
static getPages()
Get a list of query page classes and their associated special pages, for periodic updates.
Definition QueryPage.php:74
bool $shownavigation
Whether to show prev/next links.
Definition QueryPage.php:63
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.
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:53
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:38
execute( $par)
This is the actual workhorse.
getLimitOffset()
Returns limit and offset, as returned by $this->getRequest()->getLimitOffset().
getRecacheDB()
Get a DB connection to be used for slow recache queries.
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.
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.
displayRestrictionError()
Output an error message telling the user what access level they have to have.
getLanguage()
Shortcut to get user's language.
userCanExecute(User $user)
Checks if the given user (identified by an object) can execute this special page (as defined by $mRes...
Database error base class.
Definition DBError.php:30
Interface for configuration instances.
Definition Config.php:28
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 query wrapper.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
Result wrapper for grabbing data queried from an IDatabase object.
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26
if(!isset( $args[0])) $lang