X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/includes/api/ApiQuery.php diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index ec4da791..31bcc7a7 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -1,10 +1,10 @@ @gmail.com + * Copyright © 2006 Yuri Astrakhan "@gmail.com" * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,10 +24,7 @@ * @file */ -if ( !defined( 'MEDIAWIKI' ) ) { - // Eclipse helper - will be ignored in production - require_once( 'ApiBase.php' ); -} +use Wikimedia\Rdbms\IDatabase; /** * This is the main query class. It behaves similar to ApiMain: based on the @@ -42,105 +39,127 @@ if ( !defined( 'MEDIAWIKI' ) ) { */ class ApiQuery extends ApiBase { - private $mPropModuleNames, $mListModuleNames, $mMetaModuleNames; - private $mPageSet; - private $params, $redirects, $convertTitles; - - private $mQueryPropModules = array( + /** + * List of Api Query prop modules + * @var array + */ + private static $QueryPropModules = [ + 'categories' => 'ApiQueryCategories', + 'categoryinfo' => 'ApiQueryCategoryInfo', + 'contributors' => 'ApiQueryContributors', + 'deletedrevisions' => 'ApiQueryDeletedRevisions', + 'duplicatefiles' => 'ApiQueryDuplicateFiles', + 'extlinks' => 'ApiQueryExternalLinks', + 'fileusage' => 'ApiQueryBacklinksprop', + 'images' => 'ApiQueryImages', + 'imageinfo' => 'ApiQueryImageInfo', 'info' => 'ApiQueryInfo', - 'revisions' => 'ApiQueryRevisions', 'links' => 'ApiQueryLinks', + 'linkshere' => 'ApiQueryBacklinksprop', 'iwlinks' => 'ApiQueryIWLinks', 'langlinks' => 'ApiQueryLangLinks', - 'images' => 'ApiQueryImages', - 'imageinfo' => 'ApiQueryImageInfo', + 'pageprops' => 'ApiQueryPageProps', + 'redirects' => 'ApiQueryBacklinksprop', + 'revisions' => 'ApiQueryRevisions', 'stashimageinfo' => 'ApiQueryStashImageInfo', 'templates' => 'ApiQueryLinks', - 'categories' => 'ApiQueryCategories', - 'extlinks' => 'ApiQueryExternalLinks', - 'categoryinfo' => 'ApiQueryCategoryInfo', - 'duplicatefiles' => 'ApiQueryDuplicateFiles', - 'pageprops' => 'ApiQueryPageProps', - ); + 'transcludedin' => 'ApiQueryBacklinksprop', + ]; - private $mQueryListModules = array( - 'allimages' => 'ApiQueryAllimages', - 'allpages' => 'ApiQueryAllpages', - 'alllinks' => 'ApiQueryAllLinks', + /** + * List of Api Query list modules + * @var array + */ + private static $QueryListModules = [ 'allcategories' => 'ApiQueryAllCategories', + 'alldeletedrevisions' => 'ApiQueryAllDeletedRevisions', + 'allfileusages' => 'ApiQueryAllLinks', + 'allimages' => 'ApiQueryAllImages', + 'alllinks' => 'ApiQueryAllLinks', + 'allpages' => 'ApiQueryAllPages', + 'allredirects' => 'ApiQueryAllLinks', + 'allrevisions' => 'ApiQueryAllRevisions', + 'mystashedfiles' => 'ApiQueryMyStashedFiles', + 'alltransclusions' => 'ApiQueryAllLinks', 'allusers' => 'ApiQueryAllUsers', 'backlinks' => 'ApiQueryBacklinks', 'blocks' => 'ApiQueryBlocks', 'categorymembers' => 'ApiQueryCategoryMembers', 'deletedrevs' => 'ApiQueryDeletedrevs', 'embeddedin' => 'ApiQueryBacklinks', + 'exturlusage' => 'ApiQueryExtLinksUsage', 'filearchive' => 'ApiQueryFilearchive', 'imageusage' => 'ApiQueryBacklinks', 'iwbacklinks' => 'ApiQueryIWBacklinks', + 'langbacklinks' => 'ApiQueryLangBacklinks', 'logevents' => 'ApiQueryLogEvents', + 'pageswithprop' => 'ApiQueryPagesWithProp', + 'pagepropnames' => 'ApiQueryPagePropNames', + 'prefixsearch' => 'ApiQueryPrefixSearch', + 'protectedtitles' => 'ApiQueryProtectedTitles', + 'querypage' => 'ApiQueryQueryPage', + 'random' => 'ApiQueryRandom', 'recentchanges' => 'ApiQueryRecentChanges', 'search' => 'ApiQuerySearch', 'tags' => 'ApiQueryTags', 'usercontribs' => 'ApiQueryContributions', + 'users' => 'ApiQueryUsers', 'watchlist' => 'ApiQueryWatchlist', 'watchlistraw' => 'ApiQueryWatchlistRaw', - 'exturlusage' => 'ApiQueryExtLinksUsage', - 'users' => 'ApiQueryUsers', - 'random' => 'ApiQueryRandom', - 'protectedtitles' => 'ApiQueryProtectedTitles', - ); + ]; - private $mQueryMetaModules = array( + /** + * List of Api Query meta modules + * @var array + */ + private static $QueryMetaModules = [ + 'allmessages' => 'ApiQueryAllMessages', + 'authmanagerinfo' => 'ApiQueryAuthManagerInfo', 'siteinfo' => 'ApiQuerySiteinfo', 'userinfo' => 'ApiQueryUserInfo', - 'allmessages' => 'ApiQueryAllmessages', - ); + 'filerepoinfo' => 'ApiQueryFileRepoInfo', + 'tokens' => 'ApiQueryTokens', + ]; - private $mSlaveDB = null; - private $mNamedDB = array(); + /** + * @var ApiPageSet + */ + private $mPageSet; - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - - // Allow custom modules to be added in LocalSettings.php - global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules; - self::appendUserModules( $this->mQueryPropModules, $wgAPIPropModules ); - self::appendUserModules( $this->mQueryListModules, $wgAPIListModules ); - self::appendUserModules( $this->mQueryMetaModules, $wgAPIMetaModules ); - - $this->mPropModuleNames = array_keys( $this->mQueryPropModules ); - $this->mListModuleNames = array_keys( $this->mQueryListModules ); - $this->mMetaModuleNames = array_keys( $this->mQueryMetaModules ); - - // Allow the entire list of modules at first, - // but during module instantiation check if it can be used as a generator. - $this->mAllowedGenerators = array_merge( $this->mListModuleNames, $this->mPropModuleNames ); - } + private $mParams; + private $mNamedDB = []; + private $mModuleMgr; /** - * Helper function to append any add-in modules to the list - * @param $modules array Module array - * @param $newModules array Module array to add to $modules + * @param ApiMain $main + * @param string $action */ - private static function appendUserModules( &$modules, $newModules ) { - if ( is_array( $newModules ) ) { - foreach ( $newModules as $moduleName => $moduleClass ) { - $modules[$moduleName] = $moduleClass; - } - } + public function __construct( ApiMain $main, $action ) { + parent::__construct( $main, $action ); + + $this->mModuleMgr = new ApiModuleManager( $this ); + + // Allow custom modules to be added in LocalSettings.php + $config = $this->getConfig(); + $this->mModuleMgr->addModules( self::$QueryPropModules, 'prop' ); + $this->mModuleMgr->addModules( $config->get( 'APIPropModules' ), 'prop' ); + $this->mModuleMgr->addModules( self::$QueryListModules, 'list' ); + $this->mModuleMgr->addModules( $config->get( 'APIListModules' ), 'list' ); + $this->mModuleMgr->addModules( self::$QueryMetaModules, 'meta' ); + $this->mModuleMgr->addModules( $config->get( 'APIMetaModules' ), 'meta' ); + + Hooks::run( 'ApiQuery::moduleManager', [ $this->mModuleMgr ] ); + + // Create PageSet that will process titles/pageids/revids/generator + $this->mPageSet = new ApiPageSet( $this ); } /** - * Gets a default slave database connection object - * @return Database + * Overrides to return this instance's module manager. + * @return ApiModuleManager */ - public function getDB() { - if ( !isset( $this->mSlaveDB ) ) { - $this->profileDBIn(); - $this->mSlaveDB = wfGetDB( DB_SLAVE, 'api' ); - $this->profileDBOut(); - } - return $this->mSlaveDB; + public function getModuleManager() { + return $this->mModuleMgr; } /** @@ -148,17 +167,16 @@ class ApiQuery extends ApiBase { * If no such connection has been requested before, it will be created. * Subsequent calls with the same $name will return the same connection * as the first, regardless of the values of $db and $groups - * @param $name string Name to assign to the database connection - * @param $db int One of the DB_* constants - * @param $groups array Query groups - * @return Database + * @param string $name Name to assign to the database connection + * @param int $db One of the DB_* constants + * @param string|string[] $groups Query groups + * @return IDatabase */ public function getNamedDB( $name, $db, $groups ) { if ( !array_key_exists( $name, $this->mNamedDB ) ) { - $this->profileDBIn(); $this->mNamedDB[$name] = wfGetDB( $db, $groups ); - $this->profileDBOut(); } + return $this->mNamedDB[$name]; } @@ -171,39 +189,13 @@ class ApiQuery extends ApiBase { } /** - * Get the array mapping module names to class names - * @return array(modulename => classname) + * @return ApiFormatRaw|null */ - function getModules() { - return array_merge( $this->mQueryPropModules, $this->mQueryListModules, $this->mQueryMetaModules ); - } - - /** - * Get whether the specified module is a prop, list or a meta query module - * @param $moduleName string Name of the module to find type for - * @return mixed string or null - */ - function getModuleType( $moduleName ) { - if ( array_key_exists ( $moduleName, $this->mQueryPropModules ) ) { - return 'prop'; - } - - if ( array_key_exists ( $moduleName, $this->mQueryListModules ) ) { - return 'list'; - } - - if ( array_key_exists ( $moduleName, $this->mQueryMetaModules ) ) { - return 'meta'; - } - - return null; - } - public function getCustomPrinter() { // If &exportnowrap is set, use the raw formatter if ( $this->getParameter( 'export' ) && - $this->getParameter( 'exportnowrap' ) ) - { + $this->getParameter( 'exportnowrap' ) + ) { return new ApiFormatRaw( $this->getMain(), $this->getMain()->createPrinterByName( 'xml' ) ); } else { @@ -222,56 +214,75 @@ class ApiQuery extends ApiBase { * #5 Execute all requested modules */ public function execute() { - $this->params = $this->extractRequestParams(); - $this->redirects = $this->params['redirects']; - $this->convertTitles = $this->params['converttitles']; - - // Create PageSet - $this->mPageSet = new ApiPageSet( $this, $this->redirects, $this->convertTitles ); + $this->mParams = $this->extractRequestParams(); // Instantiate requested modules - $modules = array(); - $this->instantiateModules( $modules, 'prop', $this->mQueryPropModules ); - $this->instantiateModules( $modules, 'list', $this->mQueryListModules ); - $this->instantiateModules( $modules, 'meta', $this->mQueryMetaModules ); - - $cacheMode = 'public'; - - // If given, execute generator to substitute user supplied data with generated data. - if ( isset( $this->params['generator'] ) ) { - $generator = $this->newGenerator( $this->params['generator'] ); - $params = $generator->extractRequestParams(); - $cacheMode = $this->mergeCacheMode( $cacheMode, - $generator->getCacheMode( $params ) ); - $this->executeGeneratorModule( $generator, $modules ); - } else { - // Append custom fields and populate page/revision information - $this->addCustomFldsToPageSet( $modules, $this->mPageSet ); + $allModules = []; + $this->instantiateModules( $allModules, 'prop' ); + $propModules = array_keys( $allModules ); + $this->instantiateModules( $allModules, 'list' ); + $this->instantiateModules( $allModules, 'meta' ); + + // Filter modules based on continue parameter + $continuationManager = new ApiContinuationManager( $this, $allModules, $propModules ); + $this->setContinuationManager( $continuationManager ); + $modules = $continuationManager->getRunModules(); + + if ( !$continuationManager->isGeneratorDone() ) { + // Query modules may optimize data requests through the $this->getPageSet() + // object by adding extra fields from the page table. + foreach ( $modules as $module ) { + $module->requestExtraData( $this->mPageSet ); + } + // Populate page/revision information $this->mPageSet->execute(); + // Record page information (title, namespace, if exists, etc) + $this->outputGeneralPageInfo(); + } else { + $this->mPageSet->executeDryRun(); } - // Record page information (title, namespace, if exists, etc) - $this->outputGeneralPageInfo(); + $cacheMode = $this->mPageSet->getCacheMode(); - // Execute all requested modules. + // Execute all unfinished modules + /** @var ApiQueryBase $module */ foreach ( $modules as $module ) { $params = $module->extractRequestParams(); $cacheMode = $this->mergeCacheMode( $cacheMode, $module->getCacheMode( $params ) ); - $module->profileIn(); $module->execute(); - wfRunHooks( 'APIQueryAfterExecute', array( &$module ) ); - $module->profileOut(); + Hooks::run( 'APIQueryAfterExecute', [ &$module ] ); } // Set the cache mode $this->getMain()->setCacheMode( $cacheMode ); + + // Write the continuation data into the result + $this->setContinuationManager( null ); + if ( $this->mParams['rawcontinue'] ) { + $data = $continuationManager->getRawNonContinuation(); + if ( $data ) { + $this->getResult()->addValue( null, 'query-noncontinue', $data, + ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK ); + } + $data = $continuationManager->getRawContinuation(); + if ( $data ) { + $this->getResult()->addValue( null, 'query-continue', $data, + ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK ); + } + } else { + $continuationManager->setContinuationIntoResult( $this->getResult() ); + } } /** * Update a cache mode string, applying the cache mode of a new module to it. * The cache mode may increase in the level of privacy, but public modules * added to private data do not decrease the level of privacy. + * + * @param string $cacheMode + * @param string $modCacheMode + * @return string */ protected function mergeCacheMode( $cacheMode, $modCacheMode ) { if ( $modCacheMode === 'anon-public-user-private' ) { @@ -283,34 +294,30 @@ class ApiQuery extends ApiBase { } else { // private $cacheMode = 'private'; } - return $cacheMode; - } - /** - * Query modules may optimize data requests through the $this->getPageSet() object - * by adding extra fields from the page table. - * This function will gather all the extra request fields from the modules. - * @param $modules array of module objects - * @param $pageSet ApiPageSet - */ - private function addCustomFldsToPageSet( $modules, $pageSet ) { - // Query all requested modules. - foreach ( $modules as $module ) { - $module->requestExtraData( $pageSet ); - } + return $cacheMode; } /** * Create instances of all modules requested by the client - * @param $modules Array to append instantiated modules to - * @param $param string Parameter name to read modules from - * @param $moduleList Array array(modulename => classname) + * @param array $modules To append instantiated modules to + * @param string $param Parameter name to read modules from */ - private function instantiateModules( &$modules, $param, $moduleList ) { - $list = @$this->params[$param]; - if ( !is_null ( $list ) ) { - foreach ( $list as $moduleName ) { - $modules[] = new $moduleList[$moduleName] ( $this, $moduleName ); + private function instantiateModules( &$modules, $param ) { + $wasPosted = $this->getRequest()->wasPosted(); + if ( isset( $this->mParams[$param] ) ) { + foreach ( $this->mParams[$param] as $moduleName ) { + $instance = $this->mModuleMgr->getModule( $moduleName, $param ); + if ( $instance === null ) { + ApiBase::dieDebug( __METHOD__, 'Error instantiating module' ); + } + if ( !$wasPosted && $instance->mustBePosted() ) { + $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $moduleName ] ); + } + // Ignore duplicates. TODO 2.0: die()? + if ( !array_key_exists( $moduleName, $modules ) ) { + $modules[$moduleName] = $instance; + } } } } @@ -324,375 +331,218 @@ class ApiQuery extends ApiBase { $pageSet = $this->getPageSet(); $result = $this->getResult(); - // We don't check for a full result set here because we can't be adding - // more than 380K. The maximum revision size is in the megabyte range, - // and the maximum result size must be even higher than that. - - // Title normalizations - $normValues = array(); - foreach ( $pageSet->getNormalizedTitles() as $rawTitleStr => $titleStr ) { - $normValues[] = array( - 'from' => $rawTitleStr, - 'to' => $titleStr - ); - } - - if ( count( $normValues ) ) { - $result->setIndexedTagName( $normValues, 'n' ); - $result->addValue( 'query', 'normalized', $normValues ); - } - - // Title conversions - $convValues = array(); - foreach ( $pageSet->getConvertedTitles() as $rawTitleStr => $titleStr ) { - $convValues[] = array( - 'from' => $rawTitleStr, - 'to' => $titleStr - ); - } - - if ( count( $convValues ) ) { - $result->setIndexedTagName( $convValues, 'c' ); - $result->addValue( 'query', 'converted', $convValues ); - } + // We can't really handle max-result-size failure here, but we need to + // check anyway in case someone set the limit stupidly low. + $fit = true; - // Interwiki titles - $intrwValues = array(); - foreach ( $pageSet->getInterwikiTitles() as $rawTitleStr => $interwikiStr ) { - $intrwValues[] = array( - 'title' => $rawTitleStr, - 'iw' => $interwikiStr - ); + $values = $pageSet->getNormalizedTitlesAsResult( $result ); + if ( $values ) { + $fit = $fit && $result->addValue( 'query', 'normalized', $values ); } - - if ( count( $intrwValues ) ) { - $result->setIndexedTagName( $intrwValues, 'i' ); - $result->addValue( 'query', 'interwiki', $intrwValues ); + $values = $pageSet->getConvertedTitlesAsResult( $result ); + if ( $values ) { + $fit = $fit && $result->addValue( 'query', 'converted', $values ); } - - // Show redirect information - $redirValues = array(); - foreach ( $pageSet->getRedirectTitles() as $titleStrFrom => $titleStrTo ) { - $redirValues[] = array( - 'from' => strval( $titleStrFrom ), - 'to' => $titleStrTo - ); + $values = $pageSet->getInterwikiTitlesAsResult( $result, $this->mParams['iwurl'] ); + if ( $values ) { + $fit = $fit && $result->addValue( 'query', 'interwiki', $values ); } - - if ( count( $redirValues ) ) { - $result->setIndexedTagName( $redirValues, 'r' ); - $result->addValue( 'query', 'redirects', $redirValues ); + $values = $pageSet->getRedirectTitlesAsResult( $result ); + if ( $values ) { + $fit = $fit && $result->addValue( 'query', 'redirects', $values ); } - - // Missing revision elements - $missingRevIDs = $pageSet->getMissingRevisionIDs(); - if ( count( $missingRevIDs ) ) { - $revids = array(); - foreach ( $missingRevIDs as $revid ) { - $revids[$revid] = array( - 'revid' => $revid - ); - } - $result->setIndexedTagName( $revids, 'rev' ); - $result->addValue( 'query', 'badrevids', $revids ); + $values = $pageSet->getMissingRevisionIDsAsResult( $result ); + if ( $values ) { + $fit = $fit && $result->addValue( 'query', 'badrevids', $values ); } // Page elements - $pages = array(); + $pages = []; // Report any missing titles foreach ( $pageSet->getMissingTitles() as $fakeId => $title ) { - $vals = array(); + $vals = []; ApiQueryBase::addTitleInfo( $vals, $title ); - $vals['missing'] = ''; + $vals['missing'] = true; + if ( $title->isKnown() ) { + $vals['known'] = true; + } $pages[$fakeId] = $vals; } // Report any invalid titles - foreach ( $pageSet->getInvalidTitles() as $fakeId => $title ) { - $pages[$fakeId] = array( 'title' => $title, 'invalid' => '' ); + foreach ( $pageSet->getInvalidTitlesAndReasons() as $fakeId => $data ) { + $pages[$fakeId] = $data + [ 'invalid' => true ]; } // Report any missing page ids foreach ( $pageSet->getMissingPageIDs() as $pageid ) { - $pages[$pageid] = array( + $pages[$pageid] = [ 'pageid' => $pageid, - 'missing' => '' - ); + 'missing' => true, + ]; } // Report special pages + /** @var Title $title */ foreach ( $pageSet->getSpecialTitles() as $fakeId => $title ) { - $vals = array(); + $vals = []; ApiQueryBase::addTitleInfo( $vals, $title ); - $vals['special'] = ''; - if ( $title->getNamespace() == NS_SPECIAL && - !SpecialPage::exists( $title->getDbKey() ) ) { - $vals['missing'] = ''; - } elseif ( $title->getNamespace() == NS_MEDIA && - !wfFindFile( $title ) ) { - $vals['missing'] = ''; + $vals['special'] = true; + if ( !$title->isKnown() ) { + $vals['missing'] = true; } $pages[$fakeId] = $vals; } // Output general page information for found titles foreach ( $pageSet->getGoodTitles() as $pageid => $title ) { - $vals = array(); + $vals = []; $vals['pageid'] = $pageid; ApiQueryBase::addTitleInfo( $vals, $title ); $pages[$pageid] = $vals; } if ( count( $pages ) ) { - if ( $this->params['indexpageids'] ) { - $pageIDs = array_keys( $pages ); + $pageSet->populateGeneratorData( $pages ); + ApiResult::setArrayType( $pages, 'BCarray' ); + + if ( $this->mParams['indexpageids'] ) { + $pageIDs = array_keys( ApiResult::stripMetadataNonRecursive( $pages ) ); // json treats all map keys as strings - converting to match $pageIDs = array_map( 'strval', $pageIDs ); - $result->setIndexedTagName( $pageIDs, 'id' ); - $result->addValue( 'query', 'pageids', $pageIDs ); + ApiResult::setIndexedTagName( $pageIDs, 'id' ); + $fit = $fit && $result->addValue( 'query', 'pageids', $pageIDs ); } - $result->setIndexedTagName( $pages, 'page' ); - $result->addValue( 'query', 'pages', $pages ); + ApiResult::setIndexedTagName( $pages, 'page' ); + $fit = $fit && $result->addValue( 'query', 'pages', $pages ); + } + + if ( !$fit ) { + $this->dieWithError( 'apierror-badconfig-resulttoosmall', 'badconfig' ); } - if ( $this->params['export'] ) { + + if ( $this->mParams['export'] ) { $this->doExport( $pageSet, $result ); } } /** - * @param $pageSet ApiPageSet Pages to be exported - * @param $result ApiResult Result to output to + * @param ApiPageSet $pageSet Pages to be exported + * @param ApiResult $result Result to output to */ - private function doExport( $pageSet, $result ) { - $exportTitles = array(); + private function doExport( $pageSet, $result ) { + $exportTitles = []; $titles = $pageSet->getGoodTitles(); - if( count( $titles ) ) { + if ( count( $titles ) ) { + $user = $this->getUser(); + /** @var Title $title */ foreach ( $titles as $title ) { - if ( $title->userCanRead() ) { + if ( $title->userCan( 'read', $user ) ) { $exportTitles[] = $title; } } } - // only export when there are titles - if ( !count( $exportTitles ) ) { - return; - } $exporter = new WikiExporter( $this->getDB() ); - // WikiExporter writes to stdout, so catch its - // output with an ob - ob_start(); + $sink = new DumpStringOutput; + $exporter->setOutputSink( $sink ); $exporter->openStream(); foreach ( $exportTitles as $title ) { $exporter->pageByTitle( $title ); } $exporter->closeStream(); - $exportxml = ob_get_contents(); - ob_end_clean(); // Don't check the size of exported stuff // It's not continuable, so it would cause more // problems than it'd solve - $result->disableSizeCheck(); - if ( $this->params['exportnowrap'] ) { + if ( $this->mParams['exportnowrap'] ) { $result->reset(); // Raw formatter will handle this - $result->addValue( null, 'text', $exportxml ); - $result->addValue( null, 'mime', 'text/xml' ); + $result->addValue( null, 'text', $sink, ApiResult::NO_SIZE_CHECK ); + $result->addValue( null, 'mime', 'text/xml', ApiResult::NO_SIZE_CHECK ); + $result->addValue( null, 'filename', 'export.xml', ApiResult::NO_SIZE_CHECK ); } else { - $r = array(); - ApiResult::setContent( $r, $exportxml ); - $result->addValue( 'query', 'export', $r ); + $result->addValue( 'query', 'export', $sink, ApiResult::NO_SIZE_CHECK ); + $result->addValue( 'query', ApiResult::META_BC_SUBELEMENTS, [ 'export' ] ); } - $result->enableSizeCheck(); } - /** - * Create a generator object of the given type and return it - * @param $generatorName string Module name - * @return ApiQueryGeneratorBase - */ - public function newGenerator( $generatorName ) { - // Find class that implements requested generator - if ( isset( $this->mQueryListModules[$generatorName] ) ) { - $className = $this->mQueryListModules[$generatorName]; - } elseif ( isset( $this->mQueryPropModules[$generatorName] ) ) { - $className = $this->mQueryPropModules[$generatorName]; - } else { - ApiBase::dieDebug( __METHOD__, "Unknown generator=$generatorName" ); - } - $generator = new $className ( $this, $generatorName ); - if ( !$generator instanceof ApiQueryGeneratorBase ) { - $this->dieUsage( "Module $generatorName cannot be used as a generator", 'badgenerator' ); - } - $generator->setGeneratorMode(); - return $generator; - } - - /** - * For generator mode, execute generator, and use its output as new - * ApiPageSet - * @param $generator ApiQueryGeneratorBase Generator Module - * @param $modules array of module objects - */ - protected function executeGeneratorModule( $generator, $modules ) { - // Generator results - $resultPageSet = new ApiPageSet( $this, $this->redirects, $this->convertTitles ); - - // Add any additional fields modules may need - $generator->requestExtraData( $this->mPageSet ); - $this->addCustomFldsToPageSet( $modules, $resultPageSet ); - - // Populate page information with the original user input - $this->mPageSet->execute(); - - // populate resultPageSet with the generator output - $generator->profileIn(); - $generator->executeGenerator( $resultPageSet ); - wfRunHooks( 'APIQueryGeneratorAfterExecute', array( &$generator, &$resultPageSet ) ); - $resultPageSet->finishPageSetGeneration(); - $generator->profileOut(); - - // Swap the resulting pageset back in - $this->mPageSet = $resultPageSet; - } - - public function getAllowedParams() { - return array( - 'prop' => array( + public function getAllowedParams( $flags = 0 ) { + $result = [ + 'prop' => [ ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => $this->mPropModuleNames - ), - 'list' => array( + ApiBase::PARAM_TYPE => 'submodule', + ], + 'list' => [ ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => $this->mListModuleNames - ), - 'meta' => array( + ApiBase::PARAM_TYPE => 'submodule', + ], + 'meta' => [ ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => $this->mMetaModuleNames - ), - 'generator' => array( - ApiBase::PARAM_TYPE => $this->mAllowedGenerators - ), - 'redirects' => false, - 'converttitles' => false, + ApiBase::PARAM_TYPE => 'submodule', + ], 'indexpageids' => false, 'export' => false, 'exportnowrap' => false, - ); - } - - /** - * Override the parent to generate help messages for all available query modules. - * @return string - */ - public function makeHelpMsg() { - $msg = ''; - - // Make sure the internal object is empty - // (just in case a sub-module decides to optimize during instantiation) - $this->mPageSet = null; - $this->mAllowedGenerators = array(); // Will be repopulated - - $querySeparator = str_repeat( '--- ', 8 ); - $moduleSeparator = str_repeat( '*** ', 10 ); - $msg .= "\n$querySeparator Query: Prop $querySeparator\n\n"; - $msg .= $this->makeHelpMsgHelper( $this->mQueryPropModules, 'prop' ); - $msg .= "\n$querySeparator Query: List $querySeparator\n\n"; - $msg .= $this->makeHelpMsgHelper( $this->mQueryListModules, 'list' ); - $msg .= "\n$querySeparator Query: Meta $querySeparator\n\n"; - $msg .= $this->makeHelpMsgHelper( $this->mQueryMetaModules, 'meta' ); - $msg .= "\n\n$moduleSeparator Modules: continuation $moduleSeparator\n\n"; - - // Perform the base call last because the $this->mAllowedGenerators - // will be updated inside makeHelpMsgHelper() - // Use parent to make default message for the query module - $msg = parent::makeHelpMsg() . $msg; - - return $msg; - } - - /** - * For all modules in $moduleList, generate help messages and join them together - * @param $moduleList Array array(modulename => classname) - * @param $paramName string Parameter name - * @return string - */ - private function makeHelpMsgHelper( $moduleList, $paramName ) { - $moduleDescriptions = array(); - - foreach ( $moduleList as $moduleName => $moduleClass ) { - $module = new $moduleClass ( $this, $moduleName, null ); - - $msg = ApiMain::makeHelpMsgHeader( $module, $paramName ); - $msg2 = $module->makeHelpMsg(); - if ( $msg2 !== false ) { - $msg .= $msg2; - } - if ( $module instanceof ApiQueryGeneratorBase ) { - $this->mAllowedGenerators[] = $moduleName; - $msg .= "Generator:\n This module may be used as a generator\n"; - } - $moduleDescriptions[] = $msg; + 'iwurl' => false, + 'continue' => [ + ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', + ], + 'rawcontinue' => false, + ]; + if ( $flags ) { + $result += $this->getPageSet()->getFinalParams( $flags ); } - return implode( "\n", $moduleDescriptions ); - } - - /** - * Override to add extra parameters from PageSet - * @return string - */ - public function makeHelpMsgParameters() { - $psModule = new ApiPageSet( $this ); - return $psModule->makeHelpMsgParameters() . parent::makeHelpMsgParameters(); - } - - public function shouldCheckMaxlag() { - return true; + return $result; } - public function getParamDescription() { - return array( - 'prop' => 'Which properties to get for the titles/revisions/pageids. Module help is available below', - 'list' => 'Which lists to get. Module help is available below', - 'meta' => 'Which metadata to get about the site. Module help is available below', - 'generator' => array( 'Use the output of a list as the input for other prop/list/meta items', - 'NOTE: generator parameter names must be prefixed with a \'g\', see examples' ), - 'redirects' => 'Automatically resolve redirects', - 'converttitles' => array( "Convert titles to other variants if necessary. Only works if the wiki's content language supports variant conversion.", - 'Languages that support variant conversion include kk, ku, gan, tg, sr, zh' ), - 'indexpageids' => 'Include an additional pageids section listing all returned page IDs', - 'export' => 'Export the current revisions of all given or generated pages', - 'exportnowrap' => 'Return the export XML without wrapping it in an XML result (same format as Special:Export). Can only be used with export', + public function isReadMode() { + // We need to make an exception for certain meta modules that should be + // accessible even without the 'read' right. Restrict the exception as + // much as possible: no other modules allowed, and no pageset + // parameters either. We do allow the 'rawcontinue' and 'indexpageids' + // parameters since frameworks might add these unconditionally and they + // can't expose anything here. + $this->mParams = $this->extractRequestParams(); + $params = array_filter( + array_diff_key( + $this->mParams + $this->getPageSet()->extractRequestParams(), + [ 'rawcontinue' => 1, 'indexpageids' => 1 ] + ) ); - } + if ( array_keys( $params ) !== [ 'meta' ] ) { + return true; + } - public function getDescription() { - return array( - 'Query API module allows applications to get needed pieces of data from the MediaWiki databases,', - 'and is loosely based on the old query.php interface.', - 'All data modifications will first have to use query to acquire a token to prevent abuse from malicious sites' - ); - } + // Ask each module if it requires read mode. Any true => this returns + // true. + $modules = []; + $this->instantiateModules( $modules, 'meta' ); + foreach ( $modules as $module ) { + if ( $module->isReadMode() ) { + return true; + } + } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'badgenerator', 'info' => 'Module $generatorName cannot be used as a generator' ), - ) ); + return false; } - protected function getExamples() { - return array( - 'api.php?action=query&prop=revisions&meta=siteinfo&titles=Main%20Page&rvprop=user|comment', - 'api.php?action=query&generator=allpages&gapprefix=API/&prop=revisions', - ); + protected function getExamplesMessages() { + return [ + 'action=query&prop=revisions&meta=siteinfo&' . + 'titles=Main%20Page&rvprop=user|comment&continue=' + => 'apihelp-query-example-revisions', + 'action=query&generator=allpages&gapprefix=API/&prop=revisions&continue=' + => 'apihelp-query-example-allpages', + ]; } - public function getVersion() { - $psModule = new ApiPageSet( $this ); - $vers = array(); - $vers[] = __CLASS__ . ': $Id$'; - $vers[] = $psModule->getVersion(); - return $vers; + public function getHelpUrls() { + return [ + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Query', + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Meta', + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Properties', + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Lists', + ]; } }