MediaWiki  REL1_24
QueryPage.php
Go to the documentation of this file.
00001 <?php
00030 abstract class QueryPage extends SpecialPage {
00032     protected $listoutput = false;
00033 
00035     protected $offset = 0;
00036 
00038     protected $limit = 0;
00039 
00045     protected $numRows;
00046 
00047     protected $cachedTimestamp = null;
00048 
00052     protected $shownavigation = true;
00053 
00062     public static function getPages() {
00063         global $wgDisableCounters;
00064         static $qp = null;
00065 
00066         if ( $qp === null ) {
00067             // QueryPage subclass, Special page name
00068             $qp = array(
00069                 array( 'AncientPagesPage', 'Ancientpages' ),
00070                 array( 'BrokenRedirectsPage', 'BrokenRedirects' ),
00071                 array( 'DeadendPagesPage', 'Deadendpages' ),
00072                 array( 'DoubleRedirectsPage', 'DoubleRedirects' ),
00073                 array( 'FileDuplicateSearchPage', 'FileDuplicateSearch' ),
00074                 array( 'ListDuplicatedFilesPage', 'ListDuplicatedFiles'),
00075                 array( 'LinkSearchPage', 'LinkSearch' ),
00076                 array( 'ListredirectsPage', 'Listredirects' ),
00077                 array( 'LonelyPagesPage', 'Lonelypages' ),
00078                 array( 'LongPagesPage', 'Longpages' ),
00079                 array( 'MediaStatisticsPage', 'MediaStatistics' ),
00080                 array( 'MIMEsearchPage', 'MIMEsearch' ),
00081                 array( 'MostcategoriesPage', 'Mostcategories' ),
00082                 array( 'MostimagesPage', 'Mostimages' ),
00083                 array( 'MostinterwikisPage', 'Mostinterwikis' ),
00084                 array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ),
00085                 array( 'MostlinkedtemplatesPage', 'Mostlinkedtemplates' ),
00086                 array( 'MostlinkedPage', 'Mostlinked' ),
00087                 array( 'MostrevisionsPage', 'Mostrevisions' ),
00088                 array( 'FewestrevisionsPage', 'Fewestrevisions' ),
00089                 array( 'ShortPagesPage', 'Shortpages' ),
00090                 array( 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ),
00091                 array( 'UncategorizedPagesPage', 'Uncategorizedpages' ),
00092                 array( 'UncategorizedImagesPage', 'Uncategorizedimages' ),
00093                 array( 'UncategorizedTemplatesPage', 'Uncategorizedtemplates' ),
00094                 array( 'UnusedCategoriesPage', 'Unusedcategories' ),
00095                 array( 'UnusedimagesPage', 'Unusedimages' ),
00096                 array( 'WantedCategoriesPage', 'Wantedcategories' ),
00097                 array( 'WantedFilesPage', 'Wantedfiles' ),
00098                 array( 'WantedPagesPage', 'Wantedpages' ),
00099                 array( 'WantedTemplatesPage', 'Wantedtemplates' ),
00100                 array( 'UnwatchedPagesPage', 'Unwatchedpages' ),
00101                 array( 'UnusedtemplatesPage', 'Unusedtemplates' ),
00102                 array( 'WithoutInterwikiPage', 'Withoutinterwiki' ),
00103             );
00104             wfRunHooks( 'wgQueryPages', array( &$qp ) );
00105 
00106             if ( !$wgDisableCounters ) {
00107                 $qp[] = array( 'PopularPagesPage', 'Popularpages' );
00108             }
00109         }
00110 
00111         return $qp;
00112     }
00113 
00119     function setListoutput( $bool ) {
00120         $this->listoutput = $bool;
00121     }
00122 
00149     function getQueryInfo() {
00150         return null;
00151     }
00152 
00159     function getSQL() {
00160         /* Implement getQueryInfo() instead */
00161         throw new MWException( "Bug in a QueryPage: doesn't implement getQueryInfo() nor "
00162             . "getQuery() properly" );
00163     }
00164 
00172     function getOrderFields() {
00173         return array( 'value' );
00174     }
00175 
00186     function usesTimestamps() {
00187         return false;
00188     }
00189 
00195     function sortDescending() {
00196         return true;
00197     }
00198 
00206     function isExpensive() {
00207         return $this->getConfig()->get( 'DisableQueryPages' );
00208     }
00209 
00217     public function isCacheable() {
00218         return true;
00219     }
00220 
00227     function isCached() {
00228         return $this->isExpensive() && $this->getConfig()->get( 'MiserMode' );
00229     }
00230 
00236     function isSyndicated() {
00237         return true;
00238     }
00239 
00249     abstract function formatResult( $skin, $result );
00250 
00256     function getPageHeader() {
00257         return '';
00258     }
00259 
00267     function linkParameters() {
00268         return array();
00269     }
00270 
00279     function tryLastResult() {
00280         return false;
00281     }
00282 
00291     function recache( $limit, $ignoreErrors = true ) {
00292         if ( !$this->isCacheable() ) {
00293             return 0;
00294         }
00295 
00296         $fname = get_class( $this ) . '::recache';
00297         $dbw = wfGetDB( DB_MASTER );
00298         if ( !$dbw ) {
00299             return false;
00300         }
00301 
00302         try {
00303             # Do query
00304             $res = $this->reallyDoQuery( $limit, false );
00305             $num = false;
00306             if ( $res ) {
00307                 $num = $res->numRows();
00308                 # Fetch results
00309                 $vals = array();
00310                 foreach ( $res as $row ) {
00311                     if ( isset( $row->value ) ) {
00312                         if ( $this->usesTimestamps() ) {
00313                             $value = wfTimestamp( TS_UNIX,
00314                                 $row->value );
00315                         } else {
00316                             $value = intval( $row->value ); // @bug 14414
00317                         }
00318                     } else {
00319                         $value = 0;
00320                     }
00321 
00322                     $vals[] = array( 'qc_type' => $this->getName(),
00323                             'qc_namespace' => $row->namespace,
00324                             'qc_title' => $row->title,
00325                             'qc_value' => $value );
00326                 }
00327 
00328                 $dbw->startAtomic( __METHOD__ );
00329                 # Clear out any old cached data
00330                 $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname );
00331                 # Save results into the querycache table on the master
00332                 if ( count( $vals ) ) {
00333                     $dbw->insert( 'querycache', $vals, __METHOD__ );
00334                 }
00335                 # Update the querycache_info record for the page
00336                 $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname );
00337                 $dbw->insert( 'querycache_info',
00338                     array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ),
00339                     $fname );
00340                 $dbw->endAtomic( __METHOD__ );
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 
00356     function getRecacheDB() {
00357         return wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) );
00358     }
00359 
00367     function reallyDoQuery( $limit, $offset = false ) {
00368         $fname = get_class( $this ) . "::reallyDoQuery";
00369         $dbr = $this->getRecacheDB();
00370         $query = $this->getQueryInfo();
00371         $order = $this->getOrderFields();
00372 
00373         if ( $this->sortDescending() ) {
00374             foreach ( $order as &$field ) {
00375                 $field .= ' DESC';
00376             }
00377         }
00378 
00379         if ( is_array( $query ) ) {
00380             $tables = isset( $query['tables'] ) ? (array)$query['tables'] : array();
00381             $fields = isset( $query['fields'] ) ? (array)$query['fields'] : array();
00382             $conds = isset( $query['conds'] ) ? (array)$query['conds'] : array();
00383             $options = isset( $query['options'] ) ? (array)$query['options'] : array();
00384             $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : array();
00385 
00386             if ( count( $order ) ) {
00387                 $options['ORDER BY'] = $order;
00388             }
00389 
00390             if ( $limit !== false ) {
00391                 $options['LIMIT'] = intval( $limit );
00392             }
00393 
00394             if ( $offset !== false ) {
00395                 $options['OFFSET'] = intval( $offset );
00396             }
00397 
00398             $res = $dbr->select( $tables, $fields, $conds, $fname,
00399                     $options, $join_conds
00400             );
00401         } else {
00402             // Old-fashioned raw SQL style, deprecated
00403             $sql = $this->getSQL();
00404             $sql .= ' ORDER BY ' . implode( ', ', $order );
00405             $sql = $dbr->limitResult( $sql, $limit, $offset );
00406             $res = $dbr->query( $sql, $fname );
00407         }
00408 
00409         return $res;
00410     }
00411 
00418     function doQuery( $offset = false, $limit = false ) {
00419         if ( $this->isCached() && $this->isCacheable() ) {
00420             return $this->fetchFromCache( $limit, $offset );
00421         } else {
00422             return $this->reallyDoQuery( $limit, $offset );
00423         }
00424     }
00425 
00433     function fetchFromCache( $limit, $offset = false ) {
00434         $dbr = wfGetDB( DB_SLAVE );
00435         $options = array();
00436         if ( $limit !== false ) {
00437             $options['LIMIT'] = intval( $limit );
00438         }
00439         if ( $offset !== false ) {
00440             $options['OFFSET'] = intval( $offset );
00441         }
00442         if ( $this->sortDescending() ) {
00443             $options['ORDER BY'] = 'qc_value DESC';
00444         } else {
00445             $options['ORDER BY'] = 'qc_value ASC';
00446         }
00447         $res = $dbr->select( 'querycache', array( 'qc_type',
00448                 'namespace' => 'qc_namespace',
00449                 'title' => 'qc_title',
00450                 'value' => 'qc_value' ),
00451                 array( 'qc_type' => $this->getName() ),
00452                 __METHOD__, $options
00453         );
00454         return $dbr->resultObject( $res );
00455     }
00456 
00457     public function getCachedTimestamp() {
00458         if ( is_null( $this->cachedTimestamp ) ) {
00459             $dbr = wfGetDB( DB_SLAVE );
00460             $fname = get_class( $this ) . '::getCachedTimestamp';
00461             $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
00462                 array( 'qci_type' => $this->getName() ), $fname );
00463         }
00464         return $this->cachedTimestamp;
00465     }
00466 
00472     function execute( $par ) {
00473         $user = $this->getUser();
00474         if ( !$this->userCanExecute( $user ) ) {
00475             $this->displayRestrictionError();
00476             return;
00477         }
00478 
00479         $this->setHeaders();
00480         $this->outputHeader();
00481 
00482         $out = $this->getOutput();
00483 
00484         if ( $this->isCached() && !$this->isCacheable() ) {
00485             $out->addWikiMsg( 'querypage-disabled' );
00486             return;
00487         }
00488 
00489         $out->setSyndicated( $this->isSyndicated() );
00490 
00491         if ( $this->limit == 0 && $this->offset == 0 ) {
00492             list( $this->limit, $this->offset ) = $this->getRequest()->getLimitOffset();
00493         }
00494 
00495         // @todo Use doQuery()
00496         if ( !$this->isCached() ) {
00497             # select one extra row for navigation
00498             $res = $this->reallyDoQuery( $this->limit + 1, $this->offset );
00499         } else {
00500             # Get the cached result, select one extra row for navigation
00501             $res = $this->fetchFromCache( $this->limit + 1, $this->offset );
00502             if ( !$this->listoutput ) {
00503 
00504                 # Fetch the timestamp of this update
00505                 $ts = $this->getCachedTimestamp();
00506                 $lang = $this->getLanguage();
00507                 $maxResults = $lang->formatNum( $this->getConfig()->get( 'QueryCacheLimit' ) );
00508 
00509                 if ( $ts ) {
00510                     $updated = $lang->userTimeAndDate( $ts, $user );
00511                     $updateddate = $lang->userDate( $ts, $user );
00512                     $updatedtime = $lang->userTime( $ts, $user );
00513                     $out->addMeta( 'Data-Cache-Time', $ts );
00514                     $out->addJsConfigVars( 'dataCacheTime', $ts );
00515                     $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
00516                 } else {
00517                     $out->addWikiMsg( 'perfcached', $maxResults );
00518                 }
00519 
00520                 # If updates on this page have been disabled, let the user know
00521                 # that the data set won't be refreshed for now
00522                 if ( is_array( $this->getConfig()->get( 'DisableQueryPageUpdate' ) )
00523                     && in_array( $this->getName(), $this->getConfig()->get( 'DisableQueryPageUpdate' ) )
00524                 ) {
00525                     $out->wrapWikiMsg(
00526                         "<div class=\"mw-querypage-no-updates\">\n$1\n</div>",
00527                         'querypage-no-updates'
00528                     );
00529                 }
00530             }
00531         }
00532 
00533         $this->numRows = $res->numRows();
00534 
00535         $dbr = wfGetDB( DB_SLAVE );
00536         $this->preprocessResults( $dbr, $res );
00537 
00538         $out->addHTML( Xml::openElement( 'div', array( 'class' => 'mw-spcontent' ) ) );
00539 
00540         # Top header and navigation
00541         if ( $this->shownavigation ) {
00542             $out->addHTML( $this->getPageHeader() );
00543             if ( $this->numRows > 0 ) {
00544                 $out->addHTML( $this->msg( 'showingresultsinrange' )->numParams(
00545                     min( $this->numRows, $this->limit ), # do not show the one extra row, if exist
00546                     $this->offset + 1, ( min( $this->numRows, $this->limit ) + $this->offset ) )->parseAsBlock() );
00547                 # Disable the "next" link when we reach the end
00548                 $paging = $this->getLanguage()->viewPrevNext( $this->getPageTitle( $par ), $this->offset,
00549                     $this->limit, $this->linkParameters(), ( $this->numRows <= $this->limit ) );
00550                 $out->addHTML( '<p>' . $paging . '</p>' );
00551             } else {
00552                 # No results to show, so don't bother with "showing X of Y" etc.
00553                 # -- just let the user know and give up now
00554                 $out->addWikiMsg( 'specialpage-empty' );
00555                 $out->addHTML( Xml::closeElement( 'div' ) );
00556                 return;
00557             }
00558         }
00559 
00560         # The actual results; specialist subclasses will want to handle this
00561         # with more than a straight list, so we hand them the info, plus
00562         # an OutputPage, and let them get on with it
00563         $this->outputResults( $out,
00564             $this->getSkin(),
00565             $dbr, # Should use a ResultWrapper for this
00566             $res,
00567             min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
00568             $this->offset );
00569 
00570         # Repeat the paging links at the bottom
00571         if ( $this->shownavigation ) {
00572             $out->addHTML( '<p>' . $paging . '</p>' );
00573         }
00574 
00575         $out->addHTML( Xml::closeElement( 'div' ) );
00576     }
00577 
00589     protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
00590         global $wgContLang;
00591 
00592         if ( $num > 0 ) {
00593             $html = array();
00594             if ( !$this->listoutput ) {
00595                 $html[] = $this->openList( $offset );
00596             }
00597 
00598             # $res might contain the whole 1,000 rows, so we read up to
00599             # $num [should update this to use a Pager]
00600             // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
00601             for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
00602                 // @codingStandardsIgnoreEnd
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             # Flush the final result
00615             if ( $this->tryLastResult() ) {
00616                 $row = null;
00617                 $line = $this->formatResult( $skin, $row );
00618                 if ( $line ) {
00619                     $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
00620                         ? ' class="not-patrolled"'
00621                         : '';
00622                     $html[] = $this->listoutput
00623                         ? $line
00624                         : "<li{$attr}>{$line}</li>\n";
00625                 }
00626             }
00627 
00628             if ( !$this->listoutput ) {
00629                 $html[] = $this->closeList();
00630             }
00631 
00632             $html = $this->listoutput
00633                 ? $wgContLang->listToText( $html )
00634                 : implode( '', $html );
00635 
00636             $out->addHTML( $html );
00637         }
00638     }
00639 
00644     function openList( $offset ) {
00645         return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
00646     }
00647 
00651     function closeList() {
00652         return "</ol>\n";
00653     }
00654 
00660     function preprocessResults( $db, $res ) {
00661     }
00662 
00669     function doFeed( $class = '', $limit = 50 ) {
00670         if ( !$this->getConfig()->get( 'Feed' ) ) {
00671             $this->getOutput()->addWikiMsg( 'feed-unavailable' );
00672             return false;
00673         }
00674 
00675         $limit = min( $limit, $this->getConfig()->get( 'FeedLimit' ) );
00676 
00677         $feedClasses = $this->getConfig()->get( 'FeedClasses' );
00678         if ( isset( $feedClasses[$class] ) ) {
00680             $feed = new $feedClasses[$class](
00681                 $this->feedTitle(),
00682                 $this->feedDesc(),
00683                 $this->feedUrl() );
00684             $feed->outHeader();
00685 
00686             $res = $this->reallyDoQuery( $limit, 0 );
00687             foreach ( $res as $obj ) {
00688                 $item = $this->feedResult( $obj );
00689                 if ( $item ) {
00690                     $feed->outItem( $item );
00691                 }
00692             }
00693 
00694             $feed->outFooter();
00695             return true;
00696         } else {
00697             return false;
00698         }
00699     }
00700 
00707     function feedResult( $row ) {
00708         if ( !isset( $row->title ) ) {
00709             return null;
00710         }
00711         $title = Title::makeTitle( intval( $row->namespace ), $row->title );
00712         if ( $title ) {
00713             $date = isset( $row->timestamp ) ? $row->timestamp : '';
00714             $comments = '';
00715             if ( $title ) {
00716                 $talkpage = $title->getTalkPage();
00717                 $comments = $talkpage->getFullURL();
00718             }
00719 
00720             return new FeedItem(
00721                 $title->getPrefixedText(),
00722                 $this->feedItemDesc( $row ),
00723                 $title->getFullURL(),
00724                 $date,
00725                 $this->feedItemAuthor( $row ),
00726                 $comments );
00727         } else {
00728             return null;
00729         }
00730     }
00731 
00732     function feedItemDesc( $row ) {
00733         return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : '';
00734     }
00735 
00736     function feedItemAuthor( $row ) {
00737         return isset( $row->user_text ) ? $row->user_text : '';
00738     }
00739 
00740     function feedTitle() {
00741         $desc = $this->getDescription();
00742         $code = $this->getConfig()->get( 'LanguageCode' );
00743         $sitename = $this->getConfig()->get( 'Sitename' );
00744         return "$sitename - $desc [$code]";
00745     }
00746 
00747     function feedDesc() {
00748         return $this->msg( 'tagline' )->text();
00749     }
00750 
00751     function feedUrl() {
00752         return $this->getPageTitle()->getFullURL();
00753     }
00754 }