MediaWiki  REL1_19
ApiPageSet.php
Go to the documentation of this file.
00001 <?php
00040 class ApiPageSet extends ApiQueryBase {
00041 
00042         private $mAllPages; // [ns][dbkey] => page_id or negative when missing
00043         private $mTitles, $mGoodTitles, $mMissingTitles, $mInvalidTitles;
00044         private $mMissingPageIDs, $mRedirectTitles, $mSpecialTitles;
00045         private $mNormalizedTitles, $mInterwikiTitles;
00046         private $mResolveRedirects, $mPendingRedirectIDs;
00047         private $mConvertTitles, $mConvertedTitles;
00048         private $mGoodRevIDs, $mMissingRevIDs;
00049         private $mFakePageId;
00050 
00051         private $mRequestedPageFields;
00052 
00059         public function __construct( $query, $resolveRedirects = false, $convertTitles = false ) {
00060                 parent::__construct( $query, 'query' );
00061 
00062                 $this->mAllPages = array();
00063                 $this->mTitles = array();
00064                 $this->mGoodTitles = array();
00065                 $this->mMissingTitles = array();
00066                 $this->mInvalidTitles = array();
00067                 $this->mMissingPageIDs = array();
00068                 $this->mRedirectTitles = array();
00069                 $this->mNormalizedTitles = array();
00070                 $this->mInterwikiTitles = array();
00071                 $this->mGoodRevIDs = array();
00072                 $this->mMissingRevIDs = array();
00073                 $this->mSpecialTitles = array();
00074 
00075                 $this->mRequestedPageFields = array();
00076                 $this->mResolveRedirects = $resolveRedirects;
00077                 if ( $resolveRedirects ) {
00078                         $this->mPendingRedirectIDs = array();
00079                 }
00080 
00081                 $this->mConvertTitles = $convertTitles;
00082                 $this->mConvertedTitles = array();
00083 
00084                 $this->mFakePageId = - 1;
00085         }
00086 
00091         public function isResolvingRedirects() {
00092                 return $this->mResolveRedirects;
00093         }
00094 
00100         public function requestField( $fieldName ) {
00101                 $this->mRequestedPageFields[$fieldName] = null;
00102         }
00103 
00110         public function getCustomField( $fieldName ) {
00111                 return $this->mRequestedPageFields[$fieldName];
00112         }
00113 
00120         public function getPageTableFields() {
00121                 // Ensure we get minimum required fields
00122                 // DON'T change this order
00123                 $pageFlds = array(
00124                         'page_namespace' => null,
00125                         'page_title' => null,
00126                         'page_id' => null,
00127                 );
00128 
00129                 if ( $this->mResolveRedirects ) {
00130                         $pageFlds['page_is_redirect'] = null;
00131                 }
00132 
00133                 // only store non-default fields
00134                 $this->mRequestedPageFields = array_diff_key( $this->mRequestedPageFields, $pageFlds );
00135 
00136                 $pageFlds = array_merge( $pageFlds, $this->mRequestedPageFields );
00137                 return array_keys( $pageFlds );
00138         }
00139 
00146         public function getAllTitlesByNamespace() {
00147                 return $this->mAllPages;
00148         }
00149 
00154         public function getTitles() {
00155                 return $this->mTitles;
00156         }
00157 
00162         public function getTitleCount() {
00163                 return count( $this->mTitles );
00164         }
00165 
00170         public function getGoodTitles() {
00171                 return $this->mGoodTitles;
00172         }
00173 
00178         public function getGoodTitleCount() {
00179                 return count( $this->mGoodTitles );
00180         }
00181 
00187         public function getMissingTitles() {
00188                 return $this->mMissingTitles;
00189         }
00190 
00196         public function getInvalidTitles() {
00197                 return $this->mInvalidTitles;
00198         }
00199 
00204         public function getMissingPageIDs() {
00205                 return $this->mMissingPageIDs;
00206         }
00207 
00213         public function getRedirectTitles() {
00214                 return $this->mRedirectTitles;
00215         }
00216 
00222         public function getNormalizedTitles() {
00223                 return $this->mNormalizedTitles;
00224         }
00225 
00231         public function getConvertedTitles() {
00232                 return $this->mConvertedTitles;
00233         }
00234 
00240         public function getInterwikiTitles() {
00241                 return $this->mInterwikiTitles;
00242         }
00243 
00248         public function getRevisionIDs() {
00249                 return $this->mGoodRevIDs;
00250         }
00251 
00256         public function getMissingRevisionIDs() {
00257                 return $this->mMissingRevIDs;
00258         }
00259 
00264         public function getSpecialTitles() {
00265                 return $this->mSpecialTitles;
00266         }
00267 
00272         public function getRevisionCount() {
00273                 return count( $this->getRevisionIDs() );
00274         }
00275 
00279         public function execute() {
00280                 $this->profileIn();
00281                 $params = $this->extractRequestParams();
00282 
00283                 // Only one of the titles/pageids/revids is allowed at the same time
00284                 $dataSource = null;
00285                 if ( isset( $params['titles'] ) ) {
00286                         $dataSource = 'titles';
00287                 }
00288                 if ( isset( $params['pageids'] ) ) {
00289                         if ( isset( $dataSource ) ) {
00290                                 $this->dieUsage( "Cannot use 'pageids' at the same time as '$dataSource'", 'multisource' );
00291                         }
00292                         $dataSource = 'pageids';
00293                 }
00294                 if ( isset( $params['revids'] ) ) {
00295                         if ( isset( $dataSource ) ) {
00296                                 $this->dieUsage( "Cannot use 'revids' at the same time as '$dataSource'", 'multisource' );
00297                         }
00298                         $dataSource = 'revids';
00299                 }
00300 
00301                 switch ( $dataSource ) {
00302                         case 'titles':
00303                                 $this->initFromTitles( $params['titles'] );
00304                                 break;
00305                         case 'pageids':
00306                                 $this->initFromPageIds( $params['pageids'] );
00307                                 break;
00308                         case 'revids':
00309                                 if ( $this->mResolveRedirects ) {
00310                                         $this->setWarning( 'Redirect resolution cannot be used together with the revids= parameter. ' .
00311                                         'Any redirects the revids= point to have not been resolved.' );
00312                                 }
00313                                 $this->mResolveRedirects = false;
00314                                 $this->initFromRevIDs( $params['revids'] );
00315                                 break;
00316                         default:
00317                                 // Do nothing - some queries do not need any of the data sources.
00318                                 break;
00319                 }
00320                 $this->profileOut();
00321         }
00322 
00327         public function populateFromTitles( $titles ) {
00328                 $this->profileIn();
00329                 $this->initFromTitles( $titles );
00330                 $this->profileOut();
00331         }
00332 
00337         public function populateFromPageIDs( $pageIDs ) {
00338                 $this->profileIn();
00339                 $this->initFromPageIds( $pageIDs );
00340                 $this->profileOut();
00341         }
00342 
00348         public function populateFromQueryResult( $db, $queryResult ) {
00349                 $this->profileIn();
00350                 $this->initFromQueryResult( $queryResult );
00351                 $this->profileOut();
00352         }
00353 
00358         public function populateFromRevisionIDs( $revIDs ) {
00359                 $this->profileIn();
00360                 $this->initFromRevIDs( $revIDs );
00361                 $this->profileOut();
00362         }
00363 
00368         public function processDbRow( $row ) {
00369                 // Store Title object in various data structures
00370                 $title = Title::makeTitle( $row->page_namespace, $row->page_title );
00371 
00372                 $pageId = intval( $row->page_id );
00373                 $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId;
00374                 $this->mTitles[] = $title;
00375 
00376                 if ( $this->mResolveRedirects && $row->page_is_redirect == '1' ) {
00377                         $this->mPendingRedirectIDs[$pageId] = $title;
00378                 } else {
00379                         $this->mGoodTitles[$pageId] = $title;
00380                 }
00381 
00382                 foreach ( $this->mRequestedPageFields as $fieldName => &$fieldValues ) {
00383                         $fieldValues[$pageId] = $row-> $fieldName;
00384                 }
00385         }
00386 
00390         public function finishPageSetGeneration() {
00391                 $this->profileIn();
00392                 $this->resolvePendingRedirects();
00393                 $this->profileOut();
00394         }
00395 
00412         private function initFromTitles( $titles ) {
00413                 // Get validated and normalized title objects
00414                 $linkBatch = $this->processTitlesArray( $titles );
00415                 if ( $linkBatch->isEmpty() ) {
00416                         return;
00417                 }
00418 
00419                 $db = $this->getDB();
00420                 $set = $linkBatch->constructSet( 'page', $db );
00421 
00422                 // Get pageIDs data from the `page` table
00423                 $this->profileDBIn();
00424                 $res = $db->select( 'page', $this->getPageTableFields(), $set,
00425                                         __METHOD__ );
00426                 $this->profileDBOut();
00427 
00428                 // Hack: get the ns:titles stored in array(ns => array(titles)) format
00429                 $this->initFromQueryResult( $res, $linkBatch->data, true ); // process Titles
00430 
00431                 // Resolve any found redirects
00432                 $this->resolvePendingRedirects();
00433         }
00434 
00439         private function initFromPageIds( $pageids ) {
00440                 if ( !count( $pageids ) ) {
00441                         return;
00442                 }
00443 
00444                 $pageids = array_map( 'intval', $pageids ); // paranoia
00445                 $remaining = array_flip( $pageids );
00446 
00447                 $pageids = self::getPositiveIntegers( $pageids );
00448 
00449                 $res = null;
00450                 if ( count( $pageids ) ) {
00451                         $set = array(
00452                                 'page_id' => $pageids
00453                         );
00454                         $db = $this->getDB();
00455 
00456                         // Get pageIDs data from the `page` table
00457                         $this->profileDBIn();
00458                         $res = $db->select( 'page', $this->getPageTableFields(), $set,
00459                                                 __METHOD__ );
00460                         $this->profileDBOut();
00461                 }
00462 
00463                 $this->initFromQueryResult( $res, $remaining, false );  // process PageIDs
00464 
00465                 // Resolve any found redirects
00466                 $this->resolvePendingRedirects();
00467         }
00468 
00479         private function initFromQueryResult( $res, &$remaining = null, $processTitles = null ) {
00480                 if ( !is_null( $remaining ) && is_null( $processTitles ) ) {
00481                         ApiBase::dieDebug( __METHOD__, 'Missing $processTitles parameter when $remaining is provided' );
00482                 }
00483 
00484                 if ( $res ) {
00485                         foreach ( $res as $row ) {
00486                                 $pageId = intval( $row->page_id );
00487 
00488                                 // Remove found page from the list of remaining items
00489                                 if ( isset( $remaining ) ) {
00490                                         if ( $processTitles ) {
00491                                                 unset( $remaining[$row->page_namespace][$row->page_title] );
00492                                         } else {
00493                                                 unset( $remaining[$pageId] );
00494                                         }
00495                                 }
00496 
00497                                 // Store any extra fields requested by modules
00498                                 $this->processDbRow( $row );
00499                         }
00500                 }
00501 
00502                 if ( isset( $remaining ) ) {
00503                         // Any items left in the $remaining list are added as missing
00504                         if ( $processTitles ) {
00505                                 // The remaining titles in $remaining are non-existent pages
00506                                 foreach ( $remaining as $ns => $dbkeys ) {
00507                                         foreach ( array_keys( $dbkeys ) as $dbkey ) {
00508                                                 $title = Title::makeTitle( $ns, $dbkey );
00509                                                 $this->mAllPages[$ns][$dbkey] = $this->mFakePageId;
00510                                                 $this->mMissingTitles[$this->mFakePageId] = $title;
00511                                                 $this->mFakePageId--;
00512                                                 $this->mTitles[] = $title;
00513                                         }
00514                                 }
00515                         } else {
00516                                 // The remaining pageids do not exist
00517                                 if ( !$this->mMissingPageIDs ) {
00518                                         $this->mMissingPageIDs = array_keys( $remaining );
00519                                 } else {
00520                                         $this->mMissingPageIDs = array_merge( $this->mMissingPageIDs, array_keys( $remaining ) );
00521                                 }
00522                         }
00523                 }
00524         }
00525 
00531         private function initFromRevIDs( $revids ) {
00532                 if ( !count( $revids ) ) {
00533                         return;
00534                 }
00535 
00536                 $revids = array_map( 'intval', $revids ); // paranoia
00537                 $db = $this->getDB();
00538                 $pageids = array();
00539                 $remaining = array_flip( $revids );
00540 
00541                 $revids = self::getPositiveIntegers( $revids );
00542 
00543                 if ( count( $revids ) ) {
00544                         $tables = array( 'revision', 'page' );
00545                         $fields = array( 'rev_id', 'rev_page' );
00546                         $where = array( 'rev_id' => $revids, 'rev_page = page_id' );
00547 
00548                         // Get pageIDs data from the `page` table
00549                         $this->profileDBIn();
00550                         $res = $db->select( $tables, $fields, $where,  __METHOD__ );
00551                         foreach ( $res as $row ) {
00552                                 $revid = intval( $row->rev_id );
00553                                 $pageid = intval( $row->rev_page );
00554                                 $this->mGoodRevIDs[$revid] = $pageid;
00555                                 $pageids[$pageid] = '';
00556                                 unset( $remaining[$revid] );
00557                         }
00558                         $this->profileDBOut();
00559                 }
00560 
00561                 $this->mMissingRevIDs = array_keys( $remaining );
00562 
00563                 // Populate all the page information
00564                 $this->initFromPageIds( array_keys( $pageids ) );
00565         }
00566 
00572         private function resolvePendingRedirects() {
00573                 if ( $this->mResolveRedirects ) {
00574                         $db = $this->getDB();
00575                         $pageFlds = $this->getPageTableFields();
00576 
00577                         // Repeat until all redirects have been resolved
00578                         // The infinite loop is prevented by keeping all known pages in $this->mAllPages
00579                         while ( $this->mPendingRedirectIDs ) {
00580                                 // Resolve redirects by querying the pagelinks table, and repeat the process
00581                                 // Create a new linkBatch object for the next pass
00582                                 $linkBatch = $this->getRedirectTargets();
00583 
00584                                 if ( $linkBatch->isEmpty() ) {
00585                                         break;
00586                                 }
00587 
00588                                 $set = $linkBatch->constructSet( 'page', $db );
00589                                 if ( $set === false ) {
00590                                         break;
00591                                 }
00592 
00593                                 // Get pageIDs data from the `page` table
00594                                 $this->profileDBIn();
00595                                 $res = $db->select( 'page', $pageFlds, $set, __METHOD__ );
00596                                 $this->profileDBOut();
00597 
00598                                 // Hack: get the ns:titles stored in array(ns => array(titles)) format
00599                                 $this->initFromQueryResult( $res, $linkBatch->data, true );
00600                         }
00601                 }
00602         }
00603 
00611         private function getRedirectTargets() {
00612                 $lb = new LinkBatch();
00613                 $db = $this->getDB();
00614 
00615                 $this->profileDBIn();
00616                 $res = $db->select(
00617                         'redirect',
00618                         array(
00619                                 'rd_from',
00620                                 'rd_namespace',
00621                                 'rd_fragment',
00622                                 'rd_interwiki',
00623                                 'rd_title'
00624                         ), array( 'rd_from' => array_keys( $this->mPendingRedirectIDs ) ),
00625                         __METHOD__
00626                 );
00627                 $this->profileDBOut();
00628                 foreach ( $res as $row ) {
00629                         $rdfrom = intval( $row->rd_from );
00630                         $from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText();
00631                         $to = Title::makeTitle( $row->rd_namespace, $row->rd_title, $row->rd_fragment, $row->rd_interwiki );
00632                         unset( $this->mPendingRedirectIDs[$rdfrom] );
00633                         if ( !isset( $this->mAllPages[$row->rd_namespace][$row->rd_title] ) ) {
00634                                 $lb->add( $row->rd_namespace, $row->rd_title );
00635                         }
00636                         $this->mRedirectTitles[$from] = $to;
00637                 }
00638 
00639                 if ( $this->mPendingRedirectIDs ) {
00640                         // We found pages that aren't in the redirect table
00641                         // Add them
00642                         foreach ( $this->mPendingRedirectIDs as $id => $title ) {
00643                                 $page = WikiPage::factory( $title );
00644                                 $rt = $page->insertRedirect();
00645                                 if ( !$rt ) {
00646                                         // What the hell. Let's just ignore this
00647                                         continue;
00648                                 }
00649                                 $lb->addObj( $rt );
00650                                 $this->mRedirectTitles[$title->getPrefixedText()] = $rt;
00651                                 unset( $this->mPendingRedirectIDs[$id] );
00652                         }
00653                 }
00654                 return $lb;
00655         }
00656 
00666         private function processTitlesArray( $titles ) {
00667                 $linkBatch = new LinkBatch();
00668 
00669                 foreach ( $titles as $title ) {
00670                         $titleObj = is_string( $title ) ? Title::newFromText( $title ) : $title;
00671                         if ( !$titleObj ) {
00672                                 // Handle invalid titles gracefully
00673                                 $this->mAllpages[0][$title] = $this->mFakePageId;
00674                                 $this->mInvalidTitles[$this->mFakePageId] = $title;
00675                                 $this->mFakePageId--;
00676                                 continue; // There's nothing else we can do
00677                         }
00678                         $unconvertedTitle = $titleObj->getPrefixedText();
00679                         $titleWasConverted = false;
00680                         $iw = $titleObj->getInterwiki();
00681                         if ( strval( $iw ) !== '' ) {
00682                                 // This title is an interwiki link.
00683                                 $this->mInterwikiTitles[$titleObj->getPrefixedText()] = $iw;
00684                         } else {
00685                                 // Variants checking
00686                                 global $wgContLang;
00687                                 if ( $this->mConvertTitles &&
00688                                                 count( $wgContLang->getVariants() ) > 1  &&
00689                                                 !$titleObj->exists() ) {
00690                                         // Language::findVariantLink will modify titleObj into
00691                                         // the canonical variant if possible
00692                                         $wgContLang->findVariantLink( $title, $titleObj );
00693                                         $titleWasConverted = $unconvertedTitle !== $titleObj->getPrefixedText();
00694                                 }
00695 
00696                                 if ( $titleObj->getNamespace() < 0 ) {
00697                                         // Handle Special and Media pages
00698                                         $titleObj = $titleObj->fixSpecialName();
00699                                         $this->mSpecialTitles[$this->mFakePageId] = $titleObj;
00700                                         $this->mFakePageId--;
00701                                 } else {
00702                                         // Regular page
00703                                         $linkBatch->addObj( $titleObj );
00704                                 }
00705                         }
00706 
00707                         // Make sure we remember the original title that was
00708                         // given to us. This way the caller can correlate new
00709                         // titles with the originally requested when e.g. the
00710                         // namespace is localized or the capitalization is
00711                         // different
00712                         if ( $titleWasConverted ) {
00713                                 $this->mConvertedTitles[$title] = $titleObj->getPrefixedText();
00714                         } elseif ( is_string( $title ) && $title !== $titleObj->getPrefixedText() ) {
00715                                 $this->mNormalizedTitles[$title] = $titleObj->getPrefixedText();
00716                         }
00717                 }
00718 
00719                 return $linkBatch;
00720         }
00721 
00728         private static function getPositiveIntegers( $array ) {
00729                 // bug 25734 API: possible issue with revids validation
00730                 // It seems with a load of revision rows, MySQL gets upset
00731                 // Remove any < 0 integers, as they can't be valid
00732                 foreach( $array as $i => $int ) {
00733                         if ( $int < 0 ) {
00734                                 unset( $array[$i] );
00735                         }
00736                 }
00737 
00738                 return $array;
00739         }
00740 
00741         public function getAllowedParams() {
00742                 return array(
00743                         'titles' => array(
00744                                 ApiBase::PARAM_ISMULTI => true
00745                         ),
00746                         'pageids' => array(
00747                                 ApiBase::PARAM_TYPE => 'integer',
00748                                 ApiBase::PARAM_ISMULTI => true
00749                         ),
00750                         'revids' => array(
00751                                 ApiBase::PARAM_TYPE => 'integer',
00752                                 ApiBase::PARAM_ISMULTI => true
00753                         )
00754                 );
00755         }
00756 
00757         public function getParamDescription() {
00758                 return array(
00759                         'titles' => 'A list of titles to work on',
00760                         'pageids' => 'A list of page IDs to work on',
00761                         'revids' => 'A list of revision IDs to work on'
00762                 );
00763         }
00764 
00765         public function getPossibleErrors() {
00766                 return array_merge( parent::getPossibleErrors(), array(
00767                         array( 'code' => 'multisource', 'info' => "Cannot use 'pageids' at the same time as 'dataSource'" ),
00768                         array( 'code' => 'multisource', 'info' => "Cannot use 'revids' at the same time as 'dataSource'" ),
00769                 ) );
00770         }
00771 
00772         public function getVersion() {
00773                 return __CLASS__ . ': $Id$';
00774         }
00775 }