66use Wikimedia\Timestamp\TimestampFormat as TS;
107 private $loadBalancer =
null;
110 private $databaseProvider =
null;
113 private $linkBatchFactory =
null;
116 private $httpRequestFactory =
null;
131 if ( $qp ===
null ) {
133 [ SpecialAncientPages::class,
'Ancientpages' ],
134 [ SpecialBrokenRedirects::class,
'BrokenRedirects' ],
135 [ SpecialDeadendPages::class,
'Deadendpages' ],
136 [ SpecialDoubleRedirects::class,
'DoubleRedirects' ],
137 [ SpecialListDuplicatedFiles::class,
'ListDuplicatedFiles' ],
138 [ SpecialLinkSearch::class,
'LinkSearch' ],
139 [ SpecialListRedirects::class,
'Listredirects' ],
140 [ SpecialLonelyPages::class,
'Lonelypages' ],
141 [ SpecialLongPages::class,
'Longpages' ],
142 [ SpecialMediaStatistics::class,
'MediaStatistics', SpecialMediaStatistics::MAX_LIMIT ],
143 [ SpecialMIMESearch::class,
'MIMEsearch' ],
144 [ SpecialMostCategories::class,
'Mostcategories' ],
145 [ SpecialMostImages::class,
'Mostimages' ],
146 [ SpecialMostInterwikis::class,
'Mostinterwikis' ],
147 [ SpecialMostLinkedCategories::class,
'Mostlinkedcategories' ],
148 [ SpecialMostLinkedTemplates::class,
'Mostlinkedtemplates' ],
149 [ SpecialMostLinked::class,
'Mostlinked' ],
150 [ SpecialMostRevisions::class,
'Mostrevisions' ],
151 [ SpecialFewestRevisions::class,
'Fewestrevisions' ],
152 [ SpecialShortPages::class,
'Shortpages' ],
153 [ SpecialUncategorizedCategories::class,
'Uncategorizedcategories' ],
154 [ SpecialUncategorizedPages::class,
'Uncategorizedpages' ],
155 [ SpecialUncategorizedImages::class,
'Uncategorizedimages' ],
156 [ SpecialUncategorizedTemplates::class,
'Uncategorizedtemplates' ],
157 [ SpecialUnusedCategories::class,
'Unusedcategories' ],
158 [ SpecialUnusedImages::class,
'Unusedimages' ],
159 [ SpecialWantedCategories::class,
'Wantedcategories' ],
160 [ SpecialWantedFiles::class,
'Wantedfiles' ],
161 [ SpecialWantedPages::class,
'Wantedpages' ],
162 [ SpecialWantedTemplates::class,
'Wantedtemplates' ],
163 [ SpecialUnwatchedPages::class,
'Unwatchedpages' ],
164 [ SpecialUnusedTemplates::class,
'Unusedtemplates' ],
165 [ SpecialWithoutInterwiki::class,
'Withoutinterwiki' ],
178 $this->linkBatchFactory = $linkBatchFactory;
186 if ( $this->linkBatchFactory === null ) {
191 return $this->linkBatchFactory;
198 if ( $this->httpRequestFactory === null ) {
203 return $this->httpRequestFactory;
212 $disableQueryPageUpdate = $config->
get( MainConfigNames::DisableQueryPageUpdate );
214 if ( !is_array( $disableQueryPageUpdate ) ) {
219 foreach ( $disableQueryPageUpdate as $name => $runMode ) {
220 if ( is_int( $name ) ) {
222 $pages[$runMode] =
'disabled';
224 $pages[$name] = $runMode;
236 $this->listoutput = $bool;
322 return $this->getConfig()->get( MainConfigNames::DisableQueryPages );
345 return $this->isExpensive() && $this->getConfig()->get( MainConfigNames::MiserMode );
366 $config = $this->getConfig();
367 $externalSources = $config->get( MainConfigNames::ExternalQuerySources );
368 $pageName = $this->getName();
370 return !empty( $externalSources[$pageName][
'enabled'] ) &&
371 !empty( $externalSources[$pageName][
'url'] );
403 $this->getOutput()->addWikiMsg(
'specialpage-empty' );
428 public function recache( $limit, $unused =
true ) {
429 if ( !$this->isCacheable() ) {
433 $fname = static::class .
'::recache';
434 $dbw = $this->getDatabaseProvider()->getPrimaryDatabase();
437 $res = $this->reallyDoQuery( $limit,
false );
440 $num = $res->numRows();
443 foreach ( $res as $i => $row ) {
444 if ( isset( $row->value ) ) {
445 if ( $this->usesTimestamps() ) {
446 $value = (int)
wfTimestamp( TS::UNIX, $row->value );
448 $value = intval( $row->value );
455 'qc_type' => $this->getName(),
456 'qc_namespace' => $row->namespace,
457 'qc_title' => $row->title,
462 $dbw->doAtomicSection(
467 ->deleteFrom(
'querycache' )
468 ->where( [
'qc_type' => $this->getName() ] )
469 ->caller( $fname )->execute();
472 ->insertInto(
'querycache_info' )
473 ->row( [
'qci_type' => $this->getName(),
'qci_timestamp' => $dbw->
timestamp() ] )
474 ->onDuplicateKeyUpdate()
475 ->uniqueIndexFields( [
'qci_type' ] )
476 ->set( [
'qci_timestamp' => $dbw->
timestamp() ] )
477 ->caller( $fname )->execute();
481 if ( count( $vals ) ) {
482 foreach ( array_chunk( $vals, 500 ) as $chunk ) {
484 ->insertInto(
'querycache' )
486 ->caller( $fname )->execute();
500 return $this->getDatabaseProvider()->getReplicaDatabase(
false,
'vslow' );
510 if ( $this->isCached() ) {
511 $dbw = $this->getDatabaseProvider()->getPrimaryDatabase();
512 $dbw->newDeleteQueryBuilder()
513 ->deleteFrom(
'querycache' )
515 'qc_type' => $this->getName(),
516 'qc_namespace' => $title->getNamespace(),
517 'qc_title' => $title->getDBkey(),
519 ->caller( __METHOD__ )->execute();
529 $fname = static::class .
'::' . __FUNCTION__;
530 $dbw = $this->getDatabaseProvider()->getPrimaryDatabase();
531 $dbw->newDeleteQueryBuilder()
532 ->deleteFrom(
'querycache' )
533 ->where( [
'qc_type' => $this->getName() ] )
534 ->caller( $fname )->execute();
535 $dbw->newDeleteQueryBuilder()
536 ->deleteFrom(
'querycachetwo' )
537 ->where( [
'qcc_type' => $this->getName() ] )
538 ->caller( $fname )->execute();
539 $dbw->newDeleteQueryBuilder()
540 ->deleteFrom(
'querycache_info' )
541 ->where( [
'qci_type' => $this->getName() ] )
542 ->caller( $fname )->execute();
554 if ( $this->usesExternalSource() ) {
555 return $this->reallyDoQueryExternal( $limit, $offset );
558 return $this->reallyDoQueryInternal( $limit, $offset );
568 private function reallyDoQueryInternal( $limit, $offset =
false ) {
569 $fname = static::class .
'::reallyDoQueryInternal';
570 $dbr = $this->getRecacheDB();
571 $query = $this->getQueryInfo();
572 $order = $this->getOrderFields();
574 if ( $this->sortDescending() ) {
575 foreach ( $order as &$field ) {
580 $tables = isset( $query[
'tables'] ) ? (array)$query[
'tables'] : [];
581 $fields = isset( $query[
'fields'] ) ? (array)$query[
'fields'] : [];
582 $conds = isset( $query[
'conds'] ) ? (array)$query[
'conds'] : [];
583 $options = isset( $query[
'options'] ) ? (array)$query[
'options'] : [];
584 $join_conds = isset( $query[
'join_conds'] ) ? (array)$query[
'join_conds'] : [];
586 $queryBuilder = $dbr->newSelectQueryBuilder()
591 ->options( $options )
592 ->joinConds( $join_conds );
594 $queryBuilder->orderBy( $order );
597 if ( $limit !==
false ) {
598 $queryBuilder->limit( intval( $limit ) );
601 if ( $offset !==
false ) {
602 $queryBuilder->offset( intval( $offset ) );
605 return $queryBuilder->fetchResultSet();
615 private function reallyDoQueryExternal( $limit, $offset =
false ) {
616 $fname = static::class .
'::reallyDoQueryExternal';
617 $httpRequestFactory = $this->getHttpRequestFactory();
618 $externalSources = $this->getConfig()->get( MainConfigNames::ExternalQuerySources );
620 $config = $externalSources[$pageName];
623 if ( isset( $config[
'timeout'] ) ) {
624 $options[
'timeout'] = (int)$config[
'timeout'];
627 $request = $httpRequestFactory->create( $config[
'url'], $options, $fname );
629 $status = $request->execute();
630 if ( !$status->isOK() ) {
631 throw new RuntimeException(
"Failed to fetch data from external source '{$pageName}': " .
632 $status->getMessage()->text() );
635 $content = $request->getContent();
636 if ( $content ===
null || $content ===
'' ) {
637 throw new RuntimeException(
"Empty response received from external source '{$pageName}'" );
640 $decoded = json_decode( $content,
true );
641 if ( $decoded ===
null ) {
642 throw new RuntimeException(
"Invalid JSON response from external source '{$pageName}': " .
643 json_last_error_msg() );
646 if ( !is_array( $decoded ) ) {
647 throw new RuntimeException(
"Expected array data from external source '{$pageName}', got " .
648 gettype( $decoded ) );
652 foreach ( $decoded as $i => $row ) {
653 if ( !is_array( $row ) ) {
654 throw new RuntimeException(
"Invalid row data at index {$i} from external source '{$pageName}': " .
655 'expected array, got ' . gettype( $row ) );
658 $requiredFields = [
'qc_namespace',
'qc_title',
'qc_value' ];
659 foreach ( $requiredFields as $field ) {
660 if ( !array_key_exists( $field, $row ) ) {
661 throw new RuntimeException(
662 "Missing required field '{$field}' in row {$i} from external source '{$pageName}'" );
666 $result[] = (object)[
667 'namespace' => $row[
'qc_namespace'],
668 'title' => $row[
'qc_title'],
669 'value' => $row[
'qc_value']
673 return new FakeResultWrapper( $result );
682 public function doQuery( $offset =
false, $limit =
false ) {
683 if ( $this->isCached() && $this->isCacheable() ) {
684 return $this->fetchFromCache( $limit, $offset );
686 return $this->reallyDoQuery( $limit, $offset );
700 $dbr = $this->getDatabaseProvider()->getReplicaDatabase();
701 $queryBuilder = $dbr->newSelectQueryBuilder()
702 ->select( [
'qc_type',
'namespace' =>
'qc_namespace',
'title' =>
'qc_title',
'value' =>
'qc_value' ] )
703 ->from(
'querycache' )
704 ->where( [
'qc_type' => $this->getName() ] );
706 if ( $limit !==
false ) {
707 $queryBuilder->limit( intval( $limit ) );
710 if ( $offset !==
false ) {
711 $queryBuilder->offset( intval( $offset ) );
714 $order = $this->getCacheOrderFields();
715 if ( $this->sortDescending() ) {
716 $queryBuilder->orderBy( $order, SelectQueryBuilder::SORT_DESC );
718 $queryBuilder->orderBy( $order );
721 return $queryBuilder->caller( __METHOD__ )->fetchResultSet();
739 if ( $this->cachedTimestamp ===
null ) {
740 $dbr = $this->getDatabaseProvider()->getReplicaDatabase();
741 $fname = static::class .
'::getCachedTimestamp';
742 $this->cachedTimestamp = $dbr->newSelectQueryBuilder()
743 ->select(
'qci_timestamp' )
744 ->from(
'querycache_info' )
745 ->where( [
'qci_type' => $this->getName() ] )
746 ->caller( $fname )->fetchField();
748 return $this->cachedTimestamp;
764 [ $limit, $offset ] = $this->getRequest()
765 ->getLimitOffsetForUser( $this->getUser() );
766 if ( $this->getConfig()->
get( MainConfigNames::MiserMode ) ) {
767 $maxResults = $this->getMaxResults();
769 $limit = min( $limit, $maxResults );
771 $offset = min( $offset, $maxResults + 1 );
773 return [ $limit, $offset ];
785 $maxResults = $this->getMaxResults();
786 if ( $this->getConfig()->
get( MainConfigNames::MiserMode ) ) {
787 $limit = min( $uiLimit + 1, $maxResults - $uiOffset );
788 return max( $limit, 0 );
806 return max( $this->getConfig()->
get( MainConfigNames::QueryCacheLimit ), 10000 );
816 $this->checkPermissions();
819 $this->outputHeader();
821 $out = $this->getOutput();
823 if ( $this->isCached() && !$this->isCacheable() ) {
824 $out->addWikiMsg(
'querypage-disabled' );
828 $out->setSyndicated( $this->isSyndicated() );
830 if ( $this->limit == 0 && $this->offset == 0 ) {
831 [ $this->limit, $this->offset ] = $this->getLimitOffset();
833 $dbLimit = $this->getDBLimit( $this->limit, $this->offset );
835 if ( !$this->isCached() ) {
837 $res = $this->reallyDoQuery( $dbLimit, $this->offset );
840 $res = $this->fetchFromCache( $dbLimit, $this->offset );
841 if ( !$this->listoutput ) {
843 $ts = $this->getCachedTimestamp();
844 $lang = $this->getLanguage();
845 $maxResults = $lang->formatNum( $this->getConfig()->
get(
846 MainConfigNames::QueryCacheLimit ) );
849 $user = $this->getUser();
850 $updated = $lang->userTimeAndDate( $ts, $user );
851 $updateddate = $lang->userDate( $ts, $user );
852 $updatedtime = $lang->userTime( $ts, $user );
853 $out->addMeta(
'Data-Cache-Time', $ts );
854 $out->addJsConfigVars(
'dataCacheTime', $ts );
855 $out->addWikiMsg(
'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
857 $out->addWikiMsg(
'perfcached', $maxResults );
862 $disabledQueryPages = self::getDisabledQueryPages( $this->getConfig() );
863 if ( isset( $disabledQueryPages[$this->getName()] ) ) {
864 $runMode = $disabledQueryPages[$this->getName()];
865 if ( $runMode ===
'disabled' ) {
867 "<div class=\"mw-querypage-no-updates\">\n$1\n</div>",
868 'querypage-no-updates'
873 "<div class=\"mw-querypage-updates-" . $runMode .
"\">\n$1\n</div>",
874 'querypage-updates-' . $runMode
881 $this->numRows = $res->numRows();
883 $dbr = $this->getRecacheDB();
884 $this->preprocessResults( $dbr, $res );
886 $out->addHTML( Html::openElement(
'div', [
'class' =>
'mw-spcontent' ] ) );
889 if ( $this->shownavigation ) {
890 $out->addHTML( $this->getPageHeader() );
891 if ( $this->numRows > 0 ) {
892 $out->addHTML( $this->msg(
'showingresultsinrange' )->numParams(
893 min( $this->numRows, $this->limit ),
894 $this->offset + 1, ( min( $this->numRows, $this->limit ) + $this->offset ) )->parseAsBlock() );
896 $miserMaxResults = $this->getConfig()->get( MainConfigNames::MiserMode )
897 && ( $this->offset + $this->limit >= $this->getMaxResults() );
898 $atEnd = ( $this->numRows <= $this->limit ) || $miserMaxResults;
899 $paging = $this->buildPrevNextNavigation( $this->offset,
900 $this->limit, $this->linkParameters(), $atEnd, $par );
901 $out->addHTML(
'<p>' . $paging .
'</p>' );
905 $this->showEmptyText();
906 $out->addHTML( Html::closeElement(
'div' ) );
914 $this->outputResults( $out,
918 min( $this->numRows, $this->limit ),
922 if ( $this->shownavigation ) {
924 $out->addHTML(
'<p>' . $paging .
'</p>' );
927 $out->addHTML( Html::closeElement(
'div' ) );
943 protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
946 if ( !$this->listoutput ) {
947 $html[] = $this->openList( $offset );
953 for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
954 $line = $this->formatResult( $skin, $row );
956 $html[] = $this->listoutput
958 :
"<li>{$line}</li>\n";
962 if ( !$this->listoutput ) {
963 $html[] = $this->closeList();
966 $html = $this->listoutput
967 ? $this->getContentLanguage()->listToText( $html )
968 : implode(
'', $html );
970 $out->addHTML( $html );
979 return "\n<ol start='" . ( $offset + 1 ) .
"' class='special'>\n";
1015 $batch = $this->getLinkBatchFactory()->newLinkBatch()->setCaller( __METHOD__ );
1016 foreach ( $res as $row ) {
1017 $batch->add( $ns ?? (
int)$row->namespace, $row->title );
1030 $this->loadBalancer = $loadBalancer;
1039 if ( $this->loadBalancer === null ) {
1042 $this->loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
1044 return $this->loadBalancer;
1052 $this->databaseProvider = $databaseProvider;
1060 if ( $this->databaseProvider === null ) {
1061 $this->databaseProvider = MediaWikiServices::getInstance()->getConnectionProvider();
1063 return $this->databaseProvider;
1068class_alias( QueryPage::class,
'QueryPage' );
wfTimestamp( $outputtype=TS::UNIX, $ts=0)
Get a timestamp string in one of various formats.
if(!defined('MW_SETUP_CALLBACK'))
A class containing constants representing the names of configuration variables.
This is one of the Core classes and should be read at least once by any new developers.
Factory for LinkBatch objects to batch query page metadata.
This is a class for doing query pages; since they're almost all the same, we factor out some of the f...
getPageHeader()
The content returned by this function will be output before any result.
linkParameters()
If using extra form wheely-dealies, return a set of parameters here as an associative array.
usesExternalSource()
Check if this query page is configured to fetch data from an external source via HTTP.
bool $shownavigation
Whether to show prev/next links.
bool $listoutput
Whether or not we want plain listoutput rather than an ordered list.
getOrderFields()
Subclasses return an array of fields to order by here.
preprocessResults( $db, $res)
Do any necessary preprocessing of the result object.
setDatabaseProvider(IConnectionProvider $databaseProvider)
getLimitOffset()
Returns limit and offset, as returned by $this->getRequest()->getLimitOffsetForUser().
int $offset
The offset and limit in use, as passed to the query() function.
getDBLimit( $uiLimit, $uiOffset)
What is limit to fetch from DB.
isCached()
Whether or not the output of the page in question is retrieved from the database cache.
execute( $par)
This is the actual workhorse.
setListoutput( $bool)
A mutator for $this->listoutput;.
executeLBFromResultWrapper(IResultWrapper $res, $ns=null)
Creates a new LinkBatch object, adds all pages from the passed result wrapper (MUST include title and...
getMaxResults()
Get max number of results we can return in miser mode.
isExpensive()
Should this query page only be updated offline on large wikis?
static getPages()
Get a list of query page classes and their associated special pages, for periodic updates.
fetchFromCache( $limit, $offset=false)
Fetch the query results from the query cache.
setDBLoadBalancer(ILoadBalancer $loadBalancer)
string null false $cachedTimestamp
deleteAllCachedData()
Remove all cached value This is needed when the page is no longer using the cache.
showEmptyText()
Outputs some kind of an informative message (via OutputPage) to let the user know that the query retu...
doQuery( $offset=false, $limit=false)
Somewhat deprecated, you probably want to be using execute()
outputResults( $out, $skin, $dbr, $res, $num, $offset)
Format and output report results using the given information plus OutputPage.
getRecacheDB()
Get a DB connection to be used for slow recache queries.
static getDisabledQueryPages(Config $config)
Get a list of disabled query pages and their run mode.
sortDescending()
Override to sort by increasing values.
int $numRows
The number of rows returned by the query.
recache( $limit, $unused=true)
Clear the cache and save new results.
usesTimestamps()
Does this query return timestamps rather than integers in its 'value' field? If true,...
isSyndicated()
Sometimes we don't want to build rss / atom feeds.
getQueryInfo()
Subclasses return an SQL query here, formatted as an array with the following keys: tables => Table(s...
setLinkBatchFactory(LinkBatchFactory $linkBatchFactory)
formatResult( $skin, $result)
Formats the results of the query for display.
getCacheOrderFields()
Return the order fields for fetchFromCache.
isCacheable()
Is the output of this query cacheable? Non-cacheable expensive pages will be disabled in miser mode a...
reallyDoQuery( $limit, $offset=false)
Run the query and return the result.
Parent class for all special pages.
Implements Special:Ancientpages.
List of pages that contain no links to other pages.
List of articles with no article linking to them, thus being lonely.
Implements Special:Longpages.
List of the shortest pages in the database.
List of pages without any category.
List of pages that are not on anyone's watchlist.
List of the most-linked pages that do not exist.