MediaWiki
REL1_23
|
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 }