MediaWiki  REL1_21
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 
00081 abstract class QueryPage extends SpecialPage {
00087         var $listoutput = false;
00088 
00094         var $offset = 0;
00095         var $limit = 0;
00096 
00102         protected $numRows;
00103 
00104         protected $cachedTimestamp = null;
00105 
00109         protected $shownavigation = true;
00110 
00116         function setListoutput( $bool ) {
00117                 $this->listoutput = $bool;
00118         }
00119 
00146         function getQueryInfo() {
00147                 return null;
00148         }
00149 
00156         function getSQL() {
00157                 /* Implement getQueryInfo() instead */
00158                 throw new MWException( "Bug in a QueryPage: doesn't implement getQueryInfo() nor getQuery() properly" );
00159         }
00160 
00168         function getOrderFields() {
00169                 return array( 'value' );
00170         }
00171 
00182         function usesTimestamps() {
00183                 return false;
00184         }
00185 
00191         function sortDescending() {
00192                 return true;
00193         }
00194 
00202         function isExpensive() {
00203                 global $wgDisableQueryPages;
00204                 return $wgDisableQueryPages;
00205         }
00206 
00214         public function isCacheable() {
00215                 return true;
00216         }
00217 
00224         function isCached() {
00225                 global $wgMiserMode;
00226 
00227                 return $this->isExpensive() && $wgMiserMode;
00228         }
00229 
00235         function isSyndicated() {
00236                 return true;
00237         }
00238 
00251         abstract function formatResult( $skin, $result );
00252 
00258         function getPageHeader() {
00259                 return '';
00260         }
00261 
00269         function linkParameters() {
00270                 return array();
00271         }
00272 
00281         function tryLastResult() {
00282                 return false;
00283         }
00284 
00292         function recache( $limit, $ignoreErrors = true ) {
00293                 if ( !$this->isCacheable() ) {
00294                         return 0;
00295                 }
00296 
00297                 $fname = get_class( $this ) . '::recache';
00298                 $dbw = wfGetDB( DB_MASTER );
00299                 $dbr = wfGetDB( DB_SLAVE, array( $this->getName(), __METHOD__, 'vslow' ) );
00300                 if ( !$dbw || !$dbr ) {
00301                         return false;
00302                 }
00303 
00304                 try {
00305                         # Clear out any old cached data
00306                         $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname );
00307                         # Do query
00308                         $res = $this->reallyDoQuery( $limit, false );
00309                         $num = false;
00310                         if ( $res ) {
00311                                 $num = $res->numRows();
00312                                 # Fetch results
00313                                 $vals = array();
00314                                 while ( $res && $row = $dbr->fetchObject( $res ) ) {
00315                                         if ( isset( $row->value ) ) {
00316                                                 if ( $this->usesTimestamps() ) {
00317                                                         $value = wfTimestamp( TS_UNIX,
00318                                                                 $row->value );
00319                                                 } else {
00320                                                         $value = intval( $row->value ); // @bug 14414
00321                                                 }
00322                                         } else {
00323                                                 $value = 0;
00324                                         }
00325 
00326                                         $vals[] = array( 'qc_type' => $this->getName(),
00327                                                         'qc_namespace' => $row->namespace,
00328                                                         'qc_title' => $row->title,
00329                                                         'qc_value' => $value );
00330                                 }
00331 
00332                                 # Save results into the querycache table on the master
00333                                 if ( count( $vals ) ) {
00334                                         $dbw->insert( 'querycache', $vals, __METHOD__ );
00335                                 }
00336                                 # Update the querycache_info record for the page
00337                                 $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname );
00338                                 $dbw->insert( 'querycache_info',
00339                                         array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ),
00340                                         $fname );
00341                         }
00342                 } catch ( DBError $e ) {
00343                         if ( !$ignoreErrors ) {
00344                                 throw $e; // report query error
00345                         }
00346                         $num = false; // set result to false to indicate error
00347                 }
00348 
00349                 return $num;
00350         }
00351 
00359         function reallyDoQuery( $limit, $offset = false ) {
00360                 $fname = get_class( $this ) . "::reallyDoQuery";
00361                 $dbr = wfGetDB( DB_SLAVE );
00362                 $query = $this->getQueryInfo();
00363                 $order = $this->getOrderFields();
00364                 if ( $this->sortDescending() ) {
00365                         foreach ( $order as &$field ) {
00366                                 $field .= ' DESC';
00367                         }
00368                 }
00369                 if ( is_array( $query ) ) {
00370                         $tables = isset( $query['tables'] ) ? (array)$query['tables'] : array();
00371                         $fields = isset( $query['fields'] ) ? (array)$query['fields'] : array();
00372                         $conds = isset( $query['conds'] ) ? (array)$query['conds'] : array();
00373                         $options = isset( $query['options'] ) ? (array)$query['options'] : array();
00374                         $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : array();
00375                         if ( count( $order ) ) {
00376                                 $options['ORDER BY'] = $order;
00377                         }
00378                         if ( $limit !== false ) {
00379                                 $options['LIMIT'] = intval( $limit );
00380                         }
00381                         if ( $offset !== false ) {
00382                                 $options['OFFSET'] = intval( $offset );
00383                         }
00384 
00385                         $res = $dbr->select( $tables, $fields, $conds, $fname,
00386                                         $options, $join_conds
00387                         );
00388                 } else {
00389                         // Old-fashioned raw SQL style, deprecated
00390                         $sql = $this->getSQL();
00391                         $sql .= ' ORDER BY ' . implode( ', ', $order );
00392                         $sql = $dbr->limitResult( $sql, $limit, $offset );
00393                         $res = $dbr->query( $sql, $fname );
00394                 }
00395                 return $dbr->resultObject( $res );
00396         }
00397 
00402         function doQuery( $offset = false, $limit = false ) {
00403                 if ( $this->isCached() && $this->isCacheable() ) {
00404                         return $this->fetchFromCache( $limit, $offset );
00405                 } else {
00406                         return $this->reallyDoQuery( $limit, $offset );
00407                 }
00408         }
00409 
00417         function fetchFromCache( $limit, $offset = false ) {
00418                 $dbr = wfGetDB( DB_SLAVE );
00419                 $options = array ();
00420                 if ( $limit !== false ) {
00421                         $options['LIMIT'] = intval( $limit );
00422                 }
00423                 if ( $offset !== false ) {
00424                         $options['OFFSET'] = intval( $offset );
00425                 }
00426                 if ( $this->sortDescending() ) {
00427                         $options['ORDER BY'] = 'qc_value DESC';
00428                 } else {
00429                         $options['ORDER BY'] = 'qc_value ASC';
00430                 }
00431                 $res = $dbr->select( 'querycache', array( 'qc_type',
00432                                 'namespace' => 'qc_namespace',
00433                                 'title' => 'qc_title',
00434                                 'value' => 'qc_value' ),
00435                                 array( 'qc_type' => $this->getName() ),
00436                                 __METHOD__, $options
00437                 );
00438                 return $dbr->resultObject( $res );
00439         }
00440 
00441         public function getCachedTimestamp() {
00442                 if ( is_null( $this->cachedTimestamp ) ) {
00443                         $dbr = wfGetDB( DB_SLAVE );
00444                         $fname = get_class( $this ) . '::getCachedTimestamp';
00445                         $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
00446                                 array( 'qci_type' => $this->getName() ), $fname );
00447                 }
00448                 return $this->cachedTimestamp;
00449         }
00450 
00456         function execute( $par ) {
00457                 global $wgQueryCacheLimit, $wgDisableQueryPageUpdate;
00458 
00459                 $user = $this->getUser();
00460                 if ( !$this->userCanExecute( $user ) ) {
00461                         $this->displayRestrictionError();
00462                         return;
00463                 }
00464 
00465                 $this->setHeaders();
00466                 $this->outputHeader();
00467 
00468                 $out = $this->getOutput();
00469 
00470                 if ( $this->isCached() && !$this->isCacheable() ) {
00471                         $out->addWikiMsg( 'querypage-disabled' );
00472                         return 0;
00473                 }
00474 
00475                 $out->setSyndicated( $this->isSyndicated() );
00476 
00477                 if ( $this->limit == 0 && $this->offset == 0 ) {
00478                         list( $this->limit, $this->offset ) = $this->getRequest()->getLimitOffset();
00479                 }
00480 
00481                 // TODO: Use doQuery()
00482                 if ( !$this->isCached() ) {
00483                         # select one extra row for navigation
00484                         $res = $this->reallyDoQuery( $this->limit + 1, $this->offset );
00485                 } else {
00486                         # Get the cached result, select one extra row for navigation
00487                         $res = $this->fetchFromCache( $this->limit + 1, $this->offset );
00488                         if ( !$this->listoutput ) {
00489 
00490                                 # Fetch the timestamp of this update
00491                                 $ts = $this->getCachedTimestamp();
00492                                 $lang = $this->getLanguage();
00493                                 $maxResults = $lang->formatNum( $wgQueryCacheLimit );
00494 
00495                                 if ( $ts ) {
00496                                         $updated = $lang->userTimeAndDate( $ts, $user );
00497                                         $updateddate = $lang->userDate( $ts, $user );
00498                                         $updatedtime = $lang->userTime( $ts, $user );
00499                                         $out->addMeta( 'Data-Cache-Time', $ts );
00500                                         $out->addJsConfigVars( 'dataCacheTime', $ts );
00501                                         $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
00502                                 } else {
00503                                         $out->addWikiMsg( 'perfcached', $maxResults );
00504                                 }
00505 
00506                                 # If updates on this page have been disabled, let the user know
00507                                 # that the data set won't be refreshed for now
00508                                 if ( is_array( $wgDisableQueryPageUpdate ) && in_array( $this->getName(), $wgDisableQueryPageUpdate ) ) {
00509                                         $out->wrapWikiMsg( "<div class=\"mw-querypage-no-updates\">\n$1\n</div>", 'querypage-no-updates' );
00510                                 }
00511                         }
00512                 }
00513 
00514                 $this->numRows = $res->numRows();
00515 
00516                 $dbr = wfGetDB( DB_SLAVE );
00517                 $this->preprocessResults( $dbr, $res );
00518 
00519                 $out->addHTML( Xml::openElement( 'div', array( 'class' => 'mw-spcontent' ) ) );
00520 
00521                 # Top header and navigation
00522                 if ( $this->shownavigation ) {
00523                         $out->addHTML( $this->getPageHeader() );
00524                         if ( $this->numRows > 0 ) {
00525                                 $out->addHTML( $this->msg( 'showingresults' )->numParams(
00526                                         min( $this->numRows, $this->limit ), # do not show the one extra row, if exist
00527                                         $this->offset + 1 )->parseAsBlock() );
00528                                 # Disable the "next" link when we reach the end
00529                                 $paging = $this->getLanguage()->viewPrevNext( $this->getTitle( $par ), $this->offset,
00530                                         $this->limit, $this->linkParameters(), ( $this->numRows <= $this->limit ) );
00531                                 $out->addHTML( '<p>' . $paging . '</p>' );
00532                         } else {
00533                                 # No results to show, so don't bother with "showing X of Y" etc.
00534                                 # -- just let the user know and give up now
00535                                 $out->addWikiMsg( 'specialpage-empty' );
00536                                 $out->addHTML( Xml::closeElement( 'div' ) );
00537                                 return;
00538                         }
00539                 }
00540 
00541                 # The actual results; specialist subclasses will want to handle this
00542                 # with more than a straight list, so we hand them the info, plus
00543                 # an OutputPage, and let them get on with it
00544                 $this->outputResults( $out,
00545                         $this->getSkin(),
00546                         $dbr, # Should use a ResultWrapper for this
00547                         $res,
00548                         min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
00549                         $this->offset );
00550 
00551                 # Repeat the paging links at the bottom
00552                 if ( $this->shownavigation ) {
00553                         $out->addHTML( '<p>' . $paging . '</p>' );
00554                 }
00555 
00556                 $out->addHTML( Xml::closeElement( 'div' ) );
00557 
00558                 return min( $this->numRows, $this->limit ); # do not return the one extra row, if exist
00559         }
00560 
00572         protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
00573                 global $wgContLang;
00574 
00575                 if ( $num > 0 ) {
00576                         $html = array();
00577                         if ( !$this->listoutput ) {
00578                                 $html[] = $this->openList( $offset );
00579                         }
00580 
00581                         # $res might contain the whole 1,000 rows, so we read up to
00582                         # $num [should update this to use a Pager]
00583                         for ( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) {
00584                                 $line = $this->formatResult( $skin, $row );
00585                                 if ( $line ) {
00586                                         $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
00587                                                 ? ' class="not-patrolled"'
00588                                                 : '';
00589                                         $html[] = $this->listoutput
00590                                                 ? $line
00591                                                 : "<li{$attr}>{$line}</li>\n";
00592                                 }
00593                         }
00594 
00595                         # Flush the final result
00596                         if ( $this->tryLastResult() ) {
00597                                 $row = null;
00598                                 $line = $this->formatResult( $skin, $row );
00599                                 if ( $line ) {
00600                                         $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
00601                                                 ? ' class="not-patrolled"'
00602                                                 : '';
00603                                         $html[] = $this->listoutput
00604                                                 ? $line
00605                                                 : "<li{$attr}>{$line}</li>\n";
00606                                 }
00607                         }
00608 
00609                         if ( !$this->listoutput ) {
00610                                 $html[] = $this->closeList();
00611                         }
00612 
00613                         $html = $this->listoutput
00614                                 ? $wgContLang->listToText( $html )
00615                                 : implode( '', $html );
00616 
00617                         $out->addHTML( $html );
00618                 }
00619         }
00620 
00625         function openList( $offset ) {
00626                 return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
00627         }
00628 
00632         function closeList() {
00633                 return "</ol>\n";
00634         }
00635 
00639         function preprocessResults( $db, $res ) {}
00640 
00645         function doFeed( $class = '', $limit = 50 ) {
00646                 global $wgFeed, $wgFeedClasses;
00647 
00648                 if ( !$wgFeed ) {
00649                         $this->getOutput()->addWikiMsg( 'feed-unavailable' );
00650                         return false;
00651                 }
00652 
00653                 global $wgFeedLimit;
00654                 if ( $limit > $wgFeedLimit ) {
00655                         $limit = $wgFeedLimit;
00656                 }
00657 
00658                 if ( isset( $wgFeedClasses[$class] ) ) {
00659                         $feed = new $wgFeedClasses[$class](
00660                                 $this->feedTitle(),
00661                                 $this->feedDesc(),
00662                                 $this->feedUrl() );
00663                         $feed->outHeader();
00664 
00665                         $res = $this->reallyDoQuery( $limit, 0 );
00666                         foreach ( $res as $obj ) {
00667                                 $item = $this->feedResult( $obj );
00668                                 if ( $item ) {
00669                                         $feed->outItem( $item );
00670                                 }
00671                         }
00672 
00673                         $feed->outFooter();
00674                         return true;
00675                 } else {
00676                         return false;
00677                 }
00678         }
00679 
00685         function feedResult( $row ) {
00686                 if ( !isset( $row->title ) ) {
00687                         return null;
00688                 }
00689                 $title = Title::makeTitle( intval( $row->namespace ), $row->title );
00690                 if ( $title ) {
00691                         $date = isset( $row->timestamp ) ? $row->timestamp : '';
00692                         $comments = '';
00693                         if ( $title ) {
00694                                 $talkpage = $title->getTalkPage();
00695                                 $comments = $talkpage->getFullURL();
00696                         }
00697 
00698                         return new FeedItem(
00699                                 $title->getPrefixedText(),
00700                                 $this->feedItemDesc( $row ),
00701                                 $title->getFullURL(),
00702                                 $date,
00703                                 $this->feedItemAuthor( $row ),
00704                                 $comments );
00705                 } else {
00706                         return null;
00707                 }
00708         }
00709 
00710         function feedItemDesc( $row ) {
00711                 return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : '';
00712         }
00713 
00714         function feedItemAuthor( $row ) {
00715                 return isset( $row->user_text ) ? $row->user_text : '';
00716         }
00717 
00718         function feedTitle() {
00719                 global $wgLanguageCode, $wgSitename;
00720                 $desc = $this->getDescription();
00721                 return "$wgSitename - $desc [$wgLanguageCode]";
00722         }
00723 
00724         function feedDesc() {
00725                 return $this->msg( 'tagline' )->text();
00726         }
00727 
00728         function feedUrl() {
00729                 return $this->getTitle()->getFullURL();
00730         }
00731 }
00732 
00737 abstract class WantedQueryPage extends QueryPage {
00738 
00739         function isExpensive() {
00740                 return true;
00741         }
00742 
00743         function isSyndicated() {
00744                 return false;
00745         }
00746 
00750         function preprocessResults( $db, $res ) {
00751                 if ( !$res->numRows() ) {
00752                         return;
00753                 }
00754 
00755                 $batch = new LinkBatch;
00756                 foreach ( $res as $row ) {
00757                         $batch->add( $row->namespace, $row->title );
00758                 }
00759                 $batch->execute();
00760 
00761                 // Back to start for display
00762                 $res->seek( 0 );
00763         }
00764 
00773         function forceExistenceCheck() {
00774                 return false;
00775         }
00776 
00784         public function formatResult( $skin, $result ) {
00785                 $title = Title::makeTitleSafe( $result->namespace, $result->title );
00786                 if ( $title instanceof Title ) {
00787                         if ( $this->isCached() || $this->forceExistenceCheck() ) {
00788                                 $pageLink = $title->isKnown()
00789                                         ? '<del>' . Linker::link( $title ) . '</del>'
00790                                         : Linker::link(
00791                                                 $title,
00792                                                 null,
00793                                                 array(),
00794                                                 array(),
00795                                                 array( 'broken' )
00796                                         );
00797                         } else {
00798                                 $pageLink = Linker::link(
00799                                         $title,
00800                                         null,
00801                                         array(),
00802                                         array(),
00803                                         array( 'broken' )
00804                                 );
00805                         }
00806                         return $this->getLanguage()->specialList( $pageLink, $this->makeWlhLink( $title, $result ) );
00807                 } else {
00808                         return $this->msg( 'wantedpages-badtitle', $result->title )->escaped();
00809                 }
00810         }
00811 
00819         private function makeWlhLink( $title, $result ) {
00820                 $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
00821                 $label = $this->msg( 'nlinks' )->numParams( $result->value )->escaped();
00822                 return Linker::link( $wlh, $label );
00823         }
00824 }