MediaWiki  master
QueryPage.php
Go to the documentation of this file.
00001 <?php
00031 global $wgQueryPages; // not redundant
00032 $wgQueryPages = array(
00033 //         QueryPage subclass           Special page name         Limit (false for none, none for the default)
00034 // ----------------------------------------------------------------------------
00035         array( 'AncientPagesPage',              'Ancientpages'                  ),
00036         array( 'BrokenRedirectsPage',           'BrokenRedirects'               ),
00037         array( 'DeadendPagesPage',              'Deadendpages'                  ),
00038         array( 'DisambiguationsPage',           'Disambiguations'               ),
00039         array( 'DoubleRedirectsPage',           'DoubleRedirects'               ),
00040         array( 'FileDuplicateSearchPage',       'FileDuplicateSearch'           ),
00041         array( 'LinkSearchPage',                'LinkSearch'                    ),
00042         array( 'ListredirectsPage',             'Listredirects'                 ),
00043         array( 'LonelyPagesPage',               'Lonelypages'                   ),
00044         array( 'LongPagesPage',                 'Longpages'                     ),
00045         array( 'MIMEsearchPage',                'MIMEsearch'                    ),
00046         array( 'MostcategoriesPage',            'Mostcategories'                ),
00047         array( 'MostimagesPage',                'Mostimages'                    ),
00048         array( 'MostinterwikisPage',            'Mostinterwikis'                ),
00049         array( 'MostlinkedCategoriesPage',      'Mostlinkedcategories'          ),
00050         array( 'MostlinkedtemplatesPage',       'Mostlinkedtemplates'           ),
00051         array( 'MostlinkedPage',                'Mostlinked'                    ),
00052         array( 'MostrevisionsPage',             'Mostrevisions'                 ),
00053         array( 'FewestrevisionsPage',           'Fewestrevisions'               ),
00054         array( 'ShortPagesPage',                'Shortpages'                    ),
00055         array( 'UncategorizedCategoriesPage',   'Uncategorizedcategories'       ),
00056         array( 'UncategorizedPagesPage',        'Uncategorizedpages'            ),
00057         array( 'UncategorizedImagesPage',       'Uncategorizedimages'           ),
00058         array( 'UncategorizedTemplatesPage',    'Uncategorizedtemplates'        ),
00059         array( 'UnusedCategoriesPage',          'Unusedcategories'              ),
00060         array( 'UnusedimagesPage',              'Unusedimages'                  ),
00061         array( 'WantedCategoriesPage',          'Wantedcategories'              ),
00062         array( 'WantedFilesPage',               'Wantedfiles'                   ),
00063         array( 'WantedPagesPage',               'Wantedpages'                   ),
00064         array( 'WantedTemplatesPage',           'Wantedtemplates'               ),
00065         array( 'UnwatchedPagesPage',            'Unwatchedpages'                ),
00066         array( 'UnusedtemplatesPage',           'Unusedtemplates'               ),
00067         array( 'WithoutInterwikiPage',          'Withoutinterwiki'              ),
00068 );
00069 wfRunHooks( 'wgQueryPages', array( &$wgQueryPages ) );
00070 
00071 global $wgDisableCounters;
00072 if ( !$wgDisableCounters )
00073         $wgQueryPages[] = array( 'PopularPagesPage', 'Popularpages' );
00074 
00075 
00082 abstract class QueryPage extends SpecialPage {
00088         var $listoutput = false;
00089 
00095         var $offset = 0;
00096         var $limit = 0;
00097 
00103         protected $numRows;
00104 
00105         protected $cachedTimestamp = null;
00106 
00110         protected $shownavigation = true;
00111 
00117         function setListoutput( $bool ) {
00118                 $this->listoutput = $bool;
00119         }
00120 
00147         function getQueryInfo() {
00148                 return null;
00149         }
00150 
00157         function getSQL() {
00158                 /* Implement getQueryInfo() instead */
00159                 throw new MWException( "Bug in a QueryPage: doesn't implement getQueryInfo() nor getQuery() properly" );
00160         }
00161 
00169         function getOrderFields() {
00170                 return array( 'value' );
00171         }
00172 
00183         function usesTimestamps() {
00184                 return false;
00185         }
00186 
00192         function sortDescending() {
00193                 return true;
00194         }
00195 
00203         function isExpensive() {
00204                 global $wgDisableQueryPages;
00205                 return $wgDisableQueryPages;
00206         }
00207 
00215         public function isCacheable() {
00216                 return true;
00217         }
00218 
00225         function isCached() {
00226                 global $wgMiserMode;
00227 
00228                 return $this->isExpensive() && $wgMiserMode;
00229         }
00230 
00236         function isSyndicated() {
00237                 return true;
00238         }
00239 
00252         abstract function formatResult( $skin, $result );
00253 
00259         function getPageHeader() {
00260                 return '';
00261         }
00262 
00270         function linkParameters() {
00271                 return array();
00272         }
00273 
00282         function tryLastResult() {
00283                 return false;
00284         }
00285 
00293         function recache( $limit, $ignoreErrors = true ) {
00294                 if ( !$this->isCacheable() ) {
00295                         return 0;
00296                 }
00297 
00298                 $fname = get_class( $this ) . '::recache';
00299                 $dbw = wfGetDB( DB_MASTER );
00300                 $dbr = wfGetDB( DB_SLAVE, array( $this->getName(), __METHOD__, 'vslow' ) );
00301                 if ( !$dbw || !$dbr ) {
00302                         return false;
00303                 }
00304 
00305                 if ( $ignoreErrors ) {
00306                         $ignoreW = $dbw->ignoreErrors( true );
00307                         $ignoreR = $dbr->ignoreErrors( true );
00308                 }
00309 
00310                 # Clear out any old cached data
00311                 $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname );
00312                 # Do query
00313                 $res = $this->reallyDoQuery( $limit, false );
00314                 $num = false;
00315                 if ( $res ) {
00316                         $num = $res->numRows();
00317                         # Fetch results
00318                         $vals = array();
00319                         while ( $res && $row = $dbr->fetchObject( $res ) ) {
00320                                 if ( isset( $row->value ) ) {
00321                                         if ( $this->usesTimestamps() ) {
00322                                                 $value = wfTimestamp( TS_UNIX,
00323                                                         $row->value );
00324                                         } else {
00325                                                 $value = intval( $row->value ); // @bug 14414
00326                                         }
00327                                 } else {
00328                                         $value = 0;
00329                                 }
00330 
00331                                 $vals[] = array( 'qc_type' => $this->getName(),
00332                                                 'qc_namespace' => $row->namespace,
00333                                                 'qc_title' => $row->title,
00334                                                 'qc_value' => $value );
00335                         }
00336 
00337                         # Save results into the querycache table on the master
00338                         if ( count( $vals ) ) {
00339                                 if ( !$dbw->insert( 'querycache', $vals, __METHOD__ ) ) {
00340                                         // Set result to false to indicate error
00341                                         $num = false;
00342                                 }
00343                         }
00344                         if ( $ignoreErrors ) {
00345                                 $dbw->ignoreErrors( $ignoreW );
00346                                 $dbr->ignoreErrors( $ignoreR );
00347                         }
00348 
00349                         # Update the querycache_info record for the page
00350                         $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname );
00351                         $dbw->insert( 'querycache_info', array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ), $fname );
00352 
00353                 }
00354                 return $num;
00355         }
00356 
00364         function reallyDoQuery( $limit, $offset = false ) {
00365                 $fname = get_class( $this ) . "::reallyDoQuery";
00366                 $dbr = wfGetDB( DB_SLAVE );
00367                 $query = $this->getQueryInfo();
00368                 $order = $this->getOrderFields();
00369                 if ( $this->sortDescending() ) {
00370                         foreach ( $order as &$field ) {
00371                                 $field .= ' DESC';
00372                         }
00373                 }
00374                 if ( is_array( $query ) ) {
00375                         $tables = isset( $query['tables'] ) ? (array)$query['tables'] : array();
00376                         $fields = isset( $query['fields'] ) ? (array)$query['fields'] : array();
00377                         $conds = isset( $query['conds'] ) ? (array)$query['conds'] : array();
00378                         $options = isset( $query['options'] ) ? (array)$query['options'] : array();
00379                         $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : array();
00380                         if ( count( $order ) ) {
00381                                 $options['ORDER BY'] = $order;
00382                         }
00383                         if ( $limit !== false ) {
00384                                 $options['LIMIT'] = intval( $limit );
00385                         }
00386                         if ( $offset !== false ) {
00387                                 $options['OFFSET'] = intval( $offset );
00388                         }
00389 
00390                         $res = $dbr->select( $tables, $fields, $conds, $fname,
00391                                         $options, $join_conds
00392                         );
00393                 } else {
00394                         // Old-fashioned raw SQL style, deprecated
00395                         $sql = $this->getSQL();
00396                         $sql .= ' ORDER BY ' . implode( ', ', $order );
00397                         $sql = $dbr->limitResult( $sql, $limit, $offset );
00398                         $res = $dbr->query( $sql, $fname );
00399                 }
00400                 return $dbr->resultObject( $res );
00401         }
00402 
00407         function doQuery( $offset = false, $limit = false ) {
00408                 if ( $this->isCached() && $this->isCacheable() ) {
00409                         return $this->fetchFromCache( $limit, $offset );
00410                 } else {
00411                         return $this->reallyDoQuery( $limit, $offset );
00412                 }
00413         }
00414 
00422         function fetchFromCache( $limit, $offset = false ) {
00423                 $dbr = wfGetDB( DB_SLAVE );
00424                 $options = array ();
00425                 if ( $limit !== false ) {
00426                         $options['LIMIT'] = intval( $limit );
00427                 }
00428                 if ( $offset !== false ) {
00429                         $options['OFFSET'] = intval( $offset );
00430                 }
00431                 if ( $this->sortDescending() ) {
00432                         $options['ORDER BY'] = 'qc_value DESC';
00433                 } else {
00434                         $options['ORDER BY'] = 'qc_value ASC';
00435                 }
00436                 $res = $dbr->select( 'querycache', array( 'qc_type',
00437                                 'namespace' => 'qc_namespace',
00438                                 'title' => 'qc_title',
00439                                 'value' => 'qc_value' ),
00440                                 array( 'qc_type' => $this->getName() ),
00441                                 __METHOD__, $options
00442                 );
00443                 return $dbr->resultObject( $res );
00444         }
00445 
00446         public function getCachedTimestamp() {
00447                 if ( is_null( $this->cachedTimestamp ) ) {
00448                         $dbr = wfGetDB( DB_SLAVE );
00449                         $fname = get_class( $this ) . '::getCachedTimestamp';
00450                         $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
00451                                 array( 'qci_type' => $this->getName() ), $fname );
00452                 }
00453                 return $this->cachedTimestamp;
00454         }
00455 
00461         function execute( $par ) {
00462                 global $wgQueryCacheLimit, $wgDisableQueryPageUpdate;
00463 
00464                 $user = $this->getUser();
00465                 if ( !$this->userCanExecute( $user ) ) {
00466                         $this->displayRestrictionError();
00467                         return;
00468                 }
00469 
00470                 $this->setHeaders();
00471                 $this->outputHeader();
00472 
00473                 $out = $this->getOutput();
00474 
00475                 if ( $this->isCached() && !$this->isCacheable() ) {
00476                         $out->addWikiMsg( 'querypage-disabled' );
00477                         return 0;
00478                 }
00479 
00480                 $out->setSyndicated( $this->isSyndicated() );
00481 
00482                 if ( $this->limit == 0 && $this->offset == 0 ) {
00483                         list( $this->limit, $this->offset ) = $this->getRequest()->getLimitOffset();
00484                 }
00485 
00486                 // TODO: Use doQuery()
00487                 if ( !$this->isCached() ) {
00488                         # select one extra row for navigation
00489                         $res = $this->reallyDoQuery( $this->limit + 1, $this->offset );
00490                 } else {
00491                         # Get the cached result, select one extra row for navigation
00492                         $res = $this->fetchFromCache( $this->limit + 1, $this->offset );
00493                         if ( !$this->listoutput ) {
00494 
00495                                 # Fetch the timestamp of this update
00496                                 $ts = $this->getCachedTimestamp();
00497                                 $lang = $this->getLanguage();
00498                                 $maxResults = $lang->formatNum( $wgQueryCacheLimit );
00499 
00500                                 if ( $ts ) {
00501                                         $updated = $lang->userTimeAndDate( $ts, $user );
00502                                         $updateddate = $lang->userDate( $ts, $user );
00503                                         $updatedtime = $lang->userTime( $ts, $user );
00504                                         $out->addMeta( 'Data-Cache-Time', $ts );
00505                                         $out->addJsConfigVars( 'dataCacheTime', $ts );
00506                                         $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
00507                                 } else {
00508                                         $out->addWikiMsg( 'perfcached', $maxResults );
00509                                 }
00510 
00511                                 # If updates on this page have been disabled, let the user know
00512                                 # that the data set won't be refreshed for now
00513                                 if ( is_array( $wgDisableQueryPageUpdate ) && in_array( $this->getName(), $wgDisableQueryPageUpdate ) ) {
00514                                         $out->wrapWikiMsg( "<div class=\"mw-querypage-no-updates\">\n$1\n</div>", 'querypage-no-updates' );
00515                                 }
00516                         }
00517                 }
00518 
00519                 $this->numRows = $res->numRows();
00520 
00521                 $dbr = wfGetDB( DB_SLAVE );
00522                 $this->preprocessResults( $dbr, $res );
00523 
00524                 $out->addHTML( Xml::openElement( 'div', array( 'class' => 'mw-spcontent' ) ) );
00525 
00526                 # Top header and navigation
00527                 if ( $this->shownavigation ) {
00528                         $out->addHTML( $this->getPageHeader() );
00529                         if ( $this->numRows > 0 ) {
00530                                 $out->addHTML( $this->msg( 'showingresults' )->numParams(
00531                                         min( $this->numRows, $this->limit ), # do not show the one extra row, if exist
00532                                         $this->offset + 1 )->parseAsBlock() );
00533                                 # Disable the "next" link when we reach the end
00534                                 $paging = $this->getLanguage()->viewPrevNext( $this->getTitle( $par ), $this->offset,
00535                                         $this->limit, $this->linkParameters(), ( $this->numRows <= $this->limit ) );
00536                                 $out->addHTML( '<p>' . $paging . '</p>' );
00537                         } else {
00538                                 # No results to show, so don't bother with "showing X of Y" etc.
00539                                 # -- just let the user know and give up now
00540                                 $out->addWikiMsg( 'specialpage-empty' );
00541                                 $out->addHTML( Xml::closeElement( 'div' ) );
00542                                 return;
00543                         }
00544                 }
00545 
00546                 # The actual results; specialist subclasses will want to handle this
00547                 # with more than a straight list, so we hand them the info, plus
00548                 # an OutputPage, and let them get on with it
00549                 $this->outputResults( $out,
00550                         $this->getSkin(),
00551                         $dbr, # Should use a ResultWrapper for this
00552                         $res,
00553                         min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
00554                         $this->offset );
00555 
00556                 # Repeat the paging links at the bottom
00557                 if ( $this->shownavigation ) {
00558                         $out->addHTML( '<p>' . $paging . '</p>' );
00559                 }
00560 
00561                 $out->addHTML( Xml::closeElement( 'div' ) );
00562 
00563                 return min( $this->numRows, $this->limit ); # do not return the one extra row, if exist
00564         }
00565 
00577         protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
00578                 global $wgContLang;
00579 
00580                 if ( $num > 0 ) {
00581                         $html = array();
00582                         if ( !$this->listoutput ) {
00583                                 $html[] = $this->openList( $offset );
00584                         }
00585 
00586                         # $res might contain the whole 1,000 rows, so we read up to
00587                         # $num [should update this to use a Pager]
00588                         for ( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) {
00589                                 $line = $this->formatResult( $skin, $row );
00590                                 if ( $line ) {
00591                                         $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
00592                                                 ? ' class="not-patrolled"'
00593                                                 : '';
00594                                         $html[] = $this->listoutput
00595                                                 ? $line
00596                                                 : "<li{$attr}>{$line}</li>\n";
00597                                 }
00598                         }
00599 
00600                         # Flush the final result
00601                         if ( $this->tryLastResult() ) {
00602                                 $row = null;
00603                                 $line = $this->formatResult( $skin, $row );
00604                                 if ( $line ) {
00605                                         $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
00606                                                 ? ' class="not-patrolled"'
00607                                                 : '';
00608                                         $html[] = $this->listoutput
00609                                                 ? $line
00610                                                 : "<li{$attr}>{$line}</li>\n";
00611                                 }
00612                         }
00613 
00614                         if ( !$this->listoutput ) {
00615                                 $html[] = $this->closeList();
00616                         }
00617 
00618                         $html = $this->listoutput
00619                                 ? $wgContLang->listToText( $html )
00620                                 : implode( '', $html );
00621 
00622                         $out->addHTML( $html );
00623                 }
00624         }
00625 
00630         function openList( $offset ) {
00631                 return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
00632         }
00633 
00637         function closeList() {
00638                 return "</ol>\n";
00639         }
00640 
00644         function preprocessResults( $db, $res ) {}
00645 
00650         function doFeed( $class = '', $limit = 50 ) {
00651                 global $wgFeed, $wgFeedClasses;
00652 
00653                 if ( !$wgFeed ) {
00654                         $this->getOutput()->addWikiMsg( 'feed-unavailable' );
00655                         return;
00656                 }
00657 
00658                 global $wgFeedLimit;
00659                 if ( $limit > $wgFeedLimit ) {
00660                         $limit = $wgFeedLimit;
00661                 }
00662 
00663                 if ( isset( $wgFeedClasses[$class] ) ) {
00664                         $feed = new $wgFeedClasses[$class](
00665                                 $this->feedTitle(),
00666                                 $this->feedDesc(),
00667                                 $this->feedUrl() );
00668                         $feed->outHeader();
00669 
00670                         $res = $this->reallyDoQuery( $limit, 0 );
00671                         foreach ( $res as $obj ) {
00672                                 $item = $this->feedResult( $obj );
00673                                 if ( $item ) {
00674                                         $feed->outItem( $item );
00675                                 }
00676                         }
00677 
00678                         $feed->outFooter();
00679                         return true;
00680                 } else {
00681                         return false;
00682                 }
00683         }
00684 
00690         function feedResult( $row ) {
00691                 if ( !isset( $row->title ) ) {
00692                         return null;
00693                 }
00694                 $title = Title::makeTitle( intval( $row->namespace ), $row->title );
00695                 if ( $title ) {
00696                         $date = isset( $row->timestamp ) ? $row->timestamp : '';
00697                         $comments = '';
00698                         if ( $title ) {
00699                                 $talkpage = $title->getTalkPage();
00700                                 $comments = $talkpage->getFullURL();
00701                         }
00702 
00703                         return new FeedItem(
00704                                 $title->getPrefixedText(),
00705                                 $this->feedItemDesc( $row ),
00706                                 $title->getFullURL(),
00707                                 $date,
00708                                 $this->feedItemAuthor( $row ),
00709                                 $comments );
00710                 } else {
00711                         return null;
00712                 }
00713         }
00714 
00715         function feedItemDesc( $row ) {
00716                 return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : '';
00717         }
00718 
00719         function feedItemAuthor( $row ) {
00720                 return isset( $row->user_text ) ? $row->user_text : '';
00721         }
00722 
00723         function feedTitle() {
00724                 global $wgLanguageCode, $wgSitename;
00725                 $desc = $this->getDescription();
00726                 return "$wgSitename - $desc [$wgLanguageCode]";
00727         }
00728 
00729         function feedDesc() {
00730                 return $this->msg( 'tagline' )->text();
00731         }
00732 
00733         function feedUrl() {
00734                 return $this->getTitle()->getFullURL();
00735         }
00736 }
00737 
00742 abstract class WantedQueryPage extends QueryPage {
00743 
00744         function isExpensive() {
00745                 return true;
00746         }
00747 
00748         function isSyndicated() {
00749                 return false;
00750         }
00751 
00755         function preprocessResults( $db, $res ) {
00756                 if ( !$res->numRows() ) {
00757                         return;
00758                 }
00759 
00760                 $batch = new LinkBatch;
00761                 foreach ( $res as $row ) {
00762                         $batch->add( $row->namespace, $row->title );
00763                 }
00764                 $batch->execute();
00765 
00766                 // Back to start for display
00767                 $res->seek( 0 );
00768         }
00769 
00778         function forceExistenceCheck() {
00779                 return false;
00780         }
00781 
00789         public function formatResult( $skin, $result ) {
00790                 $title = Title::makeTitleSafe( $result->namespace, $result->title );
00791                 if ( $title instanceof Title ) {
00792                         if ( $this->isCached() || $this->forceExistenceCheck() ) {
00793                                 $pageLink = $title->isKnown()
00794                                         ? '<del>' . Linker::link( $title ) . '</del>'
00795                                         : Linker::link(
00796                                                 $title,
00797                                                 null,
00798                                                 array(),
00799                                                 array(),
00800                                                 array( 'broken' )
00801                                         );
00802                         } else {
00803                                 $pageLink = Linker::link(
00804                                         $title,
00805                                         null,
00806                                         array(),
00807                                         array(),
00808                                         array( 'broken' )
00809                                 );
00810                         }
00811                         return $this->getLanguage()->specialList( $pageLink, $this->makeWlhLink( $title, $result ) );
00812                 } else {
00813                         return $this->msg( 'wantedpages-badtitle', $result->title )->escaped();
00814                 }
00815         }
00816 
00824         private function makeWlhLink( $title, $result ) {
00825                 $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
00826                 $label = $this->msg( 'nlinks' )->numParams( $result->value )->escaped();
00827                 return Linker::link( $wlh, $label );
00828         }
00829 }