MediaWiki  REL1_23
QueryPage.php
Go to the documentation of this file.
00001 <?php
00030 abstract class QueryPage extends SpecialPage {
00036     var $listoutput = false;
00037 
00043     var $offset = 0;
00044     var $limit = 0;
00045 
00051     protected $numRows;
00052 
00053     protected $cachedTimestamp = null;
00054 
00058     protected $shownavigation = true;
00059 
00068     public static function getPages() {
00069         global $wgDisableCounters;
00070         static $qp = null;
00071 
00072         if ( $qp === null ) {
00073             // QueryPage subclass, Special page name
00074             $qp = array(
00075                 array( 'AncientPagesPage', 'Ancientpages' ),
00076                 array( 'BrokenRedirectsPage', 'BrokenRedirects' ),
00077                 array( 'DeadendPagesPage', 'Deadendpages' ),
00078                 array( 'DoubleRedirectsPage', 'DoubleRedirects' ),
00079                 array( 'FileDuplicateSearchPage', 'FileDuplicateSearch' ),
00080                 array( 'ListDuplicatedFilesPage', 'ListDuplicatedFiles'),
00081                 array( 'LinkSearchPage', 'LinkSearch' ),
00082                 array( 'ListredirectsPage', 'Listredirects' ),
00083                 array( 'LonelyPagesPage', 'Lonelypages' ),
00084                 array( 'LongPagesPage', 'Longpages' ),
00085                 array( 'MIMEsearchPage', 'MIMEsearch' ),
00086                 array( 'MostcategoriesPage', 'Mostcategories' ),
00087                 array( 'MostimagesPage', 'Mostimages' ),
00088                 array( 'MostinterwikisPage', 'Mostinterwikis' ),
00089                 array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ),
00090                 array( 'MostlinkedtemplatesPage', 'Mostlinkedtemplates' ),
00091                 array( 'MostlinkedPage', 'Mostlinked' ),
00092                 array( 'MostrevisionsPage', 'Mostrevisions' ),
00093                 array( 'FewestrevisionsPage', 'Fewestrevisions' ),
00094                 array( 'ShortPagesPage', 'Shortpages' ),
00095                 array( 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ),
00096                 array( 'UncategorizedPagesPage', 'Uncategorizedpages' ),
00097                 array( 'UncategorizedImagesPage', 'Uncategorizedimages' ),
00098                 array( 'UncategorizedTemplatesPage', 'Uncategorizedtemplates' ),
00099                 array( 'UnusedCategoriesPage', 'Unusedcategories' ),
00100                 array( 'UnusedimagesPage', 'Unusedimages' ),
00101                 array( 'WantedCategoriesPage', 'Wantedcategories' ),
00102                 array( 'WantedFilesPage', 'Wantedfiles' ),
00103                 array( 'WantedPagesPage', 'Wantedpages' ),
00104                 array( 'WantedTemplatesPage', 'Wantedtemplates' ),
00105                 array( 'UnwatchedPagesPage', 'Unwatchedpages' ),
00106                 array( 'UnusedtemplatesPage', 'Unusedtemplates' ),
00107                 array( 'WithoutInterwikiPage', 'Withoutinterwiki' ),
00108             );
00109             wfRunHooks( 'wgQueryPages', array( &$qp ) );
00110 
00111             if ( !$wgDisableCounters ) {
00112                 $qp[] = array( 'PopularPagesPage', 'Popularpages' );
00113             }
00114         }
00115 
00116         return $qp;
00117     }
00118 
00124     function setListoutput( $bool ) {
00125         $this->listoutput = $bool;
00126     }
00127 
00154     function getQueryInfo() {
00155         return null;
00156     }
00157 
00164     function getSQL() {
00165         /* Implement getQueryInfo() instead */
00166         throw new MWException( "Bug in a QueryPage: doesn't implement getQueryInfo() nor "
00167             . "getQuery() properly" );
00168     }
00169 
00177     function getOrderFields() {
00178         return array( 'value' );
00179     }
00180 
00191     function usesTimestamps() {
00192         return false;
00193     }
00194 
00200     function sortDescending() {
00201         return true;
00202     }
00203 
00211     function isExpensive() {
00212         global $wgDisableQueryPages;
00213         return $wgDisableQueryPages;
00214     }
00215 
00223     public function isCacheable() {
00224         return true;
00225     }
00226 
00233     function isCached() {
00234         global $wgMiserMode;
00235 
00236         return $this->isExpensive() && $wgMiserMode;
00237     }
00238 
00244     function isSyndicated() {
00245         return true;
00246     }
00247 
00257     abstract function formatResult( $skin, $result );
00258 
00264     function getPageHeader() {
00265         return '';
00266     }
00267 
00275     function linkParameters() {
00276         return array();
00277     }
00278 
00287     function tryLastResult() {
00288         return false;
00289     }
00290 
00299     function recache( $limit, $ignoreErrors = true ) {
00300         if ( !$this->isCacheable() ) {
00301             return 0;
00302         }
00303 
00304         $fname = get_class( $this ) . '::recache';
00305         $dbw = wfGetDB( DB_MASTER );
00306         if ( !$dbw ) {
00307             return false;
00308         }
00309 
00310         try {
00311             # Do query
00312             $res = $this->reallyDoQuery( $limit, false );
00313             $num = false;
00314             if ( $res ) {
00315                 $num = $res->numRows();
00316                 # Fetch results
00317                 $vals = array();
00318                 foreach ( $res as $row ) {
00319                     if ( isset( $row->value ) ) {
00320                         if ( $this->usesTimestamps() ) {
00321                             $value = wfTimestamp( TS_UNIX,
00322                                 $row->value );
00323                         } else {
00324                             $value = intval( $row->value ); // @bug 14414
00325                         }
00326                     } else {
00327                         $value = 0;
00328                     }
00329 
00330                     $vals[] = array( 'qc_type' => $this->getName(),
00331                             'qc_namespace' => $row->namespace,
00332                             'qc_title' => $row->title,
00333                             'qc_value' => $value );
00334                 }
00335 
00336                 $dbw->begin( __METHOD__ );
00337                 # Clear out any old cached data
00338                 $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname );
00339                 # Save results into the querycache table on the master
00340                 if ( count( $vals ) ) {
00341                     $dbw->insert( 'querycache', $vals, __METHOD__ );
00342                 }
00343                 # Update the querycache_info record for the page
00344                 $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname );
00345                 $dbw->insert( 'querycache_info',
00346                     array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ),
00347                     $fname );
00348                 $dbw->commit( __METHOD__ );
00349             }
00350         } catch ( DBError $e ) {
00351             if ( !$ignoreErrors ) {
00352                 throw $e; // report query error
00353             }
00354             $num = false; // set result to false to indicate error
00355         }
00356 
00357         return $num;
00358     }
00359 
00363     function getRecacheDB() {
00364         return wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) );
00365     }
00366 
00374     function reallyDoQuery( $limit, $offset = false ) {
00375         $fname = get_class( $this ) . "::reallyDoQuery";
00376         $dbr = $this->getRecacheDB();
00377         $query = $this->getQueryInfo();
00378         $order = $this->getOrderFields();
00379 
00380         if ( $this->sortDescending() ) {
00381             foreach ( $order as &$field ) {
00382                 $field .= ' DESC';
00383             }
00384         }
00385 
00386         if ( is_array( $query ) ) {
00387             $tables = isset( $query['tables'] ) ? (array)$query['tables'] : array();
00388             $fields = isset( $query['fields'] ) ? (array)$query['fields'] : array();
00389             $conds = isset( $query['conds'] ) ? (array)$query['conds'] : array();
00390             $options = isset( $query['options'] ) ? (array)$query['options'] : array();
00391             $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : array();
00392 
00393             if ( count( $order ) ) {
00394                 $options['ORDER BY'] = $order;
00395             }
00396 
00397             if ( $limit !== false ) {
00398                 $options['LIMIT'] = intval( $limit );
00399             }
00400 
00401             if ( $offset !== false ) {
00402                 $options['OFFSET'] = intval( $offset );
00403             }
00404 
00405             $res = $dbr->select( $tables, $fields, $conds, $fname,
00406                     $options, $join_conds
00407             );
00408         } else {
00409             // Old-fashioned raw SQL style, deprecated
00410             $sql = $this->getSQL();
00411             $sql .= ' ORDER BY ' . implode( ', ', $order );
00412             $sql = $dbr->limitResult( $sql, $limit, $offset );
00413             $res = $dbr->query( $sql, $fname );
00414         }
00415 
00416         return $dbr->resultObject( $res );
00417     }
00418 
00425     function doQuery( $offset = false, $limit = false ) {
00426         if ( $this->isCached() && $this->isCacheable() ) {
00427             return $this->fetchFromCache( $limit, $offset );
00428         } else {
00429             return $this->reallyDoQuery( $limit, $offset );
00430         }
00431     }
00432 
00440     function fetchFromCache( $limit, $offset = false ) {
00441         $dbr = wfGetDB( DB_SLAVE );
00442         $options = array();
00443         if ( $limit !== false ) {
00444             $options['LIMIT'] = intval( $limit );
00445         }
00446         if ( $offset !== false ) {
00447             $options['OFFSET'] = intval( $offset );
00448         }
00449         if ( $this->sortDescending() ) {
00450             $options['ORDER BY'] = 'qc_value DESC';
00451         } else {
00452             $options['ORDER BY'] = 'qc_value ASC';
00453         }
00454         $res = $dbr->select( 'querycache', array( 'qc_type',
00455                 'namespace' => 'qc_namespace',
00456                 'title' => 'qc_title',
00457                 'value' => 'qc_value' ),
00458                 array( 'qc_type' => $this->getName() ),
00459                 __METHOD__, $options
00460         );
00461         return $dbr->resultObject( $res );
00462     }
00463 
00464     public function getCachedTimestamp() {
00465         if ( is_null( $this->cachedTimestamp ) ) {
00466             $dbr = wfGetDB( DB_SLAVE );
00467             $fname = get_class( $this ) . '::getCachedTimestamp';
00468             $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
00469                 array( 'qci_type' => $this->getName() ), $fname );
00470         }
00471         return $this->cachedTimestamp;
00472     }
00473 
00480     function execute( $par ) {
00481         global $wgQueryCacheLimit, $wgDisableQueryPageUpdate;
00482 
00483         $user = $this->getUser();
00484         if ( !$this->userCanExecute( $user ) ) {
00485             $this->displayRestrictionError();
00486             return;
00487         }
00488 
00489         $this->setHeaders();
00490         $this->outputHeader();
00491 
00492         $out = $this->getOutput();
00493 
00494         if ( $this->isCached() && !$this->isCacheable() ) {
00495             $out->addWikiMsg( 'querypage-disabled' );
00496             return 0;
00497         }
00498 
00499         $out->setSyndicated( $this->isSyndicated() );
00500 
00501         if ( $this->limit == 0 && $this->offset == 0 ) {
00502             list( $this->limit, $this->offset ) = $this->getRequest()->getLimitOffset();
00503         }
00504 
00505         // TODO: Use doQuery()
00506         if ( !$this->isCached() ) {
00507             # select one extra row for navigation
00508             $res = $this->reallyDoQuery( $this->limit + 1, $this->offset );
00509         } else {
00510             # Get the cached result, select one extra row for navigation
00511             $res = $this->fetchFromCache( $this->limit + 1, $this->offset );
00512             if ( !$this->listoutput ) {
00513 
00514                 # Fetch the timestamp of this update
00515                 $ts = $this->getCachedTimestamp();
00516                 $lang = $this->getLanguage();
00517                 $maxResults = $lang->formatNum( $wgQueryCacheLimit );
00518 
00519                 if ( $ts ) {
00520                     $updated = $lang->userTimeAndDate( $ts, $user );
00521                     $updateddate = $lang->userDate( $ts, $user );
00522                     $updatedtime = $lang->userTime( $ts, $user );
00523                     $out->addMeta( 'Data-Cache-Time', $ts );
00524                     $out->addJsConfigVars( 'dataCacheTime', $ts );
00525                     $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
00526                 } else {
00527                     $out->addWikiMsg( 'perfcached', $maxResults );
00528                 }
00529 
00530                 # If updates on this page have been disabled, let the user know
00531                 # that the data set won't be refreshed for now
00532                 if ( is_array( $wgDisableQueryPageUpdate )
00533                     && in_array( $this->getName(), $wgDisableQueryPageUpdate )
00534                 ) {
00535                     $out->wrapWikiMsg(
00536                         "<div class=\"mw-querypage-no-updates\">\n$1\n</div>",
00537                         'querypage-no-updates'
00538                     );
00539                 }
00540             }
00541         }
00542 
00543         $this->numRows = $res->numRows();
00544 
00545         $dbr = wfGetDB( DB_SLAVE );
00546         $this->preprocessResults( $dbr, $res );
00547 
00548         $out->addHTML( Xml::openElement( 'div', array( 'class' => 'mw-spcontent' ) ) );
00549 
00550         # Top header and navigation
00551         if ( $this->shownavigation ) {
00552             $out->addHTML( $this->getPageHeader() );
00553             if ( $this->numRows > 0 ) {
00554                 $out->addHTML( $this->msg( 'showingresultsinrange' )->numParams(
00555                     min( $this->numRows, $this->limit ), # do not show the one extra row, if exist
00556                     $this->offset + 1, ( min( $this->numRows, $this->limit ) + $this->offset ) )->parseAsBlock() );
00557                 # Disable the "next" link when we reach the end
00558                 $paging = $this->getLanguage()->viewPrevNext( $this->getPageTitle( $par ), $this->offset,
00559                     $this->limit, $this->linkParameters(), ( $this->numRows <= $this->limit ) );
00560                 $out->addHTML( '<p>' . $paging . '</p>' );
00561             } else {
00562                 # No results to show, so don't bother with "showing X of Y" etc.
00563                 # -- just let the user know and give up now
00564                 $out->addWikiMsg( 'specialpage-empty' );
00565                 $out->addHTML( Xml::closeElement( 'div' ) );
00566                 return;
00567             }
00568         }
00569 
00570         # The actual results; specialist subclasses will want to handle this
00571         # with more than a straight list, so we hand them the info, plus
00572         # an OutputPage, and let them get on with it
00573         $this->outputResults( $out,
00574             $this->getSkin(),
00575             $dbr, # Should use a ResultWrapper for this
00576             $res,
00577             min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
00578             $this->offset );
00579 
00580         # Repeat the paging links at the bottom
00581         if ( $this->shownavigation ) {
00582             $out->addHTML( '<p>' . $paging . '</p>' );
00583         }
00584 
00585         $out->addHTML( Xml::closeElement( 'div' ) );
00586 
00587         return min( $this->numRows, $this->limit ); # do not return the one extra row, if exist
00588     }
00589 
00601     protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
00602         global $wgContLang;
00603 
00604         if ( $num > 0 ) {
00605             $html = array();
00606             if ( !$this->listoutput ) {
00607                 $html[] = $this->openList( $offset );
00608             }
00609 
00610             # $res might contain the whole 1,000 rows, so we read up to
00611             # $num [should update this to use a Pager]
00612             for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
00613                 $line = $this->formatResult( $skin, $row );
00614                 if ( $line ) {
00615                     $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
00616                         ? ' class="not-patrolled"'
00617                         : '';
00618                     $html[] = $this->listoutput
00619                         ? $line
00620                         : "<li{$attr}>{$line}</li>\n";
00621                 }
00622             }
00623 
00624             # Flush the final result
00625             if ( $this->tryLastResult() ) {
00626                 $row = null;
00627                 $line = $this->formatResult( $skin, $row );
00628                 if ( $line ) {
00629                     $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
00630                         ? ' class="not-patrolled"'
00631                         : '';
00632                     $html[] = $this->listoutput
00633                         ? $line
00634                         : "<li{$attr}>{$line}</li>\n";
00635                 }
00636             }
00637 
00638             if ( !$this->listoutput ) {
00639                 $html[] = $this->closeList();
00640             }
00641 
00642             $html = $this->listoutput
00643                 ? $wgContLang->listToText( $html )
00644                 : implode( '', $html );
00645 
00646             $out->addHTML( $html );
00647         }
00648     }
00649 
00654     function openList( $offset ) {
00655         return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
00656     }
00657 
00661     function closeList() {
00662         return "</ol>\n";
00663     }
00664 
00670     function preprocessResults( $db, $res ) {}
00671 
00678     function doFeed( $class = '', $limit = 50 ) {
00679         global $wgFeed, $wgFeedClasses, $wgFeedLimit;
00680 
00681         if ( !$wgFeed ) {
00682             $this->getOutput()->addWikiMsg( 'feed-unavailable' );
00683             return false;
00684         }
00685 
00686         $limit = min( $limit, $wgFeedLimit );
00687 
00688         if ( isset( $wgFeedClasses[$class] ) ) {
00689             $feed = new $wgFeedClasses[$class](
00690                 $this->feedTitle(),
00691                 $this->feedDesc(),
00692                 $this->feedUrl() );
00693             $feed->outHeader();
00694 
00695             $res = $this->reallyDoQuery( $limit, 0 );
00696             foreach ( $res as $obj ) {
00697                 $item = $this->feedResult( $obj );
00698                 if ( $item ) {
00699                     $feed->outItem( $item );
00700                 }
00701             }
00702 
00703             $feed->outFooter();
00704             return true;
00705         } else {
00706             return false;
00707         }
00708     }
00709 
00716     function feedResult( $row ) {
00717         if ( !isset( $row->title ) ) {
00718             return null;
00719         }
00720         $title = Title::makeTitle( intval( $row->namespace ), $row->title );
00721         if ( $title ) {
00722             $date = isset( $row->timestamp ) ? $row->timestamp : '';
00723             $comments = '';
00724             if ( $title ) {
00725                 $talkpage = $title->getTalkPage();
00726                 $comments = $talkpage->getFullURL();
00727             }
00728 
00729             return new FeedItem(
00730                 $title->getPrefixedText(),
00731                 $this->feedItemDesc( $row ),
00732                 $title->getFullURL(),
00733                 $date,
00734                 $this->feedItemAuthor( $row ),
00735                 $comments );
00736         } else {
00737             return null;
00738         }
00739     }
00740 
00741     function feedItemDesc( $row ) {
00742         return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : '';
00743     }
00744 
00745     function feedItemAuthor( $row ) {
00746         return isset( $row->user_text ) ? $row->user_text : '';
00747     }
00748 
00749     function feedTitle() {
00750         global $wgLanguageCode, $wgSitename;
00751         $desc = $this->getDescription();
00752         return "$wgSitename - $desc [$wgLanguageCode]";
00753     }
00754 
00755     function feedDesc() {
00756         return $this->msg( 'tagline' )->text();
00757     }
00758 
00759     function feedUrl() {
00760         return $this->getPageTitle()->getFullURL();
00761     }
00762 }
00763 
00768 abstract class WantedQueryPage extends QueryPage {
00769     function isExpensive() {
00770         return true;
00771     }
00772 
00773     function isSyndicated() {
00774         return false;
00775     }
00776 
00782     function preprocessResults( $db, $res ) {
00783         if ( !$res->numRows() ) {
00784             return;
00785         }
00786 
00787         $batch = new LinkBatch;
00788         foreach ( $res as $row ) {
00789             $batch->add( $row->namespace, $row->title );
00790         }
00791         $batch->execute();
00792 
00793         // Back to start for display
00794         $res->seek( 0 );
00795     }
00796 
00805     function forceExistenceCheck() {
00806         return false;
00807     }
00808 
00816     public function formatResult( $skin, $result ) {
00817         $title = Title::makeTitleSafe( $result->namespace, $result->title );
00818         if ( $title instanceof Title ) {
00819             if ( $this->isCached() || $this->forceExistenceCheck() ) {
00820                 $pageLink = $title->isKnown()
00821                     ? '<del>' . Linker::link( $title ) . '</del>'
00822                     : Linker::link(
00823                         $title,
00824                         null,
00825                         array(),
00826                         array(),
00827                         array( 'broken' )
00828                     );
00829             } else {
00830                 $pageLink = Linker::link(
00831                     $title,
00832                     null,
00833                     array(),
00834                     array(),
00835                     array( 'broken' )
00836                 );
00837             }
00838             return $this->getLanguage()->specialList( $pageLink, $this->makeWlhLink( $title, $result ) );
00839         } else {
00840             return $this->msg( 'wantedpages-badtitle', $result->title )->escaped();
00841         }
00842     }
00843 
00851     private function makeWlhLink( $title, $result ) {
00852         $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
00853         $label = $this->msg( 'nlinks' )->numParams( $result->value )->escaped();
00854         return Linker::link( $wlh, $label );
00855     }
00856 }