ApiQuery.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. <?php
  2. /*
  3. * Created on Sep 7, 2006
  4. *
  5. * API for MediaWiki 1.8+
  6. *
  7. * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
  8. *
  9. * This program is free software; you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License as published by
  11. * the Free Software Foundation; either version 2 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License along
  20. * with this program; if not, write to the Free Software Foundation, Inc.,
  21. * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  22. * http://www.gnu.org/copyleft/gpl.html
  23. */
  24. if (!defined('MEDIAWIKI')) {
  25. // Eclipse helper - will be ignored in production
  26. require_once ('ApiBase.php');
  27. }
  28. /**
  29. * This is the main query class. It behaves similar to ApiMain: based on the
  30. * parameters given, it will create a list of titles to work on (an ApiPageSet
  31. * object), instantiate and execute various property/list/meta modules, and
  32. * assemble all resulting data into a single ApiResult object.
  33. *
  34. * In generator mode, a generator will be executed first to populate a second
  35. * ApiPageSet object, and that object will be used for all subsequent modules.
  36. *
  37. * @ingroup API
  38. */
  39. class ApiQuery extends ApiBase {
  40. private $mPropModuleNames, $mListModuleNames, $mMetaModuleNames;
  41. private $mPageSet;
  42. private $params, $redirect;
  43. private $mQueryPropModules = array (
  44. 'info' => 'ApiQueryInfo',
  45. 'revisions' => 'ApiQueryRevisions',
  46. 'links' => 'ApiQueryLinks',
  47. 'langlinks' => 'ApiQueryLangLinks',
  48. 'images' => 'ApiQueryImages',
  49. 'imageinfo' => 'ApiQueryImageInfo',
  50. 'templates' => 'ApiQueryLinks',
  51. 'categories' => 'ApiQueryCategories',
  52. 'extlinks' => 'ApiQueryExternalLinks',
  53. 'categoryinfo' => 'ApiQueryCategoryInfo',
  54. 'duplicatefiles' => 'ApiQueryDuplicateFiles',
  55. );
  56. private $mQueryListModules = array (
  57. 'allimages' => 'ApiQueryAllimages',
  58. 'allpages' => 'ApiQueryAllpages',
  59. 'alllinks' => 'ApiQueryAllLinks',
  60. 'allcategories' => 'ApiQueryAllCategories',
  61. 'allusers' => 'ApiQueryAllUsers',
  62. 'backlinks' => 'ApiQueryBacklinks',
  63. 'blocks' => 'ApiQueryBlocks',
  64. 'categorymembers' => 'ApiQueryCategoryMembers',
  65. 'deletedrevs' => 'ApiQueryDeletedrevs',
  66. 'embeddedin' => 'ApiQueryBacklinks',
  67. 'imageusage' => 'ApiQueryBacklinks',
  68. 'logevents' => 'ApiQueryLogEvents',
  69. 'recentchanges' => 'ApiQueryRecentChanges',
  70. 'search' => 'ApiQuerySearch',
  71. 'usercontribs' => 'ApiQueryContributions',
  72. 'watchlist' => 'ApiQueryWatchlist',
  73. 'watchlistraw' => 'ApiQueryWatchlistRaw',
  74. 'exturlusage' => 'ApiQueryExtLinksUsage',
  75. 'users' => 'ApiQueryUsers',
  76. 'random' => 'ApiQueryRandom',
  77. 'protectedtitles' => 'ApiQueryProtectedTitles',
  78. );
  79. private $mQueryMetaModules = array (
  80. 'siteinfo' => 'ApiQuerySiteinfo',
  81. 'userinfo' => 'ApiQueryUserInfo',
  82. 'allmessages' => 'ApiQueryAllmessages',
  83. );
  84. private $mSlaveDB = null;
  85. private $mNamedDB = array();
  86. public function __construct($main, $action) {
  87. parent :: __construct($main, $action);
  88. // Allow custom modules to be added in LocalSettings.php
  89. global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules;
  90. self :: appendUserModules($this->mQueryPropModules, $wgAPIPropModules);
  91. self :: appendUserModules($this->mQueryListModules, $wgAPIListModules);
  92. self :: appendUserModules($this->mQueryMetaModules, $wgAPIMetaModules);
  93. $this->mPropModuleNames = array_keys($this->mQueryPropModules);
  94. $this->mListModuleNames = array_keys($this->mQueryListModules);
  95. $this->mMetaModuleNames = array_keys($this->mQueryMetaModules);
  96. // Allow the entire list of modules at first,
  97. // but during module instantiation check if it can be used as a generator.
  98. $this->mAllowedGenerators = array_merge($this->mListModuleNames, $this->mPropModuleNames);
  99. }
  100. /**
  101. * Helper function to append any add-in modules to the list
  102. * @param $modules array Module array
  103. * @param $newModules array Module array to add to $modules
  104. */
  105. private static function appendUserModules(&$modules, $newModules) {
  106. if (is_array( $newModules )) {
  107. foreach ( $newModules as $moduleName => $moduleClass) {
  108. $modules[$moduleName] = $moduleClass;
  109. }
  110. }
  111. }
  112. /**
  113. * Gets a default slave database connection object
  114. * @return Database
  115. */
  116. public function getDB() {
  117. if (!isset ($this->mSlaveDB)) {
  118. $this->profileDBIn();
  119. $this->mSlaveDB = wfGetDB(DB_SLAVE,'api');
  120. $this->profileDBOut();
  121. }
  122. return $this->mSlaveDB;
  123. }
  124. /**
  125. * Get the query database connection with the given name.
  126. * If no such connection has been requested before, it will be created.
  127. * Subsequent calls with the same $name will return the same connection
  128. * as the first, regardless of the values of $db and $groups
  129. * @param $name string Name to assign to the database connection
  130. * @param $db int One of the DB_* constants
  131. * @param $groups array Query groups
  132. * @return Database
  133. */
  134. public function getNamedDB($name, $db, $groups) {
  135. if (!array_key_exists($name, $this->mNamedDB)) {
  136. $this->profileDBIn();
  137. $this->mNamedDB[$name] = wfGetDB($db, $groups);
  138. $this->profileDBOut();
  139. }
  140. return $this->mNamedDB[$name];
  141. }
  142. /**
  143. * Gets the set of pages the user has requested (or generated)
  144. * @return ApiPageSet
  145. */
  146. public function getPageSet() {
  147. return $this->mPageSet;
  148. }
  149. /**
  150. * Get the array mapping module names to class names
  151. * @return array(modulename => classname)
  152. */
  153. function getModules() {
  154. return array_merge($this->mQueryPropModules, $this->mQueryListModules, $this->mQueryMetaModules);
  155. }
  156. public function getCustomPrinter() {
  157. // If &exportnowrap is set, use the raw formatter
  158. if ($this->getParameter('export') &&
  159. $this->getParameter('exportnowrap'))
  160. return new ApiFormatRaw($this->getMain(),
  161. $this->getMain()->createPrinterByName('xml'));
  162. else
  163. return null;
  164. }
  165. /**
  166. * Query execution happens in the following steps:
  167. * #1 Create a PageSet object with any pages requested by the user
  168. * #2 If using a generator, execute it to get a new ApiPageSet object
  169. * #3 Instantiate all requested modules.
  170. * This way the PageSet object will know what shared data is required,
  171. * and minimize DB calls.
  172. * #4 Output all normalization and redirect resolution information
  173. * #5 Execute all requested modules
  174. */
  175. public function execute() {
  176. $this->params = $this->extractRequestParams();
  177. $this->redirects = $this->params['redirects'];
  178. //
  179. // Create PageSet
  180. //
  181. $this->mPageSet = new ApiPageSet($this, $this->redirects);
  182. //
  183. // Instantiate requested modules
  184. //
  185. $modules = array ();
  186. $this->InstantiateModules($modules, 'prop', $this->mQueryPropModules);
  187. $this->InstantiateModules($modules, 'list', $this->mQueryListModules);
  188. $this->InstantiateModules($modules, 'meta', $this->mQueryMetaModules);
  189. //
  190. // If given, execute generator to substitute user supplied data with generated data.
  191. //
  192. if (isset ($this->params['generator'])) {
  193. $this->executeGeneratorModule($this->params['generator'], $modules);
  194. } else {
  195. // Append custom fields and populate page/revision information
  196. $this->addCustomFldsToPageSet($modules, $this->mPageSet);
  197. $this->mPageSet->execute();
  198. }
  199. //
  200. // Record page information (title, namespace, if exists, etc)
  201. //
  202. $this->outputGeneralPageInfo();
  203. //
  204. // Execute all requested modules.
  205. //
  206. foreach ($modules as $module) {
  207. $module->profileIn();
  208. $module->execute();
  209. wfRunHooks('APIQueryAfterExecute', array(&$module));
  210. $module->profileOut();
  211. }
  212. }
  213. /**
  214. * Query modules may optimize data requests through the $this->getPageSet() object
  215. * by adding extra fields from the page table.
  216. * This function will gather all the extra request fields from the modules.
  217. * @param $modules array of module objects
  218. * @param $pageSet ApiPageSet
  219. */
  220. private function addCustomFldsToPageSet($modules, $pageSet) {
  221. // Query all requested modules.
  222. foreach ($modules as $module) {
  223. $module->requestExtraData($pageSet);
  224. }
  225. }
  226. /**
  227. * Create instances of all modules requested by the client
  228. * @param $modules array to append instatiated modules to
  229. * @param $param string Parameter name to read modules from
  230. * @param $moduleList array(modulename => classname)
  231. */
  232. private function InstantiateModules(&$modules, $param, $moduleList) {
  233. $list = @$this->params[$param];
  234. if (!is_null ($list))
  235. foreach ($list as $moduleName)
  236. $modules[] = new $moduleList[$moduleName] ($this, $moduleName);
  237. }
  238. /**
  239. * Appends an element for each page in the current pageSet with the
  240. * most general information (id, title), plus any title normalizations
  241. * and missing or invalid title/pageids/revids.
  242. */
  243. private function outputGeneralPageInfo() {
  244. $pageSet = $this->getPageSet();
  245. $result = $this->getResult();
  246. # We don't check for a full result set here because we can't be adding
  247. # more than 380K. The maximum revision size is in the megabyte range,
  248. # and the maximum result size must be even higher than that.
  249. // Title normalizations
  250. $normValues = array ();
  251. foreach ($pageSet->getNormalizedTitles() as $rawTitleStr => $titleStr) {
  252. $normValues[] = array (
  253. 'from' => $rawTitleStr,
  254. 'to' => $titleStr
  255. );
  256. }
  257. if (count($normValues)) {
  258. $result->setIndexedTagName($normValues, 'n');
  259. $result->addValue('query', 'normalized', $normValues);
  260. }
  261. // Interwiki titles
  262. $intrwValues = array ();
  263. foreach ($pageSet->getInterwikiTitles() as $rawTitleStr => $interwikiStr) {
  264. $intrwValues[] = array (
  265. 'title' => $rawTitleStr,
  266. 'iw' => $interwikiStr
  267. );
  268. }
  269. if (count($intrwValues)) {
  270. $result->setIndexedTagName($intrwValues, 'i');
  271. $result->addValue('query', 'interwiki', $intrwValues);
  272. }
  273. // Show redirect information
  274. $redirValues = array ();
  275. foreach ($pageSet->getRedirectTitles() as $titleStrFrom => $titleStrTo) {
  276. $redirValues[] = array (
  277. 'from' => strval($titleStrFrom),
  278. 'to' => $titleStrTo
  279. );
  280. }
  281. if (count($redirValues)) {
  282. $result->setIndexedTagName($redirValues, 'r');
  283. $result->addValue('query', 'redirects', $redirValues);
  284. }
  285. //
  286. // Missing revision elements
  287. //
  288. $missingRevIDs = $pageSet->getMissingRevisionIDs();
  289. if (count($missingRevIDs)) {
  290. $revids = array ();
  291. foreach ($missingRevIDs as $revid) {
  292. $revids[$revid] = array (
  293. 'revid' => $revid
  294. );
  295. }
  296. $result->setIndexedTagName($revids, 'rev');
  297. $result->addValue('query', 'badrevids', $revids);
  298. }
  299. //
  300. // Page elements
  301. //
  302. $pages = array ();
  303. // Report any missing titles
  304. foreach ($pageSet->getMissingTitles() as $fakeId => $title) {
  305. $vals = array();
  306. ApiQueryBase :: addTitleInfo($vals, $title);
  307. $vals['missing'] = '';
  308. $pages[$fakeId] = $vals;
  309. }
  310. // Report any invalid titles
  311. foreach ($pageSet->getInvalidTitles() as $fakeId => $title)
  312. $pages[$fakeId] = array('title' => $title, 'invalid' => '');
  313. // Report any missing page ids
  314. foreach ($pageSet->getMissingPageIDs() as $pageid) {
  315. $pages[$pageid] = array (
  316. 'pageid' => $pageid,
  317. 'missing' => ''
  318. );
  319. }
  320. // Output general page information for found titles
  321. foreach ($pageSet->getGoodTitles() as $pageid => $title) {
  322. $vals = array();
  323. $vals['pageid'] = $pageid;
  324. ApiQueryBase :: addTitleInfo($vals, $title);
  325. $pages[$pageid] = $vals;
  326. }
  327. if (count($pages)) {
  328. if ($this->params['indexpageids']) {
  329. $pageIDs = array_keys($pages);
  330. // json treats all map keys as strings - converting to match
  331. $pageIDs = array_map('strval', $pageIDs);
  332. $result->setIndexedTagName($pageIDs, 'id');
  333. $result->addValue('query', 'pageids', $pageIDs);
  334. }
  335. $result->setIndexedTagName($pages, 'page');
  336. $result->addValue('query', 'pages', $pages);
  337. }
  338. if ($this->params['export']) {
  339. $exporter = new WikiExporter($this->getDB());
  340. // WikiExporter writes to stdout, so catch its
  341. // output with an ob
  342. ob_start();
  343. $exporter->openStream();
  344. foreach (@$pageSet->getGoodTitles() as $title)
  345. if ($title->userCanRead())
  346. $exporter->pageByTitle($title);
  347. $exporter->closeStream();
  348. $exportxml = ob_get_contents();
  349. ob_end_clean();
  350. // Don't check the size of exported stuff
  351. // It's not continuable, so it would cause more
  352. // problems than it'd solve
  353. $result->disableSizeCheck();
  354. if ($this->params['exportnowrap']) {
  355. $result->reset();
  356. // Raw formatter will handle this
  357. $result->addValue(null, 'text', $exportxml);
  358. $result->addValue(null, 'mime', 'text/xml');
  359. } else {
  360. $r = array();
  361. ApiResult::setContent($r, $exportxml);
  362. $result->addValue('query', 'export', $r);
  363. }
  364. $result->enableSizeCheck();
  365. }
  366. }
  367. /**
  368. * For generator mode, execute generator, and use its output as new
  369. * ApiPageSet
  370. * @param $generatorName string Module name
  371. * @param $modules array of module objects
  372. */
  373. protected function executeGeneratorModule($generatorName, $modules) {
  374. // Find class that implements requested generator
  375. if (isset ($this->mQueryListModules[$generatorName])) {
  376. $className = $this->mQueryListModules[$generatorName];
  377. } elseif (isset ($this->mQueryPropModules[$generatorName])) {
  378. $className = $this->mQueryPropModules[$generatorName];
  379. } else {
  380. ApiBase :: dieDebug(__METHOD__, "Unknown generator=$generatorName");
  381. }
  382. // Generator results
  383. $resultPageSet = new ApiPageSet($this, $this->redirects);
  384. // Create and execute the generator
  385. $generator = new $className ($this, $generatorName);
  386. if (!$generator instanceof ApiQueryGeneratorBase)
  387. $this->dieUsage("Module $generatorName cannot be used as a generator", "badgenerator");
  388. $generator->setGeneratorMode();
  389. // Add any additional fields modules may need
  390. $generator->requestExtraData($this->mPageSet);
  391. $this->addCustomFldsToPageSet($modules, $resultPageSet);
  392. // Populate page information with the original user input
  393. $this->mPageSet->execute();
  394. // populate resultPageSet with the generator output
  395. $generator->profileIn();
  396. $generator->executeGenerator($resultPageSet);
  397. wfRunHooks('APIQueryGeneratorAfterExecute', array(&$generator, &$resultPageSet));
  398. $resultPageSet->finishPageSetGeneration();
  399. $generator->profileOut();
  400. // Swap the resulting pageset back in
  401. $this->mPageSet = $resultPageSet;
  402. }
  403. public function getAllowedParams() {
  404. return array (
  405. 'prop' => array (
  406. ApiBase :: PARAM_ISMULTI => true,
  407. ApiBase :: PARAM_TYPE => $this->mPropModuleNames
  408. ),
  409. 'list' => array (
  410. ApiBase :: PARAM_ISMULTI => true,
  411. ApiBase :: PARAM_TYPE => $this->mListModuleNames
  412. ),
  413. 'meta' => array (
  414. ApiBase :: PARAM_ISMULTI => true,
  415. ApiBase :: PARAM_TYPE => $this->mMetaModuleNames
  416. ),
  417. 'generator' => array (
  418. ApiBase :: PARAM_TYPE => $this->mAllowedGenerators
  419. ),
  420. 'redirects' => false,
  421. 'indexpageids' => false,
  422. 'export' => false,
  423. 'exportnowrap' => false,
  424. );
  425. }
  426. /**
  427. * Override the parent to generate help messages for all available query modules.
  428. * @return string
  429. */
  430. public function makeHelpMsg() {
  431. $msg = '';
  432. // Make sure the internal object is empty
  433. // (just in case a sub-module decides to optimize during instantiation)
  434. $this->mPageSet = null;
  435. $this->mAllowedGenerators = array(); // Will be repopulated
  436. $astriks = str_repeat('--- ', 8);
  437. $astriks2 = str_repeat('*** ', 10);
  438. $msg .= "\n$astriks Query: Prop $astriks\n\n";
  439. $msg .= $this->makeHelpMsgHelper($this->mQueryPropModules, 'prop');
  440. $msg .= "\n$astriks Query: List $astriks\n\n";
  441. $msg .= $this->makeHelpMsgHelper($this->mQueryListModules, 'list');
  442. $msg .= "\n$astriks Query: Meta $astriks\n\n";
  443. $msg .= $this->makeHelpMsgHelper($this->mQueryMetaModules, 'meta');
  444. $msg .= "\n\n$astriks2 Modules: continuation $astriks2\n\n";
  445. // Perform the base call last because the $this->mAllowedGenerators
  446. // will be updated inside makeHelpMsgHelper()
  447. // Use parent to make default message for the query module
  448. $msg = parent :: makeHelpMsg() . $msg;
  449. return $msg;
  450. }
  451. /**
  452. * For all modules in $moduleList, generate help messages and join them together
  453. * @param $moduleList array(modulename => classname)
  454. * @param $paramName string Parameter name
  455. * @return string
  456. */
  457. private function makeHelpMsgHelper($moduleList, $paramName) {
  458. $moduleDescriptions = array ();
  459. foreach ($moduleList as $moduleName => $moduleClass) {
  460. $module = new $moduleClass ($this, $moduleName, null);
  461. $msg = ApiMain::makeHelpMsgHeader($module, $paramName);
  462. $msg2 = $module->makeHelpMsg();
  463. if ($msg2 !== false)
  464. $msg .= $msg2;
  465. if ($module instanceof ApiQueryGeneratorBase) {
  466. $this->mAllowedGenerators[] = $moduleName;
  467. $msg .= "Generator:\n This module may be used as a generator\n";
  468. }
  469. $moduleDescriptions[] = $msg;
  470. }
  471. return implode("\n", $moduleDescriptions);
  472. }
  473. /**
  474. * Override to add extra parameters from PageSet
  475. * @return string
  476. */
  477. public function makeHelpMsgParameters() {
  478. $psModule = new ApiPageSet($this);
  479. return $psModule->makeHelpMsgParameters() . parent :: makeHelpMsgParameters();
  480. }
  481. public function shouldCheckMaxlag() {
  482. return true;
  483. }
  484. public function getParamDescription() {
  485. return array (
  486. 'prop' => 'Which properties to get for the titles/revisions/pageids',
  487. 'list' => 'Which lists to get',
  488. 'meta' => 'Which meta data to get about the site',
  489. 'generator' => array('Use the output of a list as the input for other prop/list/meta items',
  490. 'NOTE: generator parameter names must be prefixed with a \'g\', see examples.'),
  491. 'redirects' => 'Automatically resolve redirects',
  492. 'indexpageids' => 'Include an additional pageids section listing all returned page IDs.',
  493. 'export' => 'Export the current revisions of all given or generated pages',
  494. 'exportnowrap' => 'Return the export XML without wrapping it in an XML result (same format as Special:Export). Can only be used with export',
  495. );
  496. }
  497. public function getDescription() {
  498. return array (
  499. 'Query API module allows applications to get needed pieces of data from the MediaWiki databases,',
  500. 'and is loosely based on the old query.php interface.',
  501. 'All data modifications will first have to use query to acquire a token to prevent abuse from malicious sites.'
  502. );
  503. }
  504. protected function getExamples() {
  505. return array (
  506. 'api.php?action=query&prop=revisions&meta=siteinfo&titles=Main%20Page&rvprop=user|comment',
  507. 'api.php?action=query&generator=allpages&gapprefix=API/&prop=revisions',
  508. );
  509. }
  510. public function getVersion() {
  511. $psModule = new ApiPageSet($this);
  512. $vers = array ();
  513. $vers[] = __CLASS__ . ': $Id: ApiQuery.php 48629 2009-03-20 11:40:54Z catrope $';
  514. $vers[] = $psModule->getVersion();
  515. return $vers;
  516. }
  517. }