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