ApiQueryBase.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  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. use Wikimedia\Rdbms\IResultWrapper;
  25. /**
  26. * This is a base class for all Query modules.
  27. * It provides some common functionality such as constructing various SQL
  28. * queries.
  29. *
  30. * @ingroup API
  31. */
  32. abstract class ApiQueryBase extends ApiBase {
  33. use ApiQueryBlockInfoTrait;
  34. private $mQueryModule, $mDb, $tables, $where, $fields, $options, $join_conds;
  35. /**
  36. * @param ApiQuery $queryModule
  37. * @param string $moduleName
  38. * @param string $paramPrefix
  39. */
  40. public function __construct( ApiQuery $queryModule, $moduleName, $paramPrefix = '' ) {
  41. parent::__construct( $queryModule->getMain(), $moduleName, $paramPrefix );
  42. $this->mQueryModule = $queryModule;
  43. $this->mDb = null;
  44. $this->resetQueryParams();
  45. }
  46. /************************************************************************//**
  47. * @name Methods to implement
  48. * @{
  49. */
  50. /**
  51. * Get the cache mode for the data generated by this module. Override
  52. * this in the module subclass. For possible return values and other
  53. * details about cache modes, see ApiMain::setCacheMode()
  54. *
  55. * Public caching will only be allowed if *all* the modules that supply
  56. * data for a given request return a cache mode of public.
  57. *
  58. * @param array $params
  59. * @return string
  60. */
  61. public function getCacheMode( $params ) {
  62. return 'private';
  63. }
  64. /**
  65. * Override this method to request extra fields from the pageSet
  66. * using $pageSet->requestField('fieldName')
  67. *
  68. * Note this only makes sense for 'prop' modules, as 'list' and 'meta'
  69. * modules should not be using the pageset.
  70. *
  71. * @param ApiPageSet $pageSet
  72. */
  73. public function requestExtraData( $pageSet ) {
  74. }
  75. /** @} */
  76. /************************************************************************//**
  77. * @name Data access
  78. * @{
  79. */
  80. /**
  81. * Get the main Query module
  82. * @return ApiQuery
  83. */
  84. public function getQuery() {
  85. return $this->mQueryModule;
  86. }
  87. /** @inheritDoc */
  88. public function getParent() {
  89. return $this->getQuery();
  90. }
  91. /**
  92. * Get the Query database connection (read-only)
  93. * @return IDatabase
  94. */
  95. protected function getDB() {
  96. if ( is_null( $this->mDb ) ) {
  97. $this->mDb = $this->getQuery()->getDB();
  98. }
  99. return $this->mDb;
  100. }
  101. /**
  102. * Selects the query database connection with the given name.
  103. * See ApiQuery::getNamedDB() for more information
  104. * @param string $name Name to assign to the database connection
  105. * @param int $db One of the DB_* constants
  106. * @param string|string[] $groups Query groups
  107. * @return IDatabase
  108. */
  109. public function selectNamedDB( $name, $db, $groups ) {
  110. $this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups );
  111. return $this->mDb;
  112. }
  113. /**
  114. * Get the PageSet object to work on
  115. * @return ApiPageSet
  116. */
  117. protected function getPageSet() {
  118. return $this->getQuery()->getPageSet();
  119. }
  120. /** @} */
  121. /************************************************************************//**
  122. * @name Querying
  123. * @{
  124. */
  125. /**
  126. * Blank the internal arrays with query parameters
  127. */
  128. protected function resetQueryParams() {
  129. $this->tables = [];
  130. $this->where = [];
  131. $this->fields = [];
  132. $this->options = [];
  133. $this->join_conds = [];
  134. }
  135. /**
  136. * Add a set of tables to the internal array
  137. * @param string|array $tables Table name or array of table names
  138. * or nested arrays for joins using parentheses for grouping
  139. * @param string|null $alias Table alias, or null for no alias. Cannot be
  140. * used with multiple tables
  141. */
  142. protected function addTables( $tables, $alias = null ) {
  143. if ( is_array( $tables ) ) {
  144. if ( $alias !== null ) {
  145. ApiBase::dieDebug( __METHOD__, 'Multiple table aliases not supported' );
  146. }
  147. $this->tables = array_merge( $this->tables, $tables );
  148. } elseif ( $alias !== null ) {
  149. $this->tables[$alias] = $tables;
  150. } else {
  151. $this->tables[] = $tables;
  152. }
  153. }
  154. /**
  155. * Add a set of JOIN conditions to the internal array
  156. *
  157. * JOIN conditions are formatted as [ tablename => [ jointype, conditions ] ]
  158. * e.g. [ 'page' => [ 'LEFT JOIN', 'page_id=rev_page' ] ].
  159. * Conditions may be a string or an addWhere()-style array.
  160. * @param array $join_conds JOIN conditions
  161. */
  162. protected function addJoinConds( $join_conds ) {
  163. if ( !is_array( $join_conds ) ) {
  164. ApiBase::dieDebug( __METHOD__, 'Join conditions have to be arrays' );
  165. }
  166. $this->join_conds = array_merge( $this->join_conds, $join_conds );
  167. }
  168. /**
  169. * Add a set of fields to select to the internal array
  170. * @param array|string $value Field name or array of field names
  171. */
  172. protected function addFields( $value ) {
  173. if ( is_array( $value ) ) {
  174. $this->fields = array_merge( $this->fields, $value );
  175. } else {
  176. $this->fields[] = $value;
  177. }
  178. }
  179. /**
  180. * Same as addFields(), but add the fields only if a condition is met
  181. * @param array|string $value See addFields()
  182. * @param bool $condition If false, do nothing
  183. * @return bool $condition
  184. */
  185. protected function addFieldsIf( $value, $condition ) {
  186. if ( $condition ) {
  187. $this->addFields( $value );
  188. return true;
  189. }
  190. return false;
  191. }
  192. /**
  193. * Add a set of WHERE clauses to the internal array.
  194. * Clauses can be formatted as 'foo=bar' or [ 'foo' => 'bar' ],
  195. * the latter only works if the value is a constant (i.e. not another field)
  196. *
  197. * If $value is an empty array, this function does nothing.
  198. *
  199. * For example, [ 'foo=bar', 'baz' => 3, 'bla' => 'foo' ] translates
  200. * to "foo=bar AND baz='3' AND bla='foo'"
  201. * @param string|array $value
  202. */
  203. protected function addWhere( $value ) {
  204. if ( is_array( $value ) ) {
  205. // Sanity check: don't insert empty arrays,
  206. // Database::makeList() chokes on them
  207. if ( count( $value ) ) {
  208. $this->where = array_merge( $this->where, $value );
  209. }
  210. } else {
  211. $this->where[] = $value;
  212. }
  213. }
  214. /**
  215. * Same as addWhere(), but add the WHERE clauses only if a condition is met
  216. * @param string|array $value
  217. * @param bool $condition If false, do nothing
  218. * @return bool $condition
  219. */
  220. protected function addWhereIf( $value, $condition ) {
  221. if ( $condition ) {
  222. $this->addWhere( $value );
  223. return true;
  224. }
  225. return false;
  226. }
  227. /**
  228. * Equivalent to addWhere( [ $field => $value ] )
  229. * @param string $field Field name
  230. * @param string|string[] $value Value; ignored if null or empty array
  231. */
  232. protected function addWhereFld( $field, $value ) {
  233. if ( $value !== null && !( is_array( $value ) && !$value ) ) {
  234. $this->where[$field] = $value;
  235. }
  236. }
  237. /**
  238. * Like addWhereFld for an integer list of IDs
  239. * @since 1.33
  240. * @param string $table Table name
  241. * @param string $field Field name
  242. * @param int[] $ids IDs
  243. * @return int Count of IDs actually included
  244. */
  245. protected function addWhereIDsFld( $table, $field, $ids ) {
  246. // Use count() to its full documented capabilities to simultaneously
  247. // test for null, empty array or empty countable object
  248. if ( count( $ids ) ) {
  249. $ids = $this->filterIDs( [ [ $table, $field ] ], $ids );
  250. if ( $ids === [] ) {
  251. // Return nothing, no IDs are valid
  252. $this->where[] = '0 = 1';
  253. } else {
  254. $this->where[$field] = $ids;
  255. }
  256. }
  257. return count( $ids );
  258. }
  259. /**
  260. * Add a WHERE clause corresponding to a range, and an ORDER BY
  261. * clause to sort in the right direction
  262. * @param string $field Field name
  263. * @param string $dir If 'newer', sort in ascending order, otherwise
  264. * sort in descending order
  265. * @param string $start Value to start the list at. If $dir == 'newer'
  266. * this is the lower boundary, otherwise it's the upper boundary
  267. * @param string $end Value to end the list at. If $dir == 'newer' this
  268. * is the upper boundary, otherwise it's the lower boundary
  269. * @param bool $sort If false, don't add an ORDER BY clause
  270. */
  271. protected function addWhereRange( $field, $dir, $start, $end, $sort = true ) {
  272. $isDirNewer = ( $dir === 'newer' );
  273. $after = ( $isDirNewer ? '>=' : '<=' );
  274. $before = ( $isDirNewer ? '<=' : '>=' );
  275. $db = $this->getDB();
  276. if ( !is_null( $start ) ) {
  277. $this->addWhere( $field . $after . $db->addQuotes( $start ) );
  278. }
  279. if ( !is_null( $end ) ) {
  280. $this->addWhere( $field . $before . $db->addQuotes( $end ) );
  281. }
  282. if ( $sort ) {
  283. $order = $field . ( $isDirNewer ? '' : ' DESC' );
  284. // Append ORDER BY
  285. $optionOrderBy = isset( $this->options['ORDER BY'] )
  286. ? (array)$this->options['ORDER BY']
  287. : [];
  288. $optionOrderBy[] = $order;
  289. $this->addOption( 'ORDER BY', $optionOrderBy );
  290. }
  291. }
  292. /**
  293. * Add a WHERE clause corresponding to a range, similar to addWhereRange,
  294. * but converts $start and $end to database timestamps.
  295. * @see addWhereRange
  296. * @param string $field
  297. * @param string $dir
  298. * @param string $start
  299. * @param string $end
  300. * @param bool $sort
  301. */
  302. protected function addTimestampWhereRange( $field, $dir, $start, $end, $sort = true ) {
  303. $db = $this->getDB();
  304. $this->addWhereRange( $field, $dir,
  305. $db->timestampOrNull( $start ), $db->timestampOrNull( $end ), $sort );
  306. }
  307. /**
  308. * Add an option such as LIMIT or USE INDEX. If an option was set
  309. * before, the old value will be overwritten
  310. * @param string $name Option name
  311. * @param string|string[]|null $value Option value
  312. */
  313. protected function addOption( $name, $value = null ) {
  314. if ( is_null( $value ) ) {
  315. $this->options[] = $name;
  316. } else {
  317. $this->options[$name] = $value;
  318. }
  319. }
  320. /**
  321. * Execute a SELECT query based on the values in the internal arrays
  322. * @param string $method Function the query should be attributed to.
  323. * You should usually use __METHOD__ here
  324. * @param array $extraQuery Query data to add but not store in the object
  325. * Format is [
  326. * 'tables' => ...,
  327. * 'fields' => ...,
  328. * 'where' => ...,
  329. * 'options' => ...,
  330. * 'join_conds' => ...
  331. * ]
  332. * @param array|null &$hookData If set, the ApiQueryBaseBeforeQuery and
  333. * ApiQueryBaseAfterQuery hooks will be called, and the
  334. * ApiQueryBaseProcessRow hook will be expected.
  335. * @return IResultWrapper
  336. */
  337. protected function select( $method, $extraQuery = [], array &$hookData = null ) {
  338. $tables = array_merge(
  339. $this->tables,
  340. isset( $extraQuery['tables'] ) ? (array)$extraQuery['tables'] : []
  341. );
  342. $fields = array_merge(
  343. $this->fields,
  344. isset( $extraQuery['fields'] ) ? (array)$extraQuery['fields'] : []
  345. );
  346. $where = array_merge(
  347. $this->where,
  348. isset( $extraQuery['where'] ) ? (array)$extraQuery['where'] : []
  349. );
  350. $options = array_merge(
  351. $this->options,
  352. isset( $extraQuery['options'] ) ? (array)$extraQuery['options'] : []
  353. );
  354. $join_conds = array_merge(
  355. $this->join_conds,
  356. isset( $extraQuery['join_conds'] ) ? (array)$extraQuery['join_conds'] : []
  357. );
  358. if ( $hookData !== null ) {
  359. Hooks::run( 'ApiQueryBaseBeforeQuery',
  360. [ $this, &$tables, &$fields, &$where, &$options, &$join_conds, &$hookData ]
  361. );
  362. }
  363. $res = $this->getDB()->select( $tables, $fields, $where, $method, $options, $join_conds );
  364. if ( $hookData !== null ) {
  365. Hooks::run( 'ApiQueryBaseAfterQuery', [ $this, $res, &$hookData ] );
  366. }
  367. return $res;
  368. }
  369. /**
  370. * Call the ApiQueryBaseProcessRow hook
  371. *
  372. * Generally, a module that passed $hookData to self::select() will call
  373. * this just before calling ApiResult::addValue(), and treat a false return
  374. * here in the same way it treats a false return from addValue().
  375. *
  376. * @since 1.28
  377. * @param object $row Database row
  378. * @param array &$data Data to be added to the result
  379. * @param array &$hookData Hook data from ApiQueryBase::select()
  380. * @return bool Return false if row processing should end with continuation
  381. */
  382. protected function processRow( $row, array &$data, array &$hookData ) {
  383. return Hooks::run( 'ApiQueryBaseProcessRow', [ $this, $row, &$data, &$hookData ] );
  384. }
  385. /** @} */
  386. /************************************************************************//**
  387. * @name Utility methods
  388. * @{
  389. */
  390. /**
  391. * Add information (title and namespace) about a Title object to a
  392. * result array
  393. * @param array &$arr Result array à la ApiResult
  394. * @param Title $title
  395. * @param string $prefix Module prefix
  396. */
  397. public static function addTitleInfo( &$arr, $title, $prefix = '' ) {
  398. $arr[$prefix . 'ns'] = (int)$title->getNamespace();
  399. $arr[$prefix . 'title'] = $title->getPrefixedText();
  400. }
  401. /**
  402. * Add a sub-element under the page element with the given page ID
  403. * @param int $pageId Page ID
  404. * @param array $data Data array à la ApiResult
  405. * @return bool Whether the element fit in the result
  406. */
  407. protected function addPageSubItems( $pageId, $data ) {
  408. $result = $this->getResult();
  409. ApiResult::setIndexedTagName( $data, $this->getModulePrefix() );
  410. return $result->addValue( [ 'query', 'pages', (int)$pageId ],
  411. $this->getModuleName(),
  412. $data );
  413. }
  414. /**
  415. * Same as addPageSubItems(), but one element of $data at a time
  416. * @param int $pageId Page ID
  417. * @param mixed $item Data à la ApiResult
  418. * @param string|null $elemname XML element name. If null, getModuleName()
  419. * is used
  420. * @return bool Whether the element fit in the result
  421. */
  422. protected function addPageSubItem( $pageId, $item, $elemname = null ) {
  423. if ( is_null( $elemname ) ) {
  424. $elemname = $this->getModulePrefix();
  425. }
  426. $result = $this->getResult();
  427. $fit = $result->addValue( [ 'query', 'pages', $pageId,
  428. $this->getModuleName() ], null, $item );
  429. if ( !$fit ) {
  430. return false;
  431. }
  432. $result->addIndexedTagName( [ 'query', 'pages', $pageId,
  433. $this->getModuleName() ], $elemname );
  434. return true;
  435. }
  436. /**
  437. * Set a query-continue value
  438. * @param string $paramName Parameter name
  439. * @param string|array $paramValue Parameter value
  440. */
  441. protected function setContinueEnumParameter( $paramName, $paramValue ) {
  442. $this->getContinuationManager()->addContinueParam( $this, $paramName, $paramValue );
  443. }
  444. /**
  445. * Convert an input title or title prefix into a dbkey.
  446. *
  447. * $namespace should always be specified in order to handle per-namespace
  448. * capitalization settings.
  449. *
  450. * @param string $titlePart Title part
  451. * @param int $namespace Namespace of the title
  452. * @return string DBkey (no namespace prefix)
  453. */
  454. public function titlePartToKey( $titlePart, $namespace = NS_MAIN ) {
  455. $t = Title::makeTitleSafe( $namespace, $titlePart . 'x' );
  456. if ( !$t || $t->hasFragment() ) {
  457. // Invalid title (e.g. bad chars) or contained a '#'.
  458. $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
  459. }
  460. if ( $namespace != $t->getNamespace() || $t->isExternal() ) {
  461. // This can happen in two cases. First, if you call titlePartToKey with a title part
  462. // that looks like a namespace, but with $defaultNamespace = NS_MAIN. It would be very
  463. // difficult to handle such a case. Such cases cannot exist and are therefore treated
  464. // as invalid user input. The second case is when somebody specifies a title interwiki
  465. // prefix.
  466. $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
  467. }
  468. return substr( $t->getDBkey(), 0, -1 );
  469. }
  470. /**
  471. * Convert an input title or title prefix into a namespace constant and dbkey.
  472. *
  473. * @since 1.26
  474. * @param string $titlePart Title part
  475. * @param int $defaultNamespace Default namespace if none is given
  476. * @return array (int, string) Namespace number and DBkey
  477. */
  478. public function prefixedTitlePartToKey( $titlePart, $defaultNamespace = NS_MAIN ) {
  479. $t = Title::newFromText( $titlePart . 'x', $defaultNamespace );
  480. if ( !$t || $t->hasFragment() || $t->isExternal() ) {
  481. // Invalid title (e.g. bad chars) or contained a '#'.
  482. $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
  483. }
  484. return [ $t->getNamespace(), substr( $t->getDBkey(), 0, -1 ) ];
  485. }
  486. /**
  487. * @param string $hash
  488. * @return bool
  489. */
  490. public function validateSha1Hash( $hash ) {
  491. return (bool)preg_match( '/^[a-f0-9]{40}$/', $hash );
  492. }
  493. /**
  494. * @param string $hash
  495. * @return bool
  496. */
  497. public function validateSha1Base36Hash( $hash ) {
  498. return (bool)preg_match( '/^[a-z0-9]{31}$/', $hash );
  499. }
  500. /**
  501. * Check whether the current user has permission to view revision-deleted
  502. * fields.
  503. * @return bool
  504. */
  505. public function userCanSeeRevDel() {
  506. return $this->getPermissionManager()->userHasAnyRight(
  507. $this->getUser(),
  508. 'deletedhistory',
  509. 'deletedtext',
  510. 'suppressrevision',
  511. 'viewsuppressed'
  512. );
  513. }
  514. /**
  515. * Preprocess the result set to fill the GenderCache with the necessary information
  516. * before using self::addTitleInfo
  517. *
  518. * @param IResultWrapper $res Result set to work on.
  519. * The result set must have _namespace and _title fields with the provided field prefix
  520. * @param string $fname The caller function name, always use __METHOD__
  521. * @param string $fieldPrefix Prefix for fields to check gender for
  522. */
  523. protected function executeGenderCacheFromResultWrapper(
  524. IResultWrapper $res, $fname = __METHOD__, $fieldPrefix = 'page'
  525. ) {
  526. if ( !$res->numRows() ) {
  527. return;
  528. }
  529. $services = MediaWikiServices::getInstance();
  530. $nsInfo = $services->getNamespaceInfo();
  531. $namespaceField = $fieldPrefix . '_namespace';
  532. $titleField = $fieldPrefix . '_title';
  533. $usernames = [];
  534. foreach ( $res as $row ) {
  535. if ( $nsInfo->hasGenderDistinction( $row->$namespaceField ) ) {
  536. $usernames[] = $row->$titleField;
  537. }
  538. }
  539. if ( $usernames === [] ) {
  540. return;
  541. }
  542. $genderCache = $services->getGenderCache();
  543. $genderCache->doQuery( $usernames, $fname );
  544. }
  545. /** @} */
  546. /************************************************************************//**
  547. * @name Deprecated methods
  548. * @{
  549. */
  550. /**
  551. * Filters hidden users (where the user doesn't have the right to view them)
  552. * Also adds relevant block information
  553. *
  554. * @deprecated since 1.34, use ApiQueryBlockInfoTrait instead
  555. * @param bool $showBlockInfo
  556. * @return void
  557. */
  558. public function showHiddenUsersAddBlockInfo( $showBlockInfo ) {
  559. wfDeprecated( __METHOD__, '1.34' );
  560. return $this->addBlockInfoToQuery( $showBlockInfo );
  561. }
  562. /** @} */
  563. }