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