MediaWiki  REL1_19
QueryPage.php
Go to the documentation of this file.
00001 <?php
00015 global $wgQueryPages; // not redundant
00016 $wgQueryPages = array(
00017 //         QueryPage subclass           Special page name         Limit (false for none, none for the default)
00018 // ----------------------------------------------------------------------------
00019         array( 'AncientPagesPage',              'Ancientpages'                  ),
00020         array( 'BrokenRedirectsPage',           'BrokenRedirects'               ),
00021         array( 'DeadendPagesPage',              'Deadendpages'                  ),
00022         array( 'DisambiguationsPage',           'Disambiguations'               ),
00023         array( 'DoubleRedirectsPage',           'DoubleRedirects'               ),
00024         array( 'FileDuplicateSearchPage',       'FileDuplicateSearch'           ),
00025         array( 'LinkSearchPage',                'LinkSearch'                    ),
00026         array( 'ListredirectsPage',             'Listredirects'                 ),
00027         array( 'LonelyPagesPage',               'Lonelypages'                   ),
00028         array( 'LongPagesPage',                 'Longpages'                     ),
00029         array( 'MIMEsearchPage',                'MIMEsearch'                    ),
00030         array( 'MostcategoriesPage',            'Mostcategories'                ),
00031         array( 'MostimagesPage',                'Mostimages'                    ),
00032         array( 'MostlinkedCategoriesPage',      'Mostlinkedcategories'          ),
00033         array( 'MostlinkedtemplatesPage',       'Mostlinkedtemplates'           ),
00034         array( 'MostlinkedPage',                'Mostlinked'                    ),
00035         array( 'MostrevisionsPage',             'Mostrevisions'                 ),
00036         array( 'FewestrevisionsPage',           'Fewestrevisions'               ),
00037         array( 'ShortPagesPage',                'Shortpages'                    ),
00038         array( 'UncategorizedCategoriesPage',   'Uncategorizedcategories'       ),
00039         array( 'UncategorizedPagesPage',        'Uncategorizedpages'            ),
00040         array( 'UncategorizedImagesPage',       'Uncategorizedimages'           ),
00041         array( 'UncategorizedTemplatesPage',    'Uncategorizedtemplates'        ),
00042         array( 'UnusedCategoriesPage',          'Unusedcategories'              ),
00043         array( 'UnusedimagesPage',              'Unusedimages'                  ),
00044         array( 'WantedCategoriesPage',          'Wantedcategories'              ),
00045         array( 'WantedFilesPage',               'Wantedfiles'                   ),
00046         array( 'WantedPagesPage',               'Wantedpages'                   ),
00047         array( 'WantedTemplatesPage',           'Wantedtemplates'               ),
00048         array( 'UnwatchedPagesPage',            'Unwatchedpages'                ),
00049         array( 'UnusedtemplatesPage',           'Unusedtemplates'               ),
00050         array( 'WithoutInterwikiPage',          'Withoutinterwiki'              ),
00051 );
00052 wfRunHooks( 'wgQueryPages', array( &$wgQueryPages ) );
00053 
00054 global $wgDisableCounters;
00055 if ( !$wgDisableCounters )
00056         $wgQueryPages[] = array( 'PopularPagesPage', 'Popularpages' );
00057 
00058 
00065 abstract class QueryPage extends SpecialPage {
00071         var $listoutput = false;
00072 
00078         var $offset = 0;
00079         var $limit = 0;
00080 
00086         protected $numRows;
00087 
00088         protected $cachedTimestamp = null;
00089 
00093         protected $shownavigation = true;
00094 
00100         function setListoutput( $bool ) {
00101                 $this->listoutput = $bool;
00102         }
00103 
00130         function getQueryInfo() {
00131                 return null;
00132         }
00133 
00139         function getSQL() {
00140                 /* Implement getQueryInfo() instead */
00141                 throw new MWException( "Bug in a QueryPage: doesn't implement getQueryInfo() nor getQuery() properly" );
00142         }
00143 
00151         function getOrderFields() {
00152                 return array( 'value' );
00153         }
00154 
00165         function usesTimestamps() {
00166                 return false;
00167         }
00168 
00174         function sortDescending() {
00175                 return true;
00176         }
00177 
00185         function isExpensive() {
00186                 global $wgDisableQueryPages;
00187                 return $wgDisableQueryPages;
00188         }
00189 
00197         public function isCacheable() {
00198                 return true;
00199         }
00200 
00207         function isCached() {
00208                 global $wgMiserMode;
00209 
00210                 return $this->isExpensive() && $wgMiserMode;
00211         }
00212 
00218         function isSyndicated() {
00219                 return true;
00220         }
00221 
00234         abstract function formatResult( $skin, $result );
00235 
00241         function getPageHeader() {
00242                 return '';
00243         }
00244 
00252         function linkParameters() {
00253                 return array();
00254         }
00255 
00263         function tryLastResult() {
00264                 return false;
00265         }
00266 
00273         function recache( $limit, $ignoreErrors = true ) {
00274                 if ( !$this->isCacheable() ) {
00275                         return 0;
00276                 }
00277 
00278                 $fname = get_class( $this ) . '::recache';
00279                 $dbw = wfGetDB( DB_MASTER );
00280                 $dbr = wfGetDB( DB_SLAVE, array( $this->getName(), __METHOD__, 'vslow' ) );
00281                 if ( !$dbw || !$dbr ) {
00282                         return false;
00283                 }
00284 
00285                 if ( $ignoreErrors ) {
00286                         $ignoreW = $dbw->ignoreErrors( true );
00287                         $ignoreR = $dbr->ignoreErrors( true );
00288                 }
00289 
00290                 # Clear out any old cached data
00291                 $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname );
00292                 # Do query
00293                 $res = $this->reallyDoQuery( $limit, false );
00294                 $num = false;
00295                 if ( $res ) {
00296                         $num = $dbr->numRows( $res );
00297                         # Fetch results
00298                         $vals = array();
00299                         while ( $res && $row = $dbr->fetchObject( $res ) ) {
00300                                 if ( isset( $row->value ) ) {
00301                                         if ( $this->usesTimestamps() ) {
00302                                                 $value = wfTimestamp( TS_UNIX,
00303                                                         $row->value );
00304                                         } else {
00305                                                 $value = intval( $row->value ); // @bug 14414
00306                                         }
00307                                 } else {
00308                                         $value = 0;
00309                                 }
00310 
00311                                 $vals[] = array( 'qc_type' => $this->getName(),
00312                                                 'qc_namespace' => $row->namespace,
00313                                                 'qc_title' => $row->title,
00314                                                 'qc_value' => $value );
00315                         }
00316 
00317                         # Save results into the querycache table on the master
00318                         if ( count( $vals ) ) {
00319                                 if ( !$dbw->insert( 'querycache', $vals, __METHOD__ ) ) {
00320                                         // Set result to false to indicate error
00321                                         $num = false;
00322                                 }
00323                         }
00324                         if ( $ignoreErrors ) {
00325                                 $dbw->ignoreErrors( $ignoreW );
00326                                 $dbr->ignoreErrors( $ignoreR );
00327                         }
00328 
00329                         # Update the querycache_info record for the page
00330                         $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname );
00331                         $dbw->insert( 'querycache_info', array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ), $fname );
00332 
00333                 }
00334                 return $num;
00335         }
00336 
00344         function reallyDoQuery( $limit, $offset = false ) {
00345                 $fname = get_class( $this ) . "::reallyDoQuery";
00346                 $dbr = wfGetDB( DB_SLAVE );
00347                 $query = $this->getQueryInfo();
00348                 $order = $this->getOrderFields();
00349                 if ( $this->sortDescending() ) {
00350                         foreach ( $order as &$field ) {
00351                                 $field .= ' DESC';
00352                         }
00353                 }
00354                 if ( is_array( $query ) ) {
00355                         $tables = isset( $query['tables'] ) ? (array)$query['tables'] : array();
00356                         $fields = isset( $query['fields'] ) ? (array)$query['fields'] : array();
00357                         $conds = isset( $query['conds'] ) ? (array)$query['conds'] : array();
00358                         $options = isset( $query['options'] ) ? (array)$query['options'] : array();
00359                         $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : array();
00360                         if ( count( $order ) ) {
00361                                 $options['ORDER BY'] = implode( ', ', $order );
00362                         }
00363                         if ( $limit !== false ) {
00364                                 $options['LIMIT'] = intval( $limit );
00365                         }
00366                         if ( $offset !== false ) {
00367                                 $options['OFFSET'] = intval( $offset );
00368                         }
00369 
00370                         $res = $dbr->select( $tables, $fields, $conds, $fname,
00371                                         $options, $join_conds
00372                         );
00373                 } else {
00374                         // Old-fashioned raw SQL style, deprecated
00375                         $sql = $this->getSQL();
00376                         $sql .= ' ORDER BY ' . implode( ', ', $order );
00377                         $sql = $dbr->limitResult( $sql, $limit, $offset );
00378                         $res = $dbr->query( $sql, $fname );
00379                 }
00380                 return $dbr->resultObject( $res );
00381         }
00382 
00386         function doQuery( $offset = false, $limit = false ) {
00387                 if ( $this->isCached() && $this->isCacheable() ) {
00388                         return $this->fetchFromCache( $limit, $offset );
00389                 } else {
00390                         return $this->reallyDoQuery( $limit, $offset );
00391                 }
00392         }
00393 
00401         function fetchFromCache( $limit, $offset = false ) {
00402                 $dbr = wfGetDB( DB_SLAVE );
00403                 $options = array ();
00404                 if ( $limit !== false ) {
00405                         $options['LIMIT'] = intval( $limit );
00406                 }
00407                 if ( $offset !== false ) {
00408                         $options['OFFSET'] = intval( $offset );
00409                 }
00410                 if ( $this->sortDescending() ) {
00411                         $options['ORDER BY'] = 'qc_value DESC';
00412                 } else {
00413                         $options['ORDER BY'] = 'qc_value ASC';
00414                 }
00415                 $res = $dbr->select( 'querycache', array( 'qc_type',
00416                                 'qc_namespace AS namespace',
00417                                 'qc_title AS title',
00418                                 'qc_value AS value' ),
00419                                 array( 'qc_type' => $this->getName() ),
00420                                 __METHOD__, $options
00421                 );
00422                 return $dbr->resultObject( $res );
00423         }
00424 
00425         public function getCachedTimestamp() {
00426                 if ( is_null( $this->cachedTimestamp ) ) {
00427                         $dbr = wfGetDB( DB_SLAVE );
00428                         $fname = get_class( $this ) . '::getCachedTimestamp';
00429                         $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
00430                                 array( 'qci_type' => $this->getName() ), $fname );
00431                 }
00432                 return $this->cachedTimestamp;
00433         }
00434 
00439         function execute( $par ) {
00440                 global $wgQueryCacheLimit, $wgDisableQueryPageUpdate;
00441 
00442                 $user = $this->getUser();
00443                 if ( !$this->userCanExecute( $user ) ) {
00444                         $this->displayRestrictionError();
00445                         return;
00446                 }
00447 
00448                 $this->setHeaders();
00449                 $this->outputHeader();
00450 
00451                 $out = $this->getOutput();
00452 
00453                 if ( $this->isCached() && !$this->isCacheable() ) {
00454                         $out->addWikiMsg( 'querypage-disabled' );
00455                         return 0;
00456                 }
00457 
00458                 $out->setSyndicated( $this->isSyndicated() );
00459 
00460                 if ( $this->limit == 0 && $this->offset == 0 ) {
00461                         list( $this->limit, $this->offset ) = $this->getRequest()->getLimitOffset();
00462                 }
00463 
00464                 // TODO: Use doQuery()
00465                 if ( !$this->isCached() ) {
00466                         $res = $this->reallyDoQuery( $this->limit, $this->offset );
00467                 } else {
00468                         # Get the cached result
00469                         $res = $this->fetchFromCache( $this->limit, $this->offset );
00470                         if ( !$this->listoutput ) {
00471 
00472                                 # Fetch the timestamp of this update
00473                                 $ts = $this->getCachedTimestamp();
00474                                 $lang = $this->getLanguage();
00475                                 $maxResults = $lang->formatNum( $wgQueryCacheLimit );
00476 
00477                                 if ( $ts ) {
00478                                         $updated = $lang->userTimeAndDate( $ts, $user );
00479                                         $updateddate = $lang->userDate( $ts, $user );
00480                                         $updatedtime = $lang->userTime( $ts, $user );
00481                                         $out->addMeta( 'Data-Cache-Time', $ts );
00482                                         $out->addInlineScript( "var dataCacheTime = '$ts';" );
00483                                         $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
00484                                 } else {
00485                                         $out->addWikiMsg( 'perfcached', $maxResults );
00486                                 }
00487 
00488                                 # If updates on this page have been disabled, let the user know
00489                                 # that the data set won't be refreshed for now
00490                                 if ( is_array( $wgDisableQueryPageUpdate ) && in_array( $this->getName(), $wgDisableQueryPageUpdate ) ) {
00491                                         $out->addWikiMsg( 'querypage-no-updates' );
00492                                 }
00493                         }
00494                 }
00495 
00496                 $this->numRows = $res->numRows();
00497 
00498                 $dbr = wfGetDB( DB_SLAVE );
00499                 $this->preprocessResults( $dbr, $res );
00500 
00501                 $out->addHTML( Xml::openElement( 'div', array( 'class' => 'mw-spcontent' ) ) );
00502 
00503                 # Top header and navigation
00504                 if ( $this->shownavigation ) {
00505                         $out->addHTML( $this->getPageHeader() );
00506                         if ( $this->numRows > 0 ) {
00507                                 $out->addHTML( $this->msg( 'showingresults' )->numParams(
00508                                         $this->numRows, $this->offset + 1 )->parseAsBlock() );
00509                                 # Disable the "next" link when we reach the end
00510                                 $paging = $this->getLanguage()->viewPrevNext( $this->getTitle( $par ), $this->offset,
00511                                         $this->limit, $this->linkParameters(), ( $this->numRows < $this->limit ) );
00512                                 $out->addHTML( '<p>' . $paging . '</p>' );
00513                         } else {
00514                                 # No results to show, so don't bother with "showing X of Y" etc.
00515                                 # -- just let the user know and give up now
00516                                 $out->addWikiMsg( 'specialpage-empty' );
00517                                 $out->addHTML( Xml::closeElement( 'div' ) );
00518                                 return;
00519                         }
00520                 }
00521 
00522                 # The actual results; specialist subclasses will want to handle this
00523                 # with more than a straight list, so we hand them the info, plus
00524                 # an OutputPage, and let them get on with it
00525                 $this->outputResults( $out,
00526                         $this->getSkin(),
00527                         $dbr, # Should use a ResultWrapper for this
00528                         $res,
00529                         $this->numRows,
00530                         $this->offset );
00531 
00532                 # Repeat the paging links at the bottom
00533                 if ( $this->shownavigation ) {
00534                         $out->addHTML( '<p>' . $paging . '</p>' );
00535                 }
00536 
00537                 $out->addHTML( Xml::closeElement( 'div' ) );
00538 
00539                 return $this->numRows;
00540         }
00541 
00553         protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
00554                 global $wgContLang;
00555 
00556                 if ( $num > 0 ) {
00557                         $html = array();
00558                         if ( !$this->listoutput ) {
00559                                 $html[] = $this->openList( $offset );
00560                         }
00561 
00562                         # $res might contain the whole 1,000 rows, so we read up to
00563                         # $num [should update this to use a Pager]
00564                         for ( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) {
00565                                 $line = $this->formatResult( $skin, $row );
00566                                 if ( $line ) {
00567                                         $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
00568                                                 ? ' class="not-patrolled"'
00569                                                 : '';
00570                                         $html[] = $this->listoutput
00571                                                 ? $line
00572                                                 : "<li{$attr}>{$line}</li>\n";
00573                                 }
00574                         }
00575 
00576                         # Flush the final result
00577                         if ( $this->tryLastResult() ) {
00578                                 $row = null;
00579                                 $line = $this->formatResult( $skin, $row );
00580                                 if ( $line ) {
00581                                         $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
00582                                                 ? ' class="not-patrolled"'
00583                                                 : '';
00584                                         $html[] = $this->listoutput
00585                                                 ? $line
00586                                                 : "<li{$attr}>{$line}</li>\n";
00587                                 }
00588                         }
00589 
00590                         if ( !$this->listoutput ) {
00591                                 $html[] = $this->closeList();
00592                         }
00593 
00594                         $html = $this->listoutput
00595                                 ? $wgContLang->listToText( $html )
00596                                 : implode( '', $html );
00597 
00598                         $out->addHTML( $html );
00599                 }
00600         }
00601 
00606         function openList( $offset ) {
00607                 return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
00608         }
00609 
00613         function closeList() {
00614                 return "</ol>\n";
00615         }
00616 
00620         function preprocessResults( $db, $res ) {}
00621 
00625         function doFeed( $class = '', $limit = 50 ) {
00626                 global $wgFeed, $wgFeedClasses;
00627 
00628                 if ( !$wgFeed ) {
00629                         $this->getOutput()->addWikiMsg( 'feed-unavailable' );
00630                         return;
00631                 }
00632 
00633                 global $wgFeedLimit;
00634                 if ( $limit > $wgFeedLimit ) {
00635                         $limit = $wgFeedLimit;
00636                 }
00637 
00638                 if ( isset( $wgFeedClasses[$class] ) ) {
00639                         $feed = new $wgFeedClasses[$class](
00640                                 $this->feedTitle(),
00641                                 $this->feedDesc(),
00642                                 $this->feedUrl() );
00643                         $feed->outHeader();
00644 
00645                         $res = $this->reallyDoQuery( $limit, 0 );
00646                         foreach ( $res as $obj ) {
00647                                 $item = $this->feedResult( $obj );
00648                                 if ( $item ) {
00649                                         $feed->outItem( $item );
00650                                 }
00651                         }
00652 
00653                         $feed->outFooter();
00654                         return true;
00655                 } else {
00656                         return false;
00657                 }
00658         }
00659 
00664         function feedResult( $row ) {
00665                 if ( !isset( $row->title ) ) {
00666                         return null;
00667                 }
00668                 $title = Title::MakeTitle( intval( $row->namespace ), $row->title );
00669                 if ( $title ) {
00670                         $date = isset( $row->timestamp ) ? $row->timestamp : '';
00671                         $comments = '';
00672                         if ( $title ) {
00673                                 $talkpage = $title->getTalkPage();
00674                                 $comments = $talkpage->getFullURL();
00675                         }
00676 
00677                         return new FeedItem(
00678                                 $title->getPrefixedText(),
00679                                 $this->feedItemDesc( $row ),
00680                                 $title->getFullURL(),
00681                                 $date,
00682                                 $this->feedItemAuthor( $row ),
00683                                 $comments );
00684                 } else {
00685                         return null;
00686                 }
00687         }
00688 
00689         function feedItemDesc( $row ) {
00690                 return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : '';
00691         }
00692 
00693         function feedItemAuthor( $row ) {
00694                 return isset( $row->user_text ) ? $row->user_text : '';
00695         }
00696 
00697         function feedTitle() {
00698                 global $wgLanguageCode, $wgSitename;
00699                 $desc = $this->getDescription();
00700                 return "$wgSitename - $desc [$wgLanguageCode]";
00701         }
00702 
00703         function feedDesc() {
00704                 return $this->msg( 'tagline' )->text();
00705         }
00706 
00707         function feedUrl() {
00708                 return $this->getTitle()->getFullURL();
00709         }
00710 }
00711 
00716 abstract class WantedQueryPage extends QueryPage {
00717 
00718         function isExpensive() {
00719                 return true;
00720         }
00721 
00722         function isSyndicated() {
00723                 return false;
00724         }
00725 
00729         function preprocessResults( $db, $res ) {
00730                 $batch = new LinkBatch;
00731                 foreach ( $res as $row ) {
00732                         $batch->add( $row->namespace, $row->title );
00733                 }
00734                 $batch->execute();
00735 
00736                 // Back to start for display
00737                 if ( $db->numRows( $res ) > 0 )
00738                         // If there are no rows we get an error seeking.
00739                         $db->dataSeek( $res, 0 );
00740         }
00741 
00749         function forceExistenceCheck() {
00750                 return false;
00751         }
00752 
00760         public function formatResult( $skin, $result ) {
00761                 $title = Title::makeTitleSafe( $result->namespace, $result->title );
00762                 if ( $title instanceof Title ) {
00763                         if ( $this->isCached() || $this->forceExistenceCheck() ) {
00764                                 $pageLink = $title->isKnown()
00765                                         ? '<del>' . Linker::link( $title ) . '</del>'
00766                                         : Linker::link(
00767                                                 $title,
00768                                                 null,
00769                                                 array(),
00770                                                 array(),
00771                                                 array( 'broken' )
00772                                         );
00773                         } else {
00774                                 $pageLink = Linker::link(
00775                                         $title,
00776                                         null,
00777                                         array(),
00778                                         array(),
00779                                         array( 'broken' )
00780                                 );
00781                         }
00782                         return $this->getLanguage()->specialList( $pageLink, $this->makeWlhLink( $title, $result ) );
00783                 } else {
00784                         return $this->msg( 'wantedpages-badtitle', $result->title )->escaped();
00785                 }
00786         }
00787 
00795         private function makeWlhLink( $title, $result ) {
00796                 $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
00797                 $label = $this->msg( 'nlinks' )->numParams( $result->value )->escaped();
00798                 return Linker::link( $wlh, $label );
00799         }
00800 }