MediaWiki  REL1_22
ApiQuery.php
Go to the documentation of this file.
00001 <?php
00038 class ApiQuery extends ApiBase {
00039 
00044     private static $QueryPropModules = array(
00045         'categories' => 'ApiQueryCategories',
00046         'categoryinfo' => 'ApiQueryCategoryInfo',
00047         'duplicatefiles' => 'ApiQueryDuplicateFiles',
00048         'extlinks' => 'ApiQueryExternalLinks',
00049         'images' => 'ApiQueryImages',
00050         'imageinfo' => 'ApiQueryImageInfo',
00051         'info' => 'ApiQueryInfo',
00052         'links' => 'ApiQueryLinks',
00053         'iwlinks' => 'ApiQueryIWLinks',
00054         'langlinks' => 'ApiQueryLangLinks',
00055         'pageprops' => 'ApiQueryPageProps',
00056         'revisions' => 'ApiQueryRevisions',
00057         'stashimageinfo' => 'ApiQueryStashImageInfo',
00058         'templates' => 'ApiQueryLinks',
00059     );
00060 
00065     private static $QueryListModules = array(
00066         'allcategories' => 'ApiQueryAllCategories',
00067         'allfileusages' => 'ApiQueryAllLinks',
00068         'allimages' => 'ApiQueryAllImages',
00069         'alllinks' => 'ApiQueryAllLinks',
00070         'allpages' => 'ApiQueryAllPages',
00071         'alltransclusions' => 'ApiQueryAllLinks',
00072         'allusers' => 'ApiQueryAllUsers',
00073         'backlinks' => 'ApiQueryBacklinks',
00074         'blocks' => 'ApiQueryBlocks',
00075         'categorymembers' => 'ApiQueryCategoryMembers',
00076         'deletedrevs' => 'ApiQueryDeletedrevs',
00077         'embeddedin' => 'ApiQueryBacklinks',
00078         'exturlusage' => 'ApiQueryExtLinksUsage',
00079         'filearchive' => 'ApiQueryFilearchive',
00080         'imageusage' => 'ApiQueryBacklinks',
00081         'iwbacklinks' => 'ApiQueryIWBacklinks',
00082         'langbacklinks' => 'ApiQueryLangBacklinks',
00083         'logevents' => 'ApiQueryLogEvents',
00084         'pageswithprop' => 'ApiQueryPagesWithProp',
00085         'pagepropnames' => 'ApiQueryPagePropNames',
00086         'protectedtitles' => 'ApiQueryProtectedTitles',
00087         'querypage' => 'ApiQueryQueryPage',
00088         'random' => 'ApiQueryRandom',
00089         'recentchanges' => 'ApiQueryRecentChanges',
00090         'search' => 'ApiQuerySearch',
00091         'tags' => 'ApiQueryTags',
00092         'usercontribs' => 'ApiQueryContributions',
00093         'users' => 'ApiQueryUsers',
00094         'watchlist' => 'ApiQueryWatchlist',
00095         'watchlistraw' => 'ApiQueryWatchlistRaw',
00096     );
00097 
00102     private static $QueryMetaModules = array(
00103         'allmessages' => 'ApiQueryAllMessages',
00104         'siteinfo' => 'ApiQuerySiteinfo',
00105         'userinfo' => 'ApiQueryUserInfo',
00106         'filerepoinfo' => 'ApiQueryFileRepoInfo',
00107     );
00108 
00112     private $mPageSet;
00113 
00114     private $mParams;
00115     private $mNamedDB = array();
00116     private $mModuleMgr;
00117     private $mGeneratorContinue;
00118     private $mUseLegacyContinue;
00119 
00124     public function __construct( $main, $action ) {
00125         parent::__construct( $main, $action );
00126 
00127         $this->mModuleMgr = new ApiModuleManager( $this );
00128 
00129         // Allow custom modules to be added in LocalSettings.php
00130         global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules;
00131         $this->mModuleMgr->addModules( self::$QueryPropModules, 'prop' );
00132         $this->mModuleMgr->addModules( $wgAPIPropModules, 'prop' );
00133         $this->mModuleMgr->addModules( self::$QueryListModules, 'list' );
00134         $this->mModuleMgr->addModules( $wgAPIListModules, 'list' );
00135         $this->mModuleMgr->addModules( self::$QueryMetaModules, 'meta' );
00136         $this->mModuleMgr->addModules( $wgAPIMetaModules, 'meta' );
00137 
00138         // Create PageSet that will process titles/pageids/revids/generator
00139         $this->mPageSet = new ApiPageSet( $this );
00140     }
00141 
00146     public function getModuleManager() {
00147         return $this->mModuleMgr;
00148     }
00149 
00160     public function getNamedDB( $name, $db, $groups ) {
00161         if ( !array_key_exists( $name, $this->mNamedDB ) ) {
00162             $this->profileDBIn();
00163             $this->mNamedDB[$name] = wfGetDB( $db, $groups );
00164             $this->profileDBOut();
00165         }
00166         return $this->mNamedDB[$name];
00167     }
00168 
00173     public function getPageSet() {
00174         return $this->mPageSet;
00175     }
00176 
00182     public function getModules() {
00183         wfDeprecated( __METHOD__, '1.21' );
00184         return $this->getModuleManager()->getNamesWithClasses();
00185     }
00186 
00192     public function getGenerators() {
00193         wfDeprecated( __METHOD__, '1.21' );
00194         $gens = array();
00195         foreach ( $this->mModuleMgr->getNamesWithClasses() as $name => $class ) {
00196             if ( is_subclass_of( $class, 'ApiQueryGeneratorBase' ) ) {
00197                 $gens[$name] = $class;
00198             }
00199         }
00200         return $gens;
00201     }
00202 
00209     function getModuleType( $moduleName ) {
00210         return $this->getModuleManager()->getModuleGroup( $moduleName );
00211     }
00212 
00216     public function getCustomPrinter() {
00217         // If &exportnowrap is set, use the raw formatter
00218         if ( $this->getParameter( 'export' ) &&
00219                 $this->getParameter( 'exportnowrap' ) )
00220         {
00221             return new ApiFormatRaw( $this->getMain(),
00222                 $this->getMain()->createPrinterByName( 'xml' ) );
00223         } else {
00224             return null;
00225         }
00226     }
00227 
00238     public function execute() {
00239         $this->mParams = $this->extractRequestParams();
00240 
00241         // $pagesetParams is a array of parameter names used by the pageset generator
00242         //   or null if pageset has already finished and is no longer needed
00243         // $completeModules is a set of complete modules with the name as key
00244         $this->initContinue( $pagesetParams, $completeModules );
00245 
00246         // Instantiate requested modules
00247         $allModules = array();
00248         $this->instantiateModules( $allModules, 'prop' );
00249         $propModules = $allModules; // Keep a copy
00250         $this->instantiateModules( $allModules, 'list' );
00251         $this->instantiateModules( $allModules, 'meta' );
00252 
00253         // Filter modules based on continue parameter
00254         $modules = $this->initModules( $allModules, $completeModules, $pagesetParams !== null );
00255 
00256         // Execute pageset if in legacy mode or if pageset is not done
00257         if ( $completeModules === null || $pagesetParams !== null ) {
00258             // Populate page/revision information
00259             $this->mPageSet->execute();
00260             // Record page information (title, namespace, if exists, etc)
00261             $this->outputGeneralPageInfo();
00262         } else {
00263             $this->mPageSet->executeDryRun();
00264         }
00265 
00266         $cacheMode = $this->mPageSet->getCacheMode();
00267 
00268         // Execute all unfinished modules
00270         foreach ( $modules as $module ) {
00271             $params = $module->extractRequestParams();
00272             $cacheMode = $this->mergeCacheMode(
00273                 $cacheMode, $module->getCacheMode( $params ) );
00274             $module->profileIn();
00275             $module->execute();
00276             wfRunHooks( 'APIQueryAfterExecute', array( &$module ) );
00277             $module->profileOut();
00278         }
00279 
00280         // Set the cache mode
00281         $this->getMain()->setCacheMode( $cacheMode );
00282 
00283         if ( $completeModules === null ) {
00284             return; // Legacy continue, we are done
00285         }
00286 
00287         // Reformat query-continue result section
00288         $result = $this->getResult();
00289         $qc = $result->getData();
00290         if ( isset( $qc['query-continue'] ) ) {
00291             $qc = $qc['query-continue'];
00292             $result->unsetValue( null, 'query-continue' );
00293         } elseif ( $this->mGeneratorContinue !== null ) {
00294             $qc = array();
00295         } else {
00296             // no more "continue"s, we are done!
00297             return;
00298         }
00299 
00300         // we are done with all the modules that do not have result in query-continue
00301         $completeModules = array_merge( $completeModules, array_diff_key( $modules, $qc ) );
00302         if ( $pagesetParams !== null ) {
00303             // The pageset is still in use, check if all props have finished
00304             $incompleteProps = array_intersect_key( $propModules, $qc );
00305             if ( count( $incompleteProps ) > 0 ) {
00306                 // Properties are not done, continue with the same pageset state - copy current parameters
00307                 $main = $this->getMain();
00308                 $contValues = array();
00309                 foreach ( $pagesetParams as $param ) {
00310                     // The param name is already prefix-encoded
00311                     $contValues[$param] = $main->getVal( $param );
00312                 }
00313             } elseif ( $this->mGeneratorContinue !== null ) {
00314                 // Move to the next set of pages produced by pageset, properties need to be restarted
00315                 $contValues = $this->mGeneratorContinue;
00316                 $pagesetParams = array_keys( $contValues );
00317                 $completeModules = array_diff_key( $completeModules, $propModules );
00318             } else {
00319                 // Done with the pageset, finish up with the the lists and meta modules
00320                 $pagesetParams = null;
00321             }
00322         }
00323 
00324         $continue = '||' . implode( '|', array_keys( $completeModules ) );
00325         if ( $pagesetParams !== null ) {
00326             // list of all pageset parameters to use in the next request
00327             $continue = implode( '|', $pagesetParams ) . $continue;
00328         } else {
00329             // we are done with the pageset
00330             $contValues = array();
00331             $continue = '-' . $continue;
00332         }
00333         $contValues['continue'] = $continue;
00334         foreach ( $qc as $qcModule ) {
00335             foreach ( $qcModule as $qcKey => $qcValue ) {
00336                 $contValues[$qcKey] = $qcValue;
00337             }
00338         }
00339         $this->getResult()->addValue( null, 'continue', $contValues );
00340     }
00341 
00347     private function initContinue( &$pagesetParams, &$completeModules ) {
00348         $pagesetParams = array();
00349         $continue = $this->mParams['continue'];
00350         if ( $continue !== null ) {
00351             $this->mUseLegacyContinue = false;
00352             if ( $continue !== '' ) {
00353                 // Format: ' pagesetParam1 | pagesetParam2 || module1 | module2 | module3 | ...
00354                 // If pageset is done, use '-'
00355                 $continue = explode( '||', $continue );
00356                 $this->dieContinueUsageIf( count( $continue ) !== 2 );
00357                 if ( $continue[0] === '-' ) {
00358                     $pagesetParams = null; // No need to execute pageset
00359                 } elseif ( $continue[0] !== '' ) {
00360                     // list of pageset params that might need to be repeated
00361                     $pagesetParams = explode( '|', $continue[0] );
00362                 }
00363                 $continue = $continue[1];
00364             }
00365             if ( $continue !== '' ) {
00366                 $completeModules = array_flip( explode( '|', $continue ) );
00367             } else {
00368                 $completeModules = array();
00369             }
00370         } else {
00371             $this->mUseLegacyContinue = true;
00372             $completeModules = null;
00373         }
00374     }
00375 
00383     private function initModules( $allModules, $completeModules, $usePageset ) {
00384         $modules = $allModules;
00385         $tmp = $completeModules;
00386         $wasPosted = $this->getRequest()->wasPosted();
00387 
00389         foreach ( $allModules as $moduleName => $module ) {
00390             if ( !$wasPosted && $module->mustBePosted() ) {
00391                 $this->dieUsageMsgOrDebug( array( 'mustbeposted', $moduleName ) );
00392             }
00393             if ( $completeModules !== null && array_key_exists( $moduleName, $completeModules ) ) {
00394                 // If this module is done, mark all its params as used
00395                 $module->extractRequestParams();
00396                 // Make sure this module is not used during execution
00397                 unset( $modules[$moduleName] );
00398                 unset( $tmp[$moduleName] );
00399             } elseif ( $completeModules === null || $usePageset ) {
00400                 // Query modules may optimize data requests through the $this->getPageSet()
00401                 // object by adding extra fields from the page table.
00402                 // This function will gather all the extra request fields from the modules.
00403                 $module->requestExtraData( $this->mPageSet );
00404             } else {
00405                 // Error - this prop module must have finished before generator is done
00406                 $this->dieContinueUsageIf( $this->mModuleMgr->getModuleGroup( $moduleName ) === 'prop' );
00407             }
00408         }
00409         $this->dieContinueUsageIf( $completeModules !== null && count( $tmp ) !== 0 );
00410         return $modules;
00411     }
00412 
00422     protected function mergeCacheMode( $cacheMode, $modCacheMode ) {
00423         if ( $modCacheMode === 'anon-public-user-private' ) {
00424             if ( $cacheMode !== 'private' ) {
00425                 $cacheMode = 'anon-public-user-private';
00426             }
00427         } elseif ( $modCacheMode === 'public' ) {
00428             // do nothing, if it's public already it will stay public
00429         } else { // private
00430             $cacheMode = 'private';
00431         }
00432         return $cacheMode;
00433     }
00434 
00440     private function instantiateModules( &$modules, $param ) {
00441         if ( isset( $this->mParams[$param] ) ) {
00442             foreach ( $this->mParams[$param] as $moduleName ) {
00443                 $instance = $this->mModuleMgr->getModule( $moduleName, $param );
00444                 if ( $instance === null ) {
00445                     ApiBase::dieDebug( __METHOD__, 'Error instantiating module' );
00446                 }
00447                 // Ignore duplicates. TODO 2.0: die()?
00448                 if ( !array_key_exists( $moduleName, $modules ) ) {
00449                     $modules[$moduleName] = $instance;
00450                 }
00451             }
00452         }
00453     }
00454 
00460     private function outputGeneralPageInfo() {
00461         $pageSet = $this->getPageSet();
00462         $result = $this->getResult();
00463 
00464         // We don't check for a full result set here because we can't be adding
00465         // more than 380K. The maximum revision size is in the megabyte range,
00466         // and the maximum result size must be even higher than that.
00467 
00468         $values = $pageSet->getNormalizedTitlesAsResult( $result );
00469         if ( $values ) {
00470             $result->addValue( 'query', 'normalized', $values );
00471         }
00472         $values = $pageSet->getConvertedTitlesAsResult( $result );
00473         if ( $values ) {
00474             $result->addValue( 'query', 'converted', $values );
00475         }
00476         $values = $pageSet->getInterwikiTitlesAsResult( $result, $this->mParams['iwurl'] );
00477         if ( $values ) {
00478             $result->addValue( 'query', 'interwiki', $values );
00479         }
00480         $values = $pageSet->getRedirectTitlesAsResult( $result );
00481         if ( $values ) {
00482             $result->addValue( 'query', 'redirects', $values );
00483         }
00484         $values = $pageSet->getMissingRevisionIDsAsResult( $result );
00485         if ( $values ) {
00486             $result->addValue( 'query', 'badrevids', $values );
00487         }
00488 
00489         // Page elements
00490         $pages = array();
00491 
00492         // Report any missing titles
00493         foreach ( $pageSet->getMissingTitles() as $fakeId => $title ) {
00494             $vals = array();
00495             ApiQueryBase::addTitleInfo( $vals, $title );
00496             $vals['missing'] = '';
00497             $pages[$fakeId] = $vals;
00498         }
00499         // Report any invalid titles
00500         foreach ( $pageSet->getInvalidTitles() as $fakeId => $title ) {
00501             $pages[$fakeId] = array( 'title' => $title, 'invalid' => '' );
00502         }
00503         // Report any missing page ids
00504         foreach ( $pageSet->getMissingPageIDs() as $pageid ) {
00505             $pages[$pageid] = array(
00506                 'pageid' => $pageid,
00507                 'missing' => ''
00508             );
00509         }
00510         // Report special pages
00512         foreach ( $pageSet->getSpecialTitles() as $fakeId => $title ) {
00513             $vals = array();
00514             ApiQueryBase::addTitleInfo( $vals, $title );
00515             $vals['special'] = '';
00516             if ( $title->isSpecialPage() &&
00517                     !SpecialPageFactory::exists( $title->getDBkey() ) ) {
00518                 $vals['missing'] = '';
00519             } elseif ( $title->getNamespace() == NS_MEDIA &&
00520                     !wfFindFile( $title ) ) {
00521                 $vals['missing'] = '';
00522             }
00523             $pages[$fakeId] = $vals;
00524         }
00525 
00526         // Output general page information for found titles
00527         foreach ( $pageSet->getGoodTitles() as $pageid => $title ) {
00528             $vals = array();
00529             $vals['pageid'] = $pageid;
00530             ApiQueryBase::addTitleInfo( $vals, $title );
00531             $pages[$pageid] = $vals;
00532         }
00533 
00534         if ( count( $pages ) ) {
00535             if ( $this->mParams['indexpageids'] ) {
00536                 $pageIDs = array_keys( $pages );
00537                 // json treats all map keys as strings - converting to match
00538                 $pageIDs = array_map( 'strval', $pageIDs );
00539                 $result->setIndexedTagName( $pageIDs, 'id' );
00540                 $result->addValue( 'query', 'pageids', $pageIDs );
00541             }
00542 
00543             $result->setIndexedTagName( $pages, 'page' );
00544             $result->addValue( 'query', 'pages', $pages );
00545         }
00546         if ( $this->mParams['export'] ) {
00547             $this->doExport( $pageSet, $result );
00548         }
00549     }
00550 
00560     public function setGeneratorContinue( $module, $paramName, $paramValue ) {
00561         if ( $this->mUseLegacyContinue ) {
00562             return false;
00563         }
00564         $paramName = $module->encodeParamName( $paramName );
00565         if ( $this->mGeneratorContinue === null ) {
00566             $this->mGeneratorContinue = array();
00567         }
00568         $this->mGeneratorContinue[$paramName] = $paramValue;
00569         return true;
00570     }
00571 
00576     private function doExport( $pageSet, $result ) {
00577         $exportTitles = array();
00578         $titles = $pageSet->getGoodTitles();
00579         if ( count( $titles ) ) {
00580             $user = $this->getUser();
00582             foreach ( $titles as $title ) {
00583                 if ( $title->userCan( 'read', $user ) ) {
00584                     $exportTitles[] = $title;
00585                 }
00586             }
00587         }
00588 
00589         $exporter = new WikiExporter( $this->getDB() );
00590         // WikiExporter writes to stdout, so catch its
00591         // output with an ob
00592         ob_start();
00593         $exporter->openStream();
00594         foreach ( $exportTitles as $title ) {
00595             $exporter->pageByTitle( $title );
00596         }
00597         $exporter->closeStream();
00598         $exportxml = ob_get_contents();
00599         ob_end_clean();
00600 
00601         // Don't check the size of exported stuff
00602         // It's not continuable, so it would cause more
00603         // problems than it'd solve
00604         $result->disableSizeCheck();
00605         if ( $this->mParams['exportnowrap'] ) {
00606             $result->reset();
00607             // Raw formatter will handle this
00608             $result->addValue( null, 'text', $exportxml );
00609             $result->addValue( null, 'mime', 'text/xml' );
00610         } else {
00611             $r = array();
00612             ApiResult::setContent( $r, $exportxml );
00613             $result->addValue( 'query', 'export', $r );
00614         }
00615         $result->enableSizeCheck();
00616     }
00617 
00618     public function getAllowedParams( $flags = 0 ) {
00619         $result = array(
00620             'prop' => array(
00621                 ApiBase::PARAM_ISMULTI => true,
00622                 ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'prop' )
00623             ),
00624             'list' => array(
00625                 ApiBase::PARAM_ISMULTI => true,
00626                 ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'list' )
00627             ),
00628             'meta' => array(
00629                 ApiBase::PARAM_ISMULTI => true,
00630                 ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'meta' )
00631             ),
00632             'indexpageids' => false,
00633             'export' => false,
00634             'exportnowrap' => false,
00635             'iwurl' => false,
00636             'continue' => null,
00637         );
00638         if ( $flags ) {
00639             $result += $this->getPageSet()->getFinalParams( $flags );
00640         }
00641         return $result;
00642     }
00643 
00648     public function makeHelpMsg() {
00649 
00650         // Use parent to make default message for the query module
00651         $msg = parent::makeHelpMsg();
00652 
00653         $querySeparator = str_repeat( '--- ', 12 );
00654         $moduleSeparator = str_repeat( '*** ', 14 );
00655         $msg .= "\n$querySeparator Query: Prop  $querySeparator\n\n";
00656         $msg .= $this->makeHelpMsgHelper( 'prop' );
00657         $msg .= "\n$querySeparator Query: List  $querySeparator\n\n";
00658         $msg .= $this->makeHelpMsgHelper( 'list' );
00659         $msg .= "\n$querySeparator Query: Meta  $querySeparator\n\n";
00660         $msg .= $this->makeHelpMsgHelper( 'meta' );
00661         $msg .= "\n\n$moduleSeparator Modules: continuation  $moduleSeparator\n\n";
00662 
00663         return $msg;
00664     }
00665 
00671     private function makeHelpMsgHelper( $group ) {
00672         $moduleDescriptions = array();
00673 
00674         $moduleNames = $this->mModuleMgr->getNames( $group );
00675         sort( $moduleNames );
00676         foreach ( $moduleNames as $name ) {
00680             $module = $this->mModuleMgr->getModule( $name );
00681 
00682             $msg = ApiMain::makeHelpMsgHeader( $module, $group );
00683             $msg2 = $module->makeHelpMsg();
00684             if ( $msg2 !== false ) {
00685                 $msg .= $msg2;
00686             }
00687             if ( $module instanceof ApiQueryGeneratorBase ) {
00688                 $msg .= "Generator:\n  This module may be used as a generator\n";
00689             }
00690             $moduleDescriptions[] = $msg;
00691         }
00692 
00693         return implode( "\n", $moduleDescriptions );
00694     }
00695 
00696     public function shouldCheckMaxlag() {
00697         return true;
00698     }
00699 
00700     public function getParamDescription() {
00701         return $this->getPageSet()->getFinalParamDescription() + array(
00702             'prop' => 'Which properties to get for the titles/revisions/pageids. Module help is available below',
00703             'list' => 'Which lists to get. Module help is available below',
00704             'meta' => 'Which metadata to get about the site. Module help is available below',
00705             'indexpageids' => 'Include an additional pageids section listing all returned page IDs',
00706             'export' => 'Export the current revisions of all given or generated pages',
00707             'exportnowrap' => 'Return the export XML without wrapping it in an XML result (same format as Special:Export). Can only be used with export',
00708             'iwurl' => 'Whether to get the full URL if the title is an interwiki link',
00709             'continue' => array(
00710                 'When present, formats query-continue as key-value pairs that should simply be merged into the original request.',
00711                 'This parameter must be set to an empty string in the initial query.',
00712                 'This parameter is recommended for all new development, and will be made default in the next API version.' ),
00713         );
00714     }
00715 
00716     public function getDescription() {
00717         return array(
00718             'Query API module allows applications to get needed pieces of data from the MediaWiki databases,',
00719             'and is loosely based on the old query.php interface.',
00720             'All data modifications will first have to use query to acquire a token to prevent abuse from malicious sites'
00721         );
00722     }
00723 
00724     public function getPossibleErrors() {
00725         return array_merge(
00726             parent::getPossibleErrors(),
00727             $this->getPageSet()->getFinalPossibleErrors()
00728         );
00729     }
00730 
00731     public function getExamples() {
00732         return array(
00733             'api.php?action=query&prop=revisions&meta=siteinfo&titles=Main%20Page&rvprop=user|comment&continue=',
00734             'api.php?action=query&generator=allpages&gapprefix=API/&prop=revisions&continue=',
00735         );
00736     }
00737 
00738     public function getHelpUrls() {
00739         return array(
00740             'https://www.mediawiki.org/wiki/API:Query',
00741             'https://www.mediawiki.org/wiki/API:Meta',
00742             'https://www.mediawiki.org/wiki/API:Properties',
00743             'https://www.mediawiki.org/wiki/API:Lists',
00744         );
00745     }
00746 }