ApiQuery.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. <?php
  2. /**
  3. * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. */
  22. use MediaWiki\MediaWikiServices;
  23. use Wikimedia\Rdbms\IDatabase;
  24. /**
  25. * This is the main query class. It behaves similar to ApiMain: based on the
  26. * parameters given, it will create a list of titles to work on (an ApiPageSet
  27. * object), instantiate and execute various property/list/meta modules, and
  28. * assemble all resulting data into a single ApiResult object.
  29. *
  30. * In generator mode, a generator will be executed first to populate a second
  31. * ApiPageSet object, and that object will be used for all subsequent modules.
  32. *
  33. * @ingroup API
  34. */
  35. class ApiQuery extends ApiBase {
  36. /**
  37. * List of Api Query prop modules
  38. * @var array
  39. */
  40. private static $QueryPropModules = [
  41. 'categories' => ApiQueryCategories::class,
  42. 'categoryinfo' => ApiQueryCategoryInfo::class,
  43. 'contributors' => ApiQueryContributors::class,
  44. 'deletedrevisions' => ApiQueryDeletedRevisions::class,
  45. 'duplicatefiles' => ApiQueryDuplicateFiles::class,
  46. 'extlinks' => ApiQueryExternalLinks::class,
  47. 'fileusage' => ApiQueryBacklinksprop::class,
  48. 'images' => ApiQueryImages::class,
  49. 'imageinfo' => ApiQueryImageInfo::class,
  50. 'info' => ApiQueryInfo::class,
  51. 'links' => ApiQueryLinks::class,
  52. 'linkshere' => ApiQueryBacklinksprop::class,
  53. 'iwlinks' => ApiQueryIWLinks::class,
  54. 'langlinks' => ApiQueryLangLinks::class,
  55. 'pageprops' => ApiQueryPageProps::class,
  56. 'redirects' => ApiQueryBacklinksprop::class,
  57. 'revisions' => ApiQueryRevisions::class,
  58. 'stashimageinfo' => ApiQueryStashImageInfo::class,
  59. 'templates' => ApiQueryLinks::class,
  60. 'transcludedin' => ApiQueryBacklinksprop::class,
  61. ];
  62. /**
  63. * List of Api Query list modules
  64. * @var array
  65. */
  66. private static $QueryListModules = [
  67. 'allcategories' => ApiQueryAllCategories::class,
  68. 'alldeletedrevisions' => ApiQueryAllDeletedRevisions::class,
  69. 'allfileusages' => ApiQueryAllLinks::class,
  70. 'allimages' => ApiQueryAllImages::class,
  71. 'alllinks' => ApiQueryAllLinks::class,
  72. 'allpages' => ApiQueryAllPages::class,
  73. 'allredirects' => ApiQueryAllLinks::class,
  74. 'allrevisions' => ApiQueryAllRevisions::class,
  75. 'mystashedfiles' => ApiQueryMyStashedFiles::class,
  76. 'alltransclusions' => ApiQueryAllLinks::class,
  77. 'allusers' => ApiQueryAllUsers::class,
  78. 'backlinks' => ApiQueryBacklinks::class,
  79. 'blocks' => ApiQueryBlocks::class,
  80. 'categorymembers' => ApiQueryCategoryMembers::class,
  81. 'deletedrevs' => ApiQueryDeletedrevs::class,
  82. 'embeddedin' => ApiQueryBacklinks::class,
  83. 'exturlusage' => ApiQueryExtLinksUsage::class,
  84. 'filearchive' => ApiQueryFilearchive::class,
  85. 'imageusage' => ApiQueryBacklinks::class,
  86. 'iwbacklinks' => ApiQueryIWBacklinks::class,
  87. 'langbacklinks' => ApiQueryLangBacklinks::class,
  88. 'logevents' => ApiQueryLogEvents::class,
  89. 'pageswithprop' => ApiQueryPagesWithProp::class,
  90. 'pagepropnames' => ApiQueryPagePropNames::class,
  91. 'prefixsearch' => ApiQueryPrefixSearch::class,
  92. 'protectedtitles' => ApiQueryProtectedTitles::class,
  93. 'querypage' => ApiQueryQueryPage::class,
  94. 'random' => ApiQueryRandom::class,
  95. 'recentchanges' => ApiQueryRecentChanges::class,
  96. 'search' => ApiQuerySearch::class,
  97. 'tags' => ApiQueryTags::class,
  98. 'usercontribs' => ApiQueryUserContribs::class,
  99. 'users' => ApiQueryUsers::class,
  100. 'watchlist' => ApiQueryWatchlist::class,
  101. 'watchlistraw' => ApiQueryWatchlistRaw::class,
  102. ];
  103. /**
  104. * List of Api Query meta modules
  105. * @var array
  106. */
  107. private static $QueryMetaModules = [
  108. 'allmessages' => ApiQueryAllMessages::class,
  109. 'authmanagerinfo' => ApiQueryAuthManagerInfo::class,
  110. 'siteinfo' => ApiQuerySiteinfo::class,
  111. 'userinfo' => ApiQueryUserInfo::class,
  112. 'filerepoinfo' => ApiQueryFileRepoInfo::class,
  113. 'tokens' => ApiQueryTokens::class,
  114. 'languageinfo' => ApiQueryLanguageinfo::class,
  115. ];
  116. /**
  117. * @var ApiPageSet
  118. */
  119. private $mPageSet;
  120. private $mParams;
  121. private $mNamedDB = [];
  122. private $mModuleMgr;
  123. /**
  124. * @param ApiMain $main
  125. * @param string $action
  126. */
  127. public function __construct( ApiMain $main, $action ) {
  128. parent::__construct( $main, $action );
  129. $this->mModuleMgr = new ApiModuleManager(
  130. $this,
  131. MediaWikiServices::getInstance()->getObjectFactory()
  132. );
  133. // Allow custom modules to be added in LocalSettings.php
  134. $config = $this->getConfig();
  135. $this->mModuleMgr->addModules( self::$QueryPropModules, 'prop' );
  136. $this->mModuleMgr->addModules( $config->get( 'APIPropModules' ), 'prop' );
  137. $this->mModuleMgr->addModules( self::$QueryListModules, 'list' );
  138. $this->mModuleMgr->addModules( $config->get( 'APIListModules' ), 'list' );
  139. $this->mModuleMgr->addModules( self::$QueryMetaModules, 'meta' );
  140. $this->mModuleMgr->addModules( $config->get( 'APIMetaModules' ), 'meta' );
  141. Hooks::run( 'ApiQuery::moduleManager', [ $this->mModuleMgr ] );
  142. // Create PageSet that will process titles/pageids/revids/generator
  143. $this->mPageSet = new ApiPageSet( $this );
  144. }
  145. /**
  146. * Overrides to return this instance's module manager.
  147. * @return ApiModuleManager
  148. */
  149. public function getModuleManager() {
  150. return $this->mModuleMgr;
  151. }
  152. /**
  153. * Get the query database connection with the given name.
  154. * If no such connection has been requested before, it will be created.
  155. * Subsequent calls with the same $name will return the same connection
  156. * as the first, regardless of the values of $db and $groups
  157. * @param string $name Name to assign to the database connection
  158. * @param int $db One of the DB_* constants
  159. * @param string|string[] $groups Query groups
  160. * @return IDatabase
  161. */
  162. public function getNamedDB( $name, $db, $groups ) {
  163. if ( !array_key_exists( $name, $this->mNamedDB ) ) {
  164. $this->mNamedDB[$name] = wfGetDB( $db, $groups );
  165. }
  166. return $this->mNamedDB[$name];
  167. }
  168. /**
  169. * Gets the set of pages the user has requested (or generated)
  170. * @return ApiPageSet
  171. */
  172. public function getPageSet() {
  173. return $this->mPageSet;
  174. }
  175. /**
  176. * @return ApiFormatRaw|null
  177. */
  178. public function getCustomPrinter() {
  179. // If &exportnowrap is set, use the raw formatter
  180. if ( $this->getParameter( 'export' ) &&
  181. $this->getParameter( 'exportnowrap' )
  182. ) {
  183. return new ApiFormatRaw( $this->getMain(),
  184. $this->getMain()->createPrinterByName( 'xml' ) );
  185. } else {
  186. return null;
  187. }
  188. }
  189. /**
  190. * Query execution happens in the following steps:
  191. * #1 Create a PageSet object with any pages requested by the user
  192. * #2 If using a generator, execute it to get a new ApiPageSet object
  193. * #3 Instantiate all requested modules.
  194. * This way the PageSet object will know what shared data is required,
  195. * and minimize DB calls.
  196. * #4 Output all normalization and redirect resolution information
  197. * #5 Execute all requested modules
  198. */
  199. public function execute() {
  200. $this->mParams = $this->extractRequestParams();
  201. // Instantiate requested modules
  202. $allModules = [];
  203. $this->instantiateModules( $allModules, 'prop' );
  204. $propModules = array_keys( $allModules );
  205. $this->instantiateModules( $allModules, 'list' );
  206. $this->instantiateModules( $allModules, 'meta' );
  207. // Filter modules based on continue parameter
  208. $continuationManager = new ApiContinuationManager( $this, $allModules, $propModules );
  209. $this->setContinuationManager( $continuationManager );
  210. /** @var ApiQueryBase[] $modules */
  211. $modules = $continuationManager->getRunModules();
  212. '@phan-var ApiQueryBase[] $modules';
  213. if ( !$continuationManager->isGeneratorDone() ) {
  214. // Query modules may optimize data requests through the $this->getPageSet()
  215. // object by adding extra fields from the page table.
  216. foreach ( $modules as $module ) {
  217. $module->requestExtraData( $this->mPageSet );
  218. }
  219. // Populate page/revision information
  220. $this->mPageSet->execute();
  221. // Record page information (title, namespace, if exists, etc)
  222. $this->outputGeneralPageInfo();
  223. } else {
  224. $this->mPageSet->executeDryRun();
  225. }
  226. $cacheMode = $this->mPageSet->getCacheMode();
  227. // Execute all unfinished modules
  228. foreach ( $modules as $module ) {
  229. $params = $module->extractRequestParams();
  230. $cacheMode = $this->mergeCacheMode(
  231. $cacheMode, $module->getCacheMode( $params ) );
  232. $module->execute();
  233. Hooks::run( 'APIQueryAfterExecute', [ &$module ] );
  234. }
  235. // Set the cache mode
  236. $this->getMain()->setCacheMode( $cacheMode );
  237. // Write the continuation data into the result
  238. $this->setContinuationManager( null );
  239. if ( $this->mParams['rawcontinue'] ) {
  240. $data = $continuationManager->getRawNonContinuation();
  241. if ( $data ) {
  242. $this->getResult()->addValue( null, 'query-noncontinue', $data,
  243. ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
  244. }
  245. $data = $continuationManager->getRawContinuation();
  246. if ( $data ) {
  247. $this->getResult()->addValue( null, 'query-continue', $data,
  248. ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
  249. }
  250. } else {
  251. $continuationManager->setContinuationIntoResult( $this->getResult() );
  252. }
  253. }
  254. /**
  255. * Update a cache mode string, applying the cache mode of a new module to it.
  256. * The cache mode may increase in the level of privacy, but public modules
  257. * added to private data do not decrease the level of privacy.
  258. *
  259. * @param string $cacheMode
  260. * @param string $modCacheMode
  261. * @return string
  262. */
  263. protected function mergeCacheMode( $cacheMode, $modCacheMode ) {
  264. if ( $modCacheMode === 'anon-public-user-private' ) {
  265. if ( $cacheMode !== 'private' ) {
  266. $cacheMode = 'anon-public-user-private';
  267. }
  268. } elseif ( $modCacheMode === 'public' ) {
  269. // do nothing, if it's public already it will stay public
  270. } else {
  271. $cacheMode = 'private';
  272. }
  273. return $cacheMode;
  274. }
  275. /**
  276. * Create instances of all modules requested by the client
  277. * @param array $modules To append instantiated modules to
  278. * @param string $param Parameter name to read modules from
  279. */
  280. private function instantiateModules( &$modules, $param ) {
  281. $wasPosted = $this->getRequest()->wasPosted();
  282. if ( isset( $this->mParams[$param] ) ) {
  283. foreach ( $this->mParams[$param] as $moduleName ) {
  284. $instance = $this->mModuleMgr->getModule( $moduleName, $param );
  285. if ( $instance === null ) {
  286. ApiBase::dieDebug( __METHOD__, 'Error instantiating module' );
  287. }
  288. if ( !$wasPosted && $instance->mustBePosted() ) {
  289. $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $moduleName ] );
  290. }
  291. // Ignore duplicates. TODO 2.0: die()?
  292. if ( !array_key_exists( $moduleName, $modules ) ) {
  293. $modules[$moduleName] = $instance;
  294. }
  295. }
  296. }
  297. }
  298. /**
  299. * Appends an element for each page in the current pageSet with the
  300. * most general information (id, title), plus any title normalizations
  301. * and missing or invalid title/pageids/revids.
  302. */
  303. private function outputGeneralPageInfo() {
  304. $pageSet = $this->getPageSet();
  305. $result = $this->getResult();
  306. // We can't really handle max-result-size failure here, but we need to
  307. // check anyway in case someone set the limit stupidly low.
  308. $fit = true;
  309. $values = $pageSet->getNormalizedTitlesAsResult( $result );
  310. if ( $values ) {
  311. $fit = $fit && $result->addValue( 'query', 'normalized', $values );
  312. }
  313. $values = $pageSet->getConvertedTitlesAsResult( $result );
  314. if ( $values ) {
  315. $fit = $fit && $result->addValue( 'query', 'converted', $values );
  316. }
  317. $values = $pageSet->getInterwikiTitlesAsResult( $result, $this->mParams['iwurl'] );
  318. if ( $values ) {
  319. $fit = $fit && $result->addValue( 'query', 'interwiki', $values );
  320. }
  321. $values = $pageSet->getRedirectTitlesAsResult( $result );
  322. if ( $values ) {
  323. $fit = $fit && $result->addValue( 'query', 'redirects', $values );
  324. }
  325. $values = $pageSet->getMissingRevisionIDsAsResult( $result );
  326. if ( $values ) {
  327. $fit = $fit && $result->addValue( 'query', 'badrevids', $values );
  328. }
  329. // Page elements
  330. $pages = [];
  331. // Report any missing titles
  332. foreach ( $pageSet->getMissingTitles() as $fakeId => $title ) {
  333. $vals = [];
  334. ApiQueryBase::addTitleInfo( $vals, $title );
  335. $vals['missing'] = true;
  336. if ( $title->isKnown() ) {
  337. $vals['known'] = true;
  338. }
  339. $pages[$fakeId] = $vals;
  340. }
  341. // Report any invalid titles
  342. foreach ( $pageSet->getInvalidTitlesAndReasons() as $fakeId => $data ) {
  343. $pages[$fakeId] = $data + [ 'invalid' => true ];
  344. }
  345. // Report any missing page ids
  346. foreach ( $pageSet->getMissingPageIDs() as $pageid ) {
  347. $pages[$pageid] = [
  348. 'pageid' => $pageid,
  349. 'missing' => true,
  350. ];
  351. }
  352. // Report special pages
  353. /** @var Title $title */
  354. foreach ( $pageSet->getSpecialTitles() as $fakeId => $title ) {
  355. $vals = [];
  356. ApiQueryBase::addTitleInfo( $vals, $title );
  357. $vals['special'] = true;
  358. if ( !$title->isKnown() ) {
  359. $vals['missing'] = true;
  360. }
  361. $pages[$fakeId] = $vals;
  362. }
  363. // Output general page information for found titles
  364. foreach ( $pageSet->getGoodTitles() as $pageid => $title ) {
  365. $vals = [];
  366. $vals['pageid'] = $pageid;
  367. ApiQueryBase::addTitleInfo( $vals, $title );
  368. $pages[$pageid] = $vals;
  369. }
  370. if ( count( $pages ) ) {
  371. $pageSet->populateGeneratorData( $pages );
  372. ApiResult::setArrayType( $pages, 'BCarray' );
  373. if ( $this->mParams['indexpageids'] ) {
  374. $pageIDs = array_keys( ApiResult::stripMetadataNonRecursive( $pages ) );
  375. // json treats all map keys as strings - converting to match
  376. $pageIDs = array_map( 'strval', $pageIDs );
  377. ApiResult::setIndexedTagName( $pageIDs, 'id' );
  378. $fit = $fit && $result->addValue( 'query', 'pageids', $pageIDs );
  379. }
  380. ApiResult::setIndexedTagName( $pages, 'page' );
  381. $fit = $fit && $result->addValue( 'query', 'pages', $pages );
  382. }
  383. if ( !$fit ) {
  384. $this->dieWithError( 'apierror-badconfig-resulttoosmall', 'badconfig' );
  385. }
  386. if ( $this->mParams['export'] ) {
  387. $this->doExport( $pageSet, $result );
  388. }
  389. }
  390. /**
  391. * @param ApiPageSet $pageSet Pages to be exported
  392. * @param ApiResult $result Result to output to
  393. */
  394. private function doExport( $pageSet, $result ) {
  395. $exportTitles = [];
  396. $titles = $pageSet->getGoodTitles();
  397. if ( count( $titles ) ) {
  398. /** @var Title $title */
  399. foreach ( $titles as $title ) {
  400. if ( $this->getPermissionManager()->userCan( 'read', $this->getUser(), $title ) ) {
  401. $exportTitles[] = $title;
  402. }
  403. }
  404. }
  405. $exporter = new WikiExporter( $this->getDB() );
  406. $sink = new DumpStringOutput;
  407. $exporter->setOutputSink( $sink );
  408. $exporter->setSchemaVersion( $this->mParams['exportschema'] );
  409. $exporter->openStream();
  410. foreach ( $exportTitles as $title ) {
  411. $exporter->pageByTitle( $title );
  412. }
  413. $exporter->closeStream();
  414. // Don't check the size of exported stuff
  415. // It's not continuable, so it would cause more
  416. // problems than it'd solve
  417. if ( $this->mParams['exportnowrap'] ) {
  418. $result->reset();
  419. // Raw formatter will handle this
  420. $result->addValue( null, 'text', $sink, ApiResult::NO_SIZE_CHECK );
  421. $result->addValue( null, 'mime', 'text/xml', ApiResult::NO_SIZE_CHECK );
  422. $result->addValue( null, 'filename', 'export.xml', ApiResult::NO_SIZE_CHECK );
  423. } else {
  424. $result->addValue( 'query', 'export', $sink, ApiResult::NO_SIZE_CHECK );
  425. $result->addValue( 'query', ApiResult::META_BC_SUBELEMENTS, [ 'export' ] );
  426. }
  427. }
  428. public function getAllowedParams( $flags = 0 ) {
  429. $result = [
  430. 'prop' => [
  431. ApiBase::PARAM_ISMULTI => true,
  432. ApiBase::PARAM_TYPE => 'submodule',
  433. ],
  434. 'list' => [
  435. ApiBase::PARAM_ISMULTI => true,
  436. ApiBase::PARAM_TYPE => 'submodule',
  437. ],
  438. 'meta' => [
  439. ApiBase::PARAM_ISMULTI => true,
  440. ApiBase::PARAM_TYPE => 'submodule',
  441. ],
  442. 'indexpageids' => false,
  443. 'export' => false,
  444. 'exportnowrap' => false,
  445. 'exportschema' => [
  446. ApiBase::PARAM_DFLT => WikiExporter::schemaVersion(),
  447. ApiBase::PARAM_TYPE => XmlDumpWriter::$supportedSchemas,
  448. ],
  449. 'iwurl' => false,
  450. 'continue' => [
  451. ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
  452. ],
  453. 'rawcontinue' => false,
  454. ];
  455. if ( $flags ) {
  456. $result += $this->getPageSet()->getFinalParams( $flags );
  457. }
  458. return $result;
  459. }
  460. public function isReadMode() {
  461. // We need to make an exception for certain meta modules that should be
  462. // accessible even without the 'read' right. Restrict the exception as
  463. // much as possible: no other modules allowed, and no pageset
  464. // parameters either. We do allow the 'rawcontinue' and 'indexpageids'
  465. // parameters since frameworks might add these unconditionally and they
  466. // can't expose anything here.
  467. $this->mParams = $this->extractRequestParams();
  468. $params = array_filter(
  469. array_diff_key(
  470. $this->mParams + $this->getPageSet()->extractRequestParams(),
  471. [ 'rawcontinue' => 1, 'indexpageids' => 1 ]
  472. )
  473. );
  474. if ( array_keys( $params ) !== [ 'meta' ] ) {
  475. return true;
  476. }
  477. // Ask each module if it requires read mode. Any true => this returns
  478. // true.
  479. $modules = [];
  480. $this->instantiateModules( $modules, 'meta' );
  481. foreach ( $modules as $module ) {
  482. if ( $module->isReadMode() ) {
  483. return true;
  484. }
  485. }
  486. return false;
  487. }
  488. protected function getExamplesMessages() {
  489. return [
  490. 'action=query&prop=revisions&meta=siteinfo&' .
  491. 'titles=Main%20Page&rvprop=user|comment&continue='
  492. => 'apihelp-query-example-revisions',
  493. 'action=query&generator=allpages&gapprefix=API/&prop=revisions&continue='
  494. => 'apihelp-query-example-allpages',
  495. ];
  496. }
  497. public function getHelpUrls() {
  498. return [
  499. 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Query',
  500. 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Meta',
  501. 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Properties',
  502. 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Lists',
  503. ];
  504. }
  505. }