MediaWiki  REL1_22
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( 'DoubleRedirectsPage',           'DoubleRedirects'               ),
00039     array( 'FileDuplicateSearchPage',       'FileDuplicateSearch'           ),
00040     array( 'LinkSearchPage',                'LinkSearch'                    ),
00041     array( 'ListredirectsPage',             'Listredirects'                 ),
00042     array( 'LonelyPagesPage',               'Lonelypages'                   ),
00043     array( 'LongPagesPage',                 'Longpages'                     ),
00044     array( 'MIMEsearchPage',                'MIMEsearch'                    ),
00045     array( 'MostcategoriesPage',            'Mostcategories'                ),
00046     array( 'MostimagesPage',                'Mostimages'                    ),
00047     array( 'MostinterwikisPage',            'Mostinterwikis'                ),
00048     array( 'MostlinkedCategoriesPage',      'Mostlinkedcategories'          ),
00049     array( 'MostlinkedtemplatesPage',       'Mostlinkedtemplates'           ),
00050     array( 'MostlinkedPage',                'Mostlinked'                    ),
00051     array( 'MostrevisionsPage',             'Mostrevisions'                 ),
00052     array( 'FewestrevisionsPage',           'Fewestrevisions'               ),
00053     array( 'ShortPagesPage',                'Shortpages'                    ),
00054     array( 'UncategorizedCategoriesPage',   'Uncategorizedcategories'       ),
00055     array( 'UncategorizedPagesPage',        'Uncategorizedpages'            ),
00056     array( 'UncategorizedImagesPage',       'Uncategorizedimages'           ),
00057     array( 'UncategorizedTemplatesPage',    'Uncategorizedtemplates'        ),
00058     array( 'UnusedCategoriesPage',          'Unusedcategories'              ),
00059     array( 'UnusedimagesPage',              'Unusedimages'                  ),
00060     array( 'WantedCategoriesPage',          'Wantedcategories'              ),
00061     array( 'WantedFilesPage',               'Wantedfiles'                   ),
00062     array( 'WantedPagesPage',               'Wantedpages'                   ),
00063     array( 'WantedTemplatesPage',           'Wantedtemplates'               ),
00064     array( 'UnwatchedPagesPage',            'Unwatchedpages'                ),
00065     array( 'UnusedtemplatesPage',           'Unusedtemplates'               ),
00066     array( 'WithoutInterwikiPage',          'Withoutinterwiki'              ),
00067 );
00068 wfRunHooks( 'wgQueryPages', array( &$wgQueryPages ) );
00069 
00070 global $wgDisableCounters;
00071 if ( !$wgDisableCounters ) {
00072     $wgQueryPages[] = array( 'PopularPagesPage', 'Popularpages' );
00073 }
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 
00248     abstract function formatResult( $skin, $result );
00249 
00255     function getPageHeader() {
00256         return '';
00257     }
00258 
00266     function linkParameters() {
00267         return array();
00268     }
00269 
00278     function tryLastResult() {
00279         return false;
00280     }
00281 
00290     function recache( $limit, $ignoreErrors = true ) {
00291         if ( !$this->isCacheable() ) {
00292             return 0;
00293         }
00294 
00295         $fname = get_class( $this ) . '::recache';
00296         $dbw = wfGetDB( DB_MASTER );
00297         $dbr = wfGetDB( DB_SLAVE, array( $this->getName(), __METHOD__, 'vslow' ) );
00298         if ( !$dbw || !$dbr ) {
00299             return false;
00300         }
00301 
00302         try {
00303             # Clear out any old cached data
00304             $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname );
00305             # Do query
00306             $res = $this->reallyDoQuery( $limit, false );
00307             $num = false;
00308             if ( $res ) {
00309                 $num = $res->numRows();
00310                 # Fetch results
00311                 $vals = array();
00312                 while ( $res && $row = $dbr->fetchObject( $res ) ) {
00313                     if ( isset( $row->value ) ) {
00314                         if ( $this->usesTimestamps() ) {
00315                             $value = wfTimestamp( TS_UNIX,
00316                                 $row->value );
00317                         } else {
00318                             $value = intval( $row->value ); // @bug 14414
00319                         }
00320                     } else {
00321                         $value = 0;
00322                     }
00323 
00324                     $vals[] = array( 'qc_type' => $this->getName(),
00325                             'qc_namespace' => $row->namespace,
00326                             'qc_title' => $row->title,
00327                             'qc_value' => $value );
00328                 }
00329 
00330                 # Save results into the querycache table on the master
00331                 if ( count( $vals ) ) {
00332                     $dbw->insert( 'querycache', $vals, __METHOD__ );
00333                 }
00334                 # Update the querycache_info record for the page
00335                 $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname );
00336                 $dbw->insert( 'querycache_info',
00337                     array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ),
00338                     $fname );
00339             }
00340         } catch ( DBError $e ) {
00341             if ( !$ignoreErrors ) {
00342                 throw $e; // report query error
00343             }
00344             $num = false; // set result to false to indicate error
00345         }
00346 
00347         return $num;
00348     }
00349 
00357     function reallyDoQuery( $limit, $offset = false ) {
00358         $fname = get_class( $this ) . "::reallyDoQuery";
00359         $dbr = wfGetDB( DB_SLAVE );
00360         $query = $this->getQueryInfo();
00361         $order = $this->getOrderFields();
00362 
00363         if ( $this->sortDescending() ) {
00364             foreach ( $order as &$field ) {
00365                 $field .= ' DESC';
00366             }
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 
00376             if ( count( $order ) ) {
00377                 $options['ORDER BY'] = $order;
00378             }
00379 
00380             if ( $limit !== false ) {
00381                 $options['LIMIT'] = intval( $limit );
00382             }
00383 
00384             if ( $offset !== false ) {
00385                 $options['OFFSET'] = intval( $offset );
00386             }
00387 
00388             $res = $dbr->select( $tables, $fields, $conds, $fname,
00389                     $options, $join_conds
00390             );
00391         } else {
00392             // Old-fashioned raw SQL style, deprecated
00393             $sql = $this->getSQL();
00394             $sql .= ' ORDER BY ' . implode( ', ', $order );
00395             $sql = $dbr->limitResult( $sql, $limit, $offset );
00396             $res = $dbr->query( $sql, $fname );
00397         }
00398 
00399         return $dbr->resultObject( $res );
00400     }
00401 
00408     function doQuery( $offset = false, $limit = false ) {
00409         if ( $this->isCached() && $this->isCacheable() ) {
00410             return $this->fetchFromCache( $limit, $offset );
00411         } else {
00412             return $this->reallyDoQuery( $limit, $offset );
00413         }
00414     }
00415 
00423     function fetchFromCache( $limit, $offset = false ) {
00424         $dbr = wfGetDB( DB_SLAVE );
00425         $options = array();
00426         if ( $limit !== false ) {
00427             $options['LIMIT'] = intval( $limit );
00428         }
00429         if ( $offset !== false ) {
00430             $options['OFFSET'] = intval( $offset );
00431         }
00432         if ( $this->sortDescending() ) {
00433             $options['ORDER BY'] = 'qc_value DESC';
00434         } else {
00435             $options['ORDER BY'] = 'qc_value ASC';
00436         }
00437         $res = $dbr->select( 'querycache', array( 'qc_type',
00438                 'namespace' => 'qc_namespace',
00439                 'title' => 'qc_title',
00440                 'value' => 'qc_value' ),
00441                 array( 'qc_type' => $this->getName() ),
00442                 __METHOD__, $options
00443         );
00444         return $dbr->resultObject( $res );
00445     }
00446 
00447     public function getCachedTimestamp() {
00448         if ( is_null( $this->cachedTimestamp ) ) {
00449             $dbr = wfGetDB( DB_SLAVE );
00450             $fname = get_class( $this ) . '::getCachedTimestamp';
00451             $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
00452                 array( 'qci_type' => $this->getName() ), $fname );
00453         }
00454         return $this->cachedTimestamp;
00455     }
00456 
00463     function execute( $par ) {
00464         global $wgQueryCacheLimit, $wgDisableQueryPageUpdate;
00465 
00466         $user = $this->getUser();
00467         if ( !$this->userCanExecute( $user ) ) {
00468             $this->displayRestrictionError();
00469             return;
00470         }
00471 
00472         $this->setHeaders();
00473         $this->outputHeader();
00474 
00475         $out = $this->getOutput();
00476 
00477         if ( $this->isCached() && !$this->isCacheable() ) {
00478             $out->addWikiMsg( 'querypage-disabled' );
00479             return 0;
00480         }
00481 
00482         $out->setSyndicated( $this->isSyndicated() );
00483 
00484         if ( $this->limit == 0 && $this->offset == 0 ) {
00485             list( $this->limit, $this->offset ) = $this->getRequest()->getLimitOffset();
00486         }
00487 
00488         // TODO: Use doQuery()
00489         if ( !$this->isCached() ) {
00490             # select one extra row for navigation
00491             $res = $this->reallyDoQuery( $this->limit + 1, $this->offset );
00492         } else {
00493             # Get the cached result, select one extra row for navigation
00494             $res = $this->fetchFromCache( $this->limit + 1, $this->offset );
00495             if ( !$this->listoutput ) {
00496 
00497                 # Fetch the timestamp of this update
00498                 $ts = $this->getCachedTimestamp();
00499                 $lang = $this->getLanguage();
00500                 $maxResults = $lang->formatNum( $wgQueryCacheLimit );
00501 
00502                 if ( $ts ) {
00503                     $updated = $lang->userTimeAndDate( $ts, $user );
00504                     $updateddate = $lang->userDate( $ts, $user );
00505                     $updatedtime = $lang->userTime( $ts, $user );
00506                     $out->addMeta( 'Data-Cache-Time', $ts );
00507                     $out->addJsConfigVars( 'dataCacheTime', $ts );
00508                     $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
00509                 } else {
00510                     $out->addWikiMsg( 'perfcached', $maxResults );
00511                 }
00512 
00513                 # If updates on this page have been disabled, let the user know
00514                 # that the data set won't be refreshed for now
00515                 if ( is_array( $wgDisableQueryPageUpdate ) && in_array( $this->getName(), $wgDisableQueryPageUpdate ) ) {
00516                     $out->wrapWikiMsg( "<div class=\"mw-querypage-no-updates\">\n$1\n</div>", 'querypage-no-updates' );
00517                 }
00518             }
00519         }
00520 
00521         $this->numRows = $res->numRows();
00522 
00523         $dbr = wfGetDB( DB_SLAVE );
00524         $this->preprocessResults( $dbr, $res );
00525 
00526         $out->addHTML( Xml::openElement( 'div', array( 'class' => 'mw-spcontent' ) ) );
00527 
00528         # Top header and navigation
00529         if ( $this->shownavigation ) {
00530             $out->addHTML( $this->getPageHeader() );
00531             if ( $this->numRows > 0 ) {
00532                 $out->addHTML( $this->msg( 'showingresults' )->numParams(
00533                     min( $this->numRows, $this->limit ), # do not show the one extra row, if exist
00534                     $this->offset + 1 )->parseAsBlock() );
00535                 # Disable the "next" link when we reach the end
00536                 $paging = $this->getLanguage()->viewPrevNext( $this->getTitle( $par ), $this->offset,
00537                     $this->limit, $this->linkParameters(), ( $this->numRows <= $this->limit ) );
00538                 $out->addHTML( '<p>' . $paging . '</p>' );
00539             } else {
00540                 # No results to show, so don't bother with "showing X of Y" etc.
00541                 # -- just let the user know and give up now
00542                 $out->addWikiMsg( 'specialpage-empty' );
00543                 $out->addHTML( Xml::closeElement( 'div' ) );
00544                 return;
00545             }
00546         }
00547 
00548         # The actual results; specialist subclasses will want to handle this
00549         # with more than a straight list, so we hand them the info, plus
00550         # an OutputPage, and let them get on with it
00551         $this->outputResults( $out,
00552             $this->getSkin(),
00553             $dbr, # Should use a ResultWrapper for this
00554             $res,
00555             min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
00556             $this->offset );
00557 
00558         # Repeat the paging links at the bottom
00559         if ( $this->shownavigation ) {
00560             $out->addHTML( '<p>' . $paging . '</p>' );
00561         }
00562 
00563         $out->addHTML( Xml::closeElement( 'div' ) );
00564 
00565         return min( $this->numRows, $this->limit ); # do not return the one extra row, if exist
00566     }
00567 
00579     protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
00580         global $wgContLang;
00581 
00582         if ( $num > 0 ) {
00583             $html = array();
00584             if ( !$this->listoutput ) {
00585                 $html[] = $this->openList( $offset );
00586             }
00587 
00588             # $res might contain the whole 1,000 rows, so we read up to
00589             # $num [should update this to use a Pager]
00590             for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
00591                 $line = $this->formatResult( $skin, $row );
00592                 if ( $line ) {
00593                     $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
00594                         ? ' class="not-patrolled"'
00595                         : '';
00596                     $html[] = $this->listoutput
00597                         ? $line
00598                         : "<li{$attr}>{$line}</li>\n";
00599                 }
00600             }
00601 
00602             # Flush the final result
00603             if ( $this->tryLastResult() ) {
00604                 $row = null;
00605                 $line = $this->formatResult( $skin, $row );
00606                 if ( $line ) {
00607                     $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
00608                         ? ' class="not-patrolled"'
00609                         : '';
00610                     $html[] = $this->listoutput
00611                         ? $line
00612                         : "<li{$attr}>{$line}</li>\n";
00613                 }
00614             }
00615 
00616             if ( !$this->listoutput ) {
00617                 $html[] = $this->closeList();
00618             }
00619 
00620             $html = $this->listoutput
00621                 ? $wgContLang->listToText( $html )
00622                 : implode( '', $html );
00623 
00624             $out->addHTML( $html );
00625         }
00626     }
00627 
00632     function openList( $offset ) {
00633         return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
00634     }
00635 
00639     function closeList() {
00640         return "</ol>\n";
00641     }
00642 
00648     function preprocessResults( $db, $res ) {}
00649 
00656     function doFeed( $class = '', $limit = 50 ) {
00657         global $wgFeed, $wgFeedClasses, $wgFeedLimit;
00658 
00659         if ( !$wgFeed ) {
00660             $this->getOutput()->addWikiMsg( 'feed-unavailable' );
00661             return false;
00662         }
00663 
00664         $limit = min( $limit, $wgFeedLimit );
00665 
00666         if ( isset( $wgFeedClasses[$class] ) ) {
00667             $feed = new $wgFeedClasses[$class](
00668                 $this->feedTitle(),
00669                 $this->feedDesc(),
00670                 $this->feedUrl() );
00671             $feed->outHeader();
00672 
00673             $res = $this->reallyDoQuery( $limit, 0 );
00674             foreach ( $res as $obj ) {
00675                 $item = $this->feedResult( $obj );
00676                 if ( $item ) {
00677                     $feed->outItem( $item );
00678                 }
00679             }
00680 
00681             $feed->outFooter();
00682             return true;
00683         } else {
00684             return false;
00685         }
00686     }
00687 
00694     function feedResult( $row ) {
00695         if ( !isset( $row->title ) ) {
00696             return null;
00697         }
00698         $title = Title::makeTitle( intval( $row->namespace ), $row->title );
00699         if ( $title ) {
00700             $date = isset( $row->timestamp ) ? $row->timestamp : '';
00701             $comments = '';
00702             if ( $title ) {
00703                 $talkpage = $title->getTalkPage();
00704                 $comments = $talkpage->getFullURL();
00705             }
00706 
00707             return new FeedItem(
00708                 $title->getPrefixedText(),
00709                 $this->feedItemDesc( $row ),
00710                 $title->getFullURL(),
00711                 $date,
00712                 $this->feedItemAuthor( $row ),
00713                 $comments );
00714         } else {
00715             return null;
00716         }
00717     }
00718 
00719     function feedItemDesc( $row ) {
00720         return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : '';
00721     }
00722 
00723     function feedItemAuthor( $row ) {
00724         return isset( $row->user_text ) ? $row->user_text : '';
00725     }
00726 
00727     function feedTitle() {
00728         global $wgLanguageCode, $wgSitename;
00729         $desc = $this->getDescription();
00730         return "$wgSitename - $desc [$wgLanguageCode]";
00731     }
00732 
00733     function feedDesc() {
00734         return $this->msg( 'tagline' )->text();
00735     }
00736 
00737     function feedUrl() {
00738         return $this->getTitle()->getFullURL();
00739     }
00740 }
00741 
00746 abstract class WantedQueryPage extends QueryPage {
00747     function isExpensive() {
00748         return true;
00749     }
00750 
00751     function isSyndicated() {
00752         return false;
00753     }
00754 
00760     function preprocessResults( $db, $res ) {
00761         if ( !$res->numRows() ) {
00762             return;
00763         }
00764 
00765         $batch = new LinkBatch;
00766         foreach ( $res as $row ) {
00767             $batch->add( $row->namespace, $row->title );
00768         }
00769         $batch->execute();
00770 
00771         // Back to start for display
00772         $res->seek( 0 );
00773     }
00774 
00783     function forceExistenceCheck() {
00784         return false;
00785     }
00786 
00794     public function formatResult( $skin, $result ) {
00795         $title = Title::makeTitleSafe( $result->namespace, $result->title );
00796         if ( $title instanceof Title ) {
00797             if ( $this->isCached() || $this->forceExistenceCheck() ) {
00798                 $pageLink = $title->isKnown()
00799                     ? '<del>' . Linker::link( $title ) . '</del>'
00800                     : Linker::link(
00801                         $title,
00802                         null,
00803                         array(),
00804                         array(),
00805                         array( 'broken' )
00806                     );
00807             } else {
00808                 $pageLink = Linker::link(
00809                     $title,
00810                     null,
00811                     array(),
00812                     array(),
00813                     array( 'broken' )
00814                 );
00815             }
00816             return $this->getLanguage()->specialList( $pageLink, $this->makeWlhLink( $title, $result ) );
00817         } else {
00818             return $this->msg( 'wantedpages-badtitle', $result->title )->escaped();
00819         }
00820     }
00821 
00829     private function makeWlhLink( $title, $result ) {
00830         $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
00831         $label = $this->msg( 'nlinks' )->numParams( $result->value )->escaped();
00832         return Linker::link( $wlh, $label );
00833     }
00834 }