MediaWiki  REL1_24
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         'contributors' => 'ApiQueryContributors',
00048         'duplicatefiles' => 'ApiQueryDuplicateFiles',
00049         'extlinks' => 'ApiQueryExternalLinks',
00050         'fileusage' => 'ApiQueryBacklinksprop',
00051         'images' => 'ApiQueryImages',
00052         'imageinfo' => 'ApiQueryImageInfo',
00053         'info' => 'ApiQueryInfo',
00054         'links' => 'ApiQueryLinks',
00055         'linkshere' => 'ApiQueryBacklinksprop',
00056         'iwlinks' => 'ApiQueryIWLinks',
00057         'langlinks' => 'ApiQueryLangLinks',
00058         'pageprops' => 'ApiQueryPageProps',
00059         'redirects' => 'ApiQueryBacklinksprop',
00060         'revisions' => 'ApiQueryRevisions',
00061         'stashimageinfo' => 'ApiQueryStashImageInfo',
00062         'templates' => 'ApiQueryLinks',
00063         'transcludedin' => 'ApiQueryBacklinksprop',
00064     );
00065 
00070     private static $QueryListModules = array(
00071         'allcategories' => 'ApiQueryAllCategories',
00072         'allfileusages' => 'ApiQueryAllLinks',
00073         'allimages' => 'ApiQueryAllImages',
00074         'alllinks' => 'ApiQueryAllLinks',
00075         'allpages' => 'ApiQueryAllPages',
00076         'allredirects' => 'ApiQueryAllLinks',
00077         'alltransclusions' => 'ApiQueryAllLinks',
00078         'allusers' => 'ApiQueryAllUsers',
00079         'backlinks' => 'ApiQueryBacklinks',
00080         'blocks' => 'ApiQueryBlocks',
00081         'categorymembers' => 'ApiQueryCategoryMembers',
00082         'deletedrevs' => 'ApiQueryDeletedrevs',
00083         'embeddedin' => 'ApiQueryBacklinks',
00084         'exturlusage' => 'ApiQueryExtLinksUsage',
00085         'filearchive' => 'ApiQueryFilearchive',
00086         'imageusage' => 'ApiQueryBacklinks',
00087         'iwbacklinks' => 'ApiQueryIWBacklinks',
00088         'langbacklinks' => 'ApiQueryLangBacklinks',
00089         'logevents' => 'ApiQueryLogEvents',
00090         'pageswithprop' => 'ApiQueryPagesWithProp',
00091         'pagepropnames' => 'ApiQueryPagePropNames',
00092         'prefixsearch' => 'ApiQueryPrefixSearch',
00093         'protectedtitles' => 'ApiQueryProtectedTitles',
00094         'querypage' => 'ApiQueryQueryPage',
00095         'random' => 'ApiQueryRandom',
00096         'recentchanges' => 'ApiQueryRecentChanges',
00097         'search' => 'ApiQuerySearch',
00098         'tags' => 'ApiQueryTags',
00099         'usercontribs' => 'ApiQueryContributions',
00100         'users' => 'ApiQueryUsers',
00101         'watchlist' => 'ApiQueryWatchlist',
00102         'watchlistraw' => 'ApiQueryWatchlistRaw',
00103     );
00104 
00109     private static $QueryMetaModules = array(
00110         'allmessages' => 'ApiQueryAllMessages',
00111         'siteinfo' => 'ApiQuerySiteinfo',
00112         'userinfo' => 'ApiQueryUserInfo',
00113         'filerepoinfo' => 'ApiQueryFileRepoInfo',
00114         'tokens' => 'ApiQueryTokens',
00115     );
00116 
00120     private $mPageSet;
00121 
00122     private $mParams;
00123     private $mNamedDB = array();
00124     private $mModuleMgr;
00125 
00130     public function __construct( ApiMain $main, $action ) {
00131         parent::__construct( $main, $action );
00132 
00133         $this->mModuleMgr = new ApiModuleManager( $this );
00134 
00135         // Allow custom modules to be added in LocalSettings.php
00136         $config = $this->getConfig();
00137         $this->mModuleMgr->addModules( self::$QueryPropModules, 'prop' );
00138         $this->mModuleMgr->addModules( $config->get( 'APIPropModules' ), 'prop' );
00139         $this->mModuleMgr->addModules( self::$QueryListModules, 'list' );
00140         $this->mModuleMgr->addModules( $config->get( 'APIListModules' ), 'list' );
00141         $this->mModuleMgr->addModules( self::$QueryMetaModules, 'meta' );
00142         $this->mModuleMgr->addModules( $config->get( 'APIMetaModules' ), 'meta' );
00143 
00144         // Create PageSet that will process titles/pageids/revids/generator
00145         $this->mPageSet = new ApiPageSet( $this );
00146     }
00147 
00152     public function getModuleManager() {
00153         return $this->mModuleMgr;
00154     }
00155 
00166     public function getNamedDB( $name, $db, $groups ) {
00167         if ( !array_key_exists( $name, $this->mNamedDB ) ) {
00168             $this->profileDBIn();
00169             $this->mNamedDB[$name] = wfGetDB( $db, $groups );
00170             $this->profileDBOut();
00171         }
00172 
00173         return $this->mNamedDB[$name];
00174     }
00175 
00180     public function getPageSet() {
00181         return $this->mPageSet;
00182     }
00183 
00189     public function getModules() {
00190         wfDeprecated( __METHOD__, '1.21' );
00191 
00192         return $this->getModuleManager()->getNamesWithClasses();
00193     }
00194 
00200     public function getGenerators() {
00201         wfDeprecated( __METHOD__, '1.21' );
00202         $gens = array();
00203         foreach ( $this->mModuleMgr->getNamesWithClasses() as $name => $class ) {
00204             if ( is_subclass_of( $class, 'ApiQueryGeneratorBase' ) ) {
00205                 $gens[$name] = $class;
00206             }
00207         }
00208 
00209         return $gens;
00210     }
00211 
00218     function getModuleType( $moduleName ) {
00219         return $this->getModuleManager()->getModuleGroup( $moduleName );
00220     }
00221 
00225     public function getCustomPrinter() {
00226         // If &exportnowrap is set, use the raw formatter
00227         if ( $this->getParameter( 'export' ) &&
00228             $this->getParameter( 'exportnowrap' )
00229         ) {
00230             return new ApiFormatRaw( $this->getMain(),
00231                 $this->getMain()->createPrinterByName( 'xml' ) );
00232         } else {
00233             return null;
00234         }
00235     }
00236 
00247     public function execute() {
00248         $this->mParams = $this->extractRequestParams();
00249 
00250         // Instantiate requested modules
00251         $allModules = array();
00252         $this->instantiateModules( $allModules, 'prop' );
00253         $propModules = array_keys( $allModules );
00254         $this->instantiateModules( $allModules, 'list' );
00255         $this->instantiateModules( $allModules, 'meta' );
00256 
00257         // Filter modules based on continue parameter
00258         list( $generatorDone, $modules ) = $this->getResult()->beginContinuation(
00259             $this->mParams['continue'], $allModules, $propModules
00260         );
00261 
00262         if ( !$generatorDone ) {
00263             // Query modules may optimize data requests through the $this->getPageSet()
00264             // object by adding extra fields from the page table.
00265             foreach ( $modules as $module ) {
00266                 $module->requestExtraData( $this->mPageSet );
00267             }
00268             // Populate page/revision information
00269             $this->mPageSet->execute();
00270             // Record page information (title, namespace, if exists, etc)
00271             $this->outputGeneralPageInfo();
00272         } else {
00273             $this->mPageSet->executeDryRun();
00274         }
00275 
00276         $cacheMode = $this->mPageSet->getCacheMode();
00277 
00278         // Execute all unfinished modules
00280         foreach ( $modules as $module ) {
00281             $params = $module->extractRequestParams();
00282             $cacheMode = $this->mergeCacheMode(
00283                 $cacheMode, $module->getCacheMode( $params ) );
00284             $module->profileIn();
00285             $module->execute();
00286             wfRunHooks( 'APIQueryAfterExecute', array( &$module ) );
00287             $module->profileOut();
00288         }
00289 
00290         // Set the cache mode
00291         $this->getMain()->setCacheMode( $cacheMode );
00292 
00293         // Write the continuation data into the result
00294         $this->getResult()->endContinuation(
00295             $this->mParams['continue'] === null ? 'raw' : 'standard'
00296         );
00297     }
00298 
00308     protected function mergeCacheMode( $cacheMode, $modCacheMode ) {
00309         if ( $modCacheMode === 'anon-public-user-private' ) {
00310             if ( $cacheMode !== 'private' ) {
00311                 $cacheMode = 'anon-public-user-private';
00312             }
00313         } elseif ( $modCacheMode === 'public' ) {
00314             // do nothing, if it's public already it will stay public
00315         } else { // private
00316             $cacheMode = 'private';
00317         }
00318 
00319         return $cacheMode;
00320     }
00321 
00327     private function instantiateModules( &$modules, $param ) {
00328         $wasPosted = $this->getRequest()->wasPosted();
00329         if ( isset( $this->mParams[$param] ) ) {
00330             foreach ( $this->mParams[$param] as $moduleName ) {
00331                 $instance = $this->mModuleMgr->getModule( $moduleName, $param );
00332                 if ( $instance === null ) {
00333                     ApiBase::dieDebug( __METHOD__, 'Error instantiating module' );
00334                 }
00335                 if ( !$wasPosted && $instance->mustBePosted() ) {
00336                     $this->dieUsageMsgOrDebug( array( 'mustbeposted', $moduleName ) );
00337                 }
00338                 // Ignore duplicates. TODO 2.0: die()?
00339                 if ( !array_key_exists( $moduleName, $modules ) ) {
00340                     $modules[$moduleName] = $instance;
00341                 }
00342             }
00343         }
00344     }
00345 
00351     private function outputGeneralPageInfo() {
00352         $pageSet = $this->getPageSet();
00353         $result = $this->getResult();
00354 
00355         // We can't really handle max-result-size failure here, but we need to
00356         // check anyway in case someone set the limit stupidly low.
00357         $fit = true;
00358 
00359         $values = $pageSet->getNormalizedTitlesAsResult( $result );
00360         if ( $values ) {
00361             $fit = $fit && $result->addValue( 'query', 'normalized', $values );
00362         }
00363         $values = $pageSet->getConvertedTitlesAsResult( $result );
00364         if ( $values ) {
00365             $fit = $fit && $result->addValue( 'query', 'converted', $values );
00366         }
00367         $values = $pageSet->getInterwikiTitlesAsResult( $result, $this->mParams['iwurl'] );
00368         if ( $values ) {
00369             $fit = $fit && $result->addValue( 'query', 'interwiki', $values );
00370         }
00371         $values = $pageSet->getRedirectTitlesAsResult( $result );
00372         if ( $values ) {
00373             $fit = $fit && $result->addValue( 'query', 'redirects', $values );
00374         }
00375         $values = $pageSet->getMissingRevisionIDsAsResult( $result );
00376         if ( $values ) {
00377             $fit = $fit && $result->addValue( 'query', 'badrevids', $values );
00378         }
00379 
00380         // Page elements
00381         $pages = array();
00382 
00383         // Report any missing titles
00384         foreach ( $pageSet->getMissingTitles() as $fakeId => $title ) {
00385             $vals = array();
00386             ApiQueryBase::addTitleInfo( $vals, $title );
00387             $vals['missing'] = '';
00388             $pages[$fakeId] = $vals;
00389         }
00390         // Report any invalid titles
00391         foreach ( $pageSet->getInvalidTitles() as $fakeId => $title ) {
00392             $pages[$fakeId] = array( 'title' => $title, 'invalid' => '' );
00393         }
00394         // Report any missing page ids
00395         foreach ( $pageSet->getMissingPageIDs() as $pageid ) {
00396             $pages[$pageid] = array(
00397                 'pageid' => $pageid,
00398                 'missing' => ''
00399             );
00400         }
00401         // Report special pages
00403         foreach ( $pageSet->getSpecialTitles() as $fakeId => $title ) {
00404             $vals = array();
00405             ApiQueryBase::addTitleInfo( $vals, $title );
00406             $vals['special'] = '';
00407             if ( $title->isSpecialPage() &&
00408                 !SpecialPageFactory::exists( $title->getDBkey() )
00409             ) {
00410                 $vals['missing'] = '';
00411             } elseif ( $title->getNamespace() == NS_MEDIA &&
00412                 !wfFindFile( $title )
00413             ) {
00414                 $vals['missing'] = '';
00415             }
00416             $pages[$fakeId] = $vals;
00417         }
00418 
00419         // Output general page information for found titles
00420         foreach ( $pageSet->getGoodTitles() as $pageid => $title ) {
00421             $vals = array();
00422             $vals['pageid'] = $pageid;
00423             ApiQueryBase::addTitleInfo( $vals, $title );
00424             $pages[$pageid] = $vals;
00425         }
00426 
00427         if ( count( $pages ) ) {
00428             if ( $this->mParams['indexpageids'] ) {
00429                 $pageIDs = array_keys( $pages );
00430                 // json treats all map keys as strings - converting to match
00431                 $pageIDs = array_map( 'strval', $pageIDs );
00432                 $result->setIndexedTagName( $pageIDs, 'id' );
00433                 $fit = $fit && $result->addValue( 'query', 'pageids', $pageIDs );
00434             }
00435 
00436             $result->setIndexedTagName( $pages, 'page' );
00437             $fit = $fit && $result->addValue( 'query', 'pages', $pages );
00438         }
00439 
00440         if ( !$fit ) {
00441             $this->dieUsage(
00442                 'The value of $wgAPIMaxResultSize on this wiki is ' .
00443                     'too small to hold basic result information',
00444                 'badconfig'
00445             );
00446         }
00447 
00448         if ( $this->mParams['export'] ) {
00449             $this->doExport( $pageSet, $result );
00450         }
00451     }
00452 
00463     public function setGeneratorContinue( $module, $paramName, $paramValue ) {
00464         wfDeprecated( __METHOD__, '1.24' );
00465         $this->getResult()->setGeneratorContinueParam( $module, $paramName, $paramValue );
00466         return $this->getParameter( 'continue' ) !== null;
00467     }
00468 
00473     private function doExport( $pageSet, $result ) {
00474         $exportTitles = array();
00475         $titles = $pageSet->getGoodTitles();
00476         if ( count( $titles ) ) {
00477             $user = $this->getUser();
00479             foreach ( $titles as $title ) {
00480                 if ( $title->userCan( 'read', $user ) ) {
00481                     $exportTitles[] = $title;
00482                 }
00483             }
00484         }
00485 
00486         $exporter = new WikiExporter( $this->getDB() );
00487         // WikiExporter writes to stdout, so catch its
00488         // output with an ob
00489         ob_start();
00490         $exporter->openStream();
00491         foreach ( $exportTitles as $title ) {
00492             $exporter->pageByTitle( $title );
00493         }
00494         $exporter->closeStream();
00495         $exportxml = ob_get_contents();
00496         ob_end_clean();
00497 
00498         // Don't check the size of exported stuff
00499         // It's not continuable, so it would cause more
00500         // problems than it'd solve
00501         if ( $this->mParams['exportnowrap'] ) {
00502             $result->reset();
00503             // Raw formatter will handle this
00504             $result->addValue( null, 'text', $exportxml, ApiResult::NO_SIZE_CHECK );
00505             $result->addValue( null, 'mime', 'text/xml', ApiResult::NO_SIZE_CHECK );
00506         } else {
00507             $r = array();
00508             ApiResult::setContent( $r, $exportxml );
00509             $result->addValue( 'query', 'export', $r, ApiResult::NO_SIZE_CHECK );
00510         }
00511     }
00512 
00513     public function getAllowedParams( $flags = 0 ) {
00514         $result = array(
00515             'prop' => array(
00516                 ApiBase::PARAM_ISMULTI => true,
00517                 ApiBase::PARAM_TYPE => 'submodule',
00518             ),
00519             'list' => array(
00520                 ApiBase::PARAM_ISMULTI => true,
00521                 ApiBase::PARAM_TYPE => 'submodule',
00522             ),
00523             'meta' => array(
00524                 ApiBase::PARAM_ISMULTI => true,
00525                 ApiBase::PARAM_TYPE => 'submodule',
00526             ),
00527             'indexpageids' => false,
00528             'export' => false,
00529             'exportnowrap' => false,
00530             'iwurl' => false,
00531             'continue' => null,
00532             'rawcontinue' => false,
00533         );
00534         if ( $flags ) {
00535             $result += $this->getPageSet()->getFinalParams( $flags );
00536         }
00537 
00538         return $result;
00539     }
00540 
00545     public function makeHelpMsg() {
00546 
00547         // Use parent to make default message for the query module
00548         $msg = parent::makeHelpMsg();
00549 
00550         $querySeparator = str_repeat( '--- ', 12 );
00551         $moduleSeparator = str_repeat( '*** ', 14 );
00552         $msg .= "\n$querySeparator Query: Prop  $querySeparator\n\n";
00553         $msg .= $this->makeHelpMsgHelper( 'prop' );
00554         $msg .= "\n$querySeparator Query: List  $querySeparator\n\n";
00555         $msg .= $this->makeHelpMsgHelper( 'list' );
00556         $msg .= "\n$querySeparator Query: Meta  $querySeparator\n\n";
00557         $msg .= $this->makeHelpMsgHelper( 'meta' );
00558         $msg .= "\n\n$moduleSeparator Modules: continuation  $moduleSeparator\n\n";
00559 
00560         return $msg;
00561     }
00562 
00568     private function makeHelpMsgHelper( $group ) {
00569         $moduleDescriptions = array();
00570 
00571         $moduleNames = $this->mModuleMgr->getNames( $group );
00572         sort( $moduleNames );
00573         foreach ( $moduleNames as $name ) {
00577             $module = $this->mModuleMgr->getModule( $name );
00578 
00579             $msg = ApiMain::makeHelpMsgHeader( $module, $group );
00580             $msg2 = $module->makeHelpMsg();
00581             if ( $msg2 !== false ) {
00582                 $msg .= $msg2;
00583             }
00584             if ( $module instanceof ApiQueryGeneratorBase ) {
00585                 $msg .= "Generator:\n  This module may be used as a generator\n";
00586             }
00587             $moduleDescriptions[] = $msg;
00588         }
00589 
00590         return implode( "\n", $moduleDescriptions );
00591     }
00592 
00593     public function shouldCheckMaxlag() {
00594         return true;
00595     }
00596 
00597     public function getParamDescription() {
00598         return $this->getPageSet()->getFinalParamDescription() + array(
00599             'prop' => 'Which properties to get for the titles/revisions/pageids. ' .
00600                 'Module help is available below',
00601             'list' => 'Which lists to get. Module help is available below',
00602             'meta' => 'Which metadata to get about the site. Module help is available below',
00603             'indexpageids' => 'Include an additional pageids section listing all returned page IDs',
00604             'export' => 'Export the current revisions of all given or generated pages',
00605             'exportnowrap' => 'Return the export XML without wrapping it in an ' .
00606                 'XML result (same format as Special:Export). Can only be used with export',
00607             'iwurl' => 'Whether to get the full URL if the title is an interwiki link',
00608             'continue' => array(
00609                 'When present, formats query-continue as key-value pairs that ' .
00610                     'should simply be merged into the original request.',
00611                 'This parameter must be set to an empty string in the initial query.',
00612                 'This parameter is recommended for all new development, and ' .
00613                     'will be made default in the next API version.'
00614             ),
00615             'rawcontinue' => 'Currently ignored. In the future, \'continue=\' will become the ' .
00616                 'default and this will be needed to receive the raw query-continue data.',
00617         );
00618     }
00619 
00620     public function getDescription() {
00621         return array(
00622             'Query API module allows applications to get needed pieces of data ' .
00623                 'from the MediaWiki databases,',
00624             'and is loosely based on the old query.php interface.',
00625             'All data modifications will first have to use query to acquire a ' .
00626                 'token to prevent abuse from malicious sites.'
00627         );
00628     }
00629 
00630     public function getExamples() {
00631         return array(
00632             'api.php?action=query&prop=revisions&meta=siteinfo&' .
00633                 'titles=Main%20Page&rvprop=user|comment&continue=',
00634             'api.php?action=query&generator=allpages&gapprefix=API/&prop=revisions&continue=',
00635         );
00636     }
00637 
00638     public function getHelpUrls() {
00639         return array(
00640             'https://www.mediawiki.org/wiki/API:Query',
00641             'https://www.mediawiki.org/wiki/API:Meta',
00642             'https://www.mediawiki.org/wiki/API:Properties',
00643             'https://www.mediawiki.org/wiki/API:Lists',
00644         );
00645     }
00646 }