3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
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.
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.
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
23 use MediaWiki\MediaWikiServices
;
24 use Wikimedia\Rdbms\IDatabase
;
25 use Wikimedia\Rdbms\IResultWrapper
;
28 * This is a base class for all Query modules.
29 * It provides some common functionality such as constructing various SQL
34 abstract class ApiQueryBase
extends ApiBase
{
35 use ApiQueryBlockInfoTrait
;
37 private $mQueryModule, $mDb, $tables, $where, $fields, $options, $join_conds;
40 * @param ApiQuery $queryModule
41 * @param string $moduleName
42 * @param string $paramPrefix
44 public function __construct( ApiQuery
$queryModule, $moduleName, $paramPrefix = '' ) {
45 parent
::__construct( $queryModule->getMain(), $moduleName, $paramPrefix );
46 $this->mQueryModule
= $queryModule;
48 $this->resetQueryParams();
51 /************************************************************************//**
52 * @name Methods to implement
57 * Get the cache mode for the data generated by this module. Override
58 * this in the module subclass. For possible return values and other
59 * details about cache modes, see ApiMain::setCacheMode()
61 * Public caching will only be allowed if *all* the modules that supply
62 * data for a given request return a cache mode of public.
64 * @param array $params
67 public function getCacheMode( $params ) {
72 * Override this method to request extra fields from the pageSet
73 * using $pageSet->requestField('fieldName')
75 * Note this only makes sense for 'prop' modules, as 'list' and 'meta'
76 * modules should not be using the pageset.
78 * @param ApiPageSet $pageSet
80 public function requestExtraData( $pageSet ) {
85 /************************************************************************//**
91 * Get the main Query module
94 public function getQuery() {
95 return $this->mQueryModule
;
99 public function getParent() {
100 return $this->getQuery();
104 * Get the Query database connection (read-only)
107 protected function getDB() {
108 if ( is_null( $this->mDb
) ) {
109 $this->mDb
= $this->getQuery()->getDB();
116 * Selects the query database connection with the given name.
117 * See ApiQuery::getNamedDB() for more information
118 * @param string $name Name to assign to the database connection
119 * @param int $db One of the DB_* constants
120 * @param string|string[] $groups Query groups
123 public function selectNamedDB( $name, $db, $groups ) {
124 $this->mDb
= $this->getQuery()->getNamedDB( $name, $db, $groups );
129 * Get the PageSet object to work on
132 protected function getPageSet() {
133 return $this->getQuery()->getPageSet();
138 /************************************************************************//**
144 * Blank the internal arrays with query parameters
146 protected function resetQueryParams() {
151 $this->join_conds
= [];
155 * Add a set of tables to the internal array
156 * @param string|array $tables Table name or array of table names
157 * or nested arrays for joins using parentheses for grouping
158 * @param string|null $alias Table alias, or null for no alias. Cannot be
159 * used with multiple tables
161 protected function addTables( $tables, $alias = null ) {
162 if ( is_array( $tables ) ) {
163 if ( $alias !== null ) {
164 ApiBase
::dieDebug( __METHOD__
, 'Multiple table aliases not supported' );
166 $this->tables
= array_merge( $this->tables
, $tables );
167 } elseif ( $alias !== null ) {
168 $this->tables
[$alias] = $tables;
170 $this->tables
[] = $tables;
175 * Add a set of JOIN conditions to the internal array
177 * JOIN conditions are formatted as [ tablename => [ jointype, conditions ] ]
178 * e.g. [ 'page' => [ 'LEFT JOIN', 'page_id=rev_page' ] ].
179 * Conditions may be a string or an addWhere()-style array.
180 * @param array $join_conds JOIN conditions
182 protected function addJoinConds( $join_conds ) {
183 if ( !is_array( $join_conds ) ) {
184 ApiBase
::dieDebug( __METHOD__
, 'Join conditions have to be arrays' );
186 $this->join_conds
= array_merge( $this->join_conds
, $join_conds );
190 * Add a set of fields to select to the internal array
191 * @param array|string $value Field name or array of field names
193 protected function addFields( $value ) {
194 if ( is_array( $value ) ) {
195 $this->fields
= array_merge( $this->fields
, $value );
197 $this->fields
[] = $value;
202 * Same as addFields(), but add the fields only if a condition is met
203 * @param array|string $value See addFields()
204 * @param bool $condition If false, do nothing
205 * @return bool $condition
207 protected function addFieldsIf( $value, $condition ) {
209 $this->addFields( $value );
218 * Add a set of WHERE clauses to the internal array.
219 * Clauses can be formatted as 'foo=bar' or [ 'foo' => 'bar' ],
220 * the latter only works if the value is a constant (i.e. not another field)
222 * If $value is an empty array, this function does nothing.
224 * For example, [ 'foo=bar', 'baz' => 3, 'bla' => 'foo' ] translates
225 * to "foo=bar AND baz='3' AND bla='foo'"
226 * @param string|array $value
228 protected function addWhere( $value ) {
229 if ( is_array( $value ) ) {
230 // Sanity check: don't insert empty arrays,
231 // Database::makeList() chokes on them
232 if ( count( $value ) ) {
233 $this->where
= array_merge( $this->where
, $value );
236 $this->where
[] = $value;
241 * Same as addWhere(), but add the WHERE clauses only if a condition is met
242 * @param string|array $value
243 * @param bool $condition If false, do nothing
244 * @return bool $condition
246 protected function addWhereIf( $value, $condition ) {
248 $this->addWhere( $value );
257 * Equivalent to addWhere( [ $field => $value ] )
258 * @param string $field Field name
259 * @param string|string[] $value Value; ignored if null or empty array
261 protected function addWhereFld( $field, $value ) {
262 if ( $value !== null && !( is_array( $value ) && !$value ) ) {
263 $this->where
[$field] = $value;
268 * Like addWhereFld for an integer list of IDs
270 * @param string $table Table name
271 * @param string $field Field name
272 * @param int[] $ids IDs
273 * @return int Count of IDs actually included
275 protected function addWhereIDsFld( $table, $field, $ids ) {
276 // Use count() to its full documented capabilities to simultaneously
277 // test for null, empty array or empty countable object
278 if ( count( $ids ) ) {
279 $ids = $this->filterIDs( [ [ $table, $field ] ], $ids );
282 // Return nothing, no IDs are valid
283 $this->where
[] = '0 = 1';
285 $this->where
[$field] = $ids;
288 return count( $ids );
292 * Add a WHERE clause corresponding to a range, and an ORDER BY
293 * clause to sort in the right direction
294 * @param string $field Field name
295 * @param string $dir If 'newer', sort in ascending order, otherwise
296 * sort in descending order
297 * @param string $start Value to start the list at. If $dir == 'newer'
298 * this is the lower boundary, otherwise it's the upper boundary
299 * @param string $end Value to end the list at. If $dir == 'newer' this
300 * is the upper boundary, otherwise it's the lower boundary
301 * @param bool $sort If false, don't add an ORDER BY clause
303 protected function addWhereRange( $field, $dir, $start, $end, $sort = true ) {
304 $isDirNewer = ( $dir === 'newer' );
305 $after = ( $isDirNewer ?
'>=' : '<=' );
306 $before = ( $isDirNewer ?
'<=' : '>=' );
307 $db = $this->getDB();
309 if ( !is_null( $start ) ) {
310 $this->addWhere( $field . $after . $db->addQuotes( $start ) );
313 if ( !is_null( $end ) ) {
314 $this->addWhere( $field . $before . $db->addQuotes( $end ) );
318 $order = $field . ( $isDirNewer ?
'' : ' DESC' );
320 $optionOrderBy = isset( $this->options
['ORDER BY'] )
321 ?
(array)$this->options
['ORDER BY']
323 $optionOrderBy[] = $order;
324 $this->addOption( 'ORDER BY', $optionOrderBy );
329 * Add a WHERE clause corresponding to a range, similar to addWhereRange,
330 * but converts $start and $end to database timestamps.
332 * @param string $field
334 * @param string $start
338 protected function addTimestampWhereRange( $field, $dir, $start, $end, $sort = true ) {
339 $db = $this->getDB();
340 $this->addWhereRange( $field, $dir,
341 $db->timestampOrNull( $start ), $db->timestampOrNull( $end ), $sort );
345 * Add an option such as LIMIT or USE INDEX. If an option was set
346 * before, the old value will be overwritten
347 * @param string $name Option name
348 * @param string|string[]|null $value Option value
350 protected function addOption( $name, $value = null ) {
351 if ( is_null( $value ) ) {
352 $this->options
[] = $name;
354 $this->options
[$name] = $value;
359 * Execute a SELECT query based on the values in the internal arrays
360 * @param string $method Function the query should be attributed to.
361 * You should usually use __METHOD__ here
362 * @param array $extraQuery Query data to add but not store in the object
368 * 'join_conds' => ...
370 * @param array|null &$hookData If set, the ApiQueryBaseBeforeQuery and
371 * ApiQueryBaseAfterQuery hooks will be called, and the
372 * ApiQueryBaseProcessRow hook will be expected.
373 * @return IResultWrapper
375 protected function select( $method, $extraQuery = [], array &$hookData = null ) {
376 $tables = array_merge(
378 isset( $extraQuery['tables'] ) ?
(array)$extraQuery['tables'] : []
380 $fields = array_merge(
382 isset( $extraQuery['fields'] ) ?
(array)$extraQuery['fields'] : []
384 $where = array_merge(
386 isset( $extraQuery['where'] ) ?
(array)$extraQuery['where'] : []
388 $options = array_merge(
390 isset( $extraQuery['options'] ) ?
(array)$extraQuery['options'] : []
392 $join_conds = array_merge(
394 isset( $extraQuery['join_conds'] ) ?
(array)$extraQuery['join_conds'] : []
397 if ( $hookData !== null ) {
398 Hooks
::run( 'ApiQueryBaseBeforeQuery',
399 [ $this, &$tables, &$fields, &$where, &$options, &$join_conds, &$hookData ]
403 $res = $this->getDB()->select( $tables, $fields, $where, $method, $options, $join_conds );
405 if ( $hookData !== null ) {
406 Hooks
::run( 'ApiQueryBaseAfterQuery', [ $this, $res, &$hookData ] );
413 * Call the ApiQueryBaseProcessRow hook
415 * Generally, a module that passed $hookData to self::select() will call
416 * this just before calling ApiResult::addValue(), and treat a false return
417 * here in the same way it treats a false return from addValue().
420 * @param object $row Database row
421 * @param array &$data Data to be added to the result
422 * @param array &$hookData Hook data from ApiQueryBase::select()
423 * @return bool Return false if row processing should end with continuation
425 protected function processRow( $row, array &$data, array &$hookData ) {
426 return Hooks
::run( 'ApiQueryBaseProcessRow', [ $this, $row, &$data, &$hookData ] );
431 /************************************************************************//**
432 * @name Utility methods
437 * Add information (title and namespace) about a Title object to a
439 * @param array &$arr Result array à la ApiResult
440 * @param Title $title
441 * @param string $prefix Module prefix
443 public static function addTitleInfo( &$arr, $title, $prefix = '' ) {
444 $arr[$prefix . 'ns'] = (int)$title->getNamespace();
445 $arr[$prefix . 'title'] = $title->getPrefixedText();
449 * Add a sub-element under the page element with the given page ID
450 * @param int $pageId Page ID
451 * @param array $data Data array à la ApiResult
452 * @return bool Whether the element fit in the result
454 protected function addPageSubItems( $pageId, $data ) {
455 $result = $this->getResult();
456 ApiResult
::setIndexedTagName( $data, $this->getModulePrefix() );
458 return $result->addValue( [ 'query', 'pages', (int)$pageId ],
459 $this->getModuleName(),
464 * Same as addPageSubItems(), but one element of $data at a time
465 * @param int $pageId Page ID
466 * @param mixed $item Data à la ApiResult
467 * @param string|null $elemname XML element name. If null, getModuleName()
469 * @return bool Whether the element fit in the result
471 protected function addPageSubItem( $pageId, $item, $elemname = null ) {
472 if ( is_null( $elemname ) ) {
473 $elemname = $this->getModulePrefix();
475 $result = $this->getResult();
476 $fit = $result->addValue( [ 'query', 'pages', $pageId,
477 $this->getModuleName() ], null, $item );
481 $result->addIndexedTagName( [ 'query', 'pages', $pageId,
482 $this->getModuleName() ], $elemname );
488 * Set a query-continue value
489 * @param string $paramName Parameter name
490 * @param string|array $paramValue Parameter value
492 protected function setContinueEnumParameter( $paramName, $paramValue ) {
493 $this->getContinuationManager()->addContinueParam( $this, $paramName, $paramValue );
497 * Convert an input title or title prefix into a dbkey.
499 * $namespace should always be specified in order to handle per-namespace
500 * capitalization settings.
502 * @param string $titlePart Title part
503 * @param int $namespace Namespace of the title
504 * @return string DBkey (no namespace prefix)
506 public function titlePartToKey( $titlePart, $namespace = NS_MAIN
) {
507 $t = Title
::makeTitleSafe( $namespace, $titlePart . 'x' );
508 if ( !$t ||
$t->hasFragment() ) {
509 // Invalid title (e.g. bad chars) or contained a '#'.
510 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
512 if ( $namespace != $t->getNamespace() ||
$t->isExternal() ) {
513 // This can happen in two cases. First, if you call titlePartToKey with a title part
514 // that looks like a namespace, but with $defaultNamespace = NS_MAIN. It would be very
515 // difficult to handle such a case. Such cases cannot exist and are therefore treated
516 // as invalid user input. The second case is when somebody specifies a title interwiki
518 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
521 return substr( $t->getDBkey(), 0, -1 );
525 * Convert an input title or title prefix into a namespace constant and dbkey.
528 * @param string $titlePart Title part
529 * @param int $defaultNamespace Default namespace if none is given
530 * @return array (int, string) Namespace number and DBkey
532 public function prefixedTitlePartToKey( $titlePart, $defaultNamespace = NS_MAIN
) {
533 $t = Title
::newFromText( $titlePart . 'x', $defaultNamespace );
534 if ( !$t ||
$t->hasFragment() ||
$t->isExternal() ) {
535 // Invalid title (e.g. bad chars) or contained a '#'.
536 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
539 return [ $t->getNamespace(), substr( $t->getDBkey(), 0, -1 ) ];
543 * @param string $hash
546 public function validateSha1Hash( $hash ) {
547 return (bool)preg_match( '/^[a-f0-9]{40}$/', $hash );
551 * @param string $hash
554 public function validateSha1Base36Hash( $hash ) {
555 return (bool)preg_match( '/^[a-z0-9]{31}$/', $hash );
559 * Check whether the current user has permission to view revision-deleted
563 public function userCanSeeRevDel() {
564 return $this->getPermissionManager()->userHasAnyRight(
574 * Preprocess the result set to fill the GenderCache with the necessary information
575 * before using self::addTitleInfo
577 * @param IResultWrapper $res Result set to work on.
578 * The result set must have _namespace and _title fields with the provided field prefix
579 * @param string $fname The caller function name, always use __METHOD__
580 * @param string $fieldPrefix Prefix for fields to check gender for
582 protected function executeGenderCacheFromResultWrapper(
583 IResultWrapper
$res, $fname = __METHOD__
, $fieldPrefix = 'page'
585 if ( !$res->numRows() ) {
589 $services = MediaWikiServices
::getInstance();
590 $nsInfo = $services->getNamespaceInfo();
591 $namespaceField = $fieldPrefix . '_namespace';
592 $titleField = $fieldPrefix . '_title';
595 foreach ( $res as $row ) {
596 if ( $nsInfo->hasGenderDistinction( $row->$namespaceField ) ) {
597 $usernames[] = $row->$titleField;
601 if ( $usernames === [] ) {
605 $genderCache = $services->getGenderCache();
606 $genderCache->doQuery( $usernames, $fname );
611 /************************************************************************//**
612 * @name Deprecated methods
617 * Filters hidden users (where the user doesn't have the right to view them)
618 * Also adds relevant block information
620 * @deprecated since 1.34, use ApiQueryBlockInfoTrait instead
621 * @param bool $showBlockInfo
624 public function showHiddenUsersAddBlockInfo( $showBlockInfo ) {
625 wfDeprecated( __METHOD__
, '1.34' );
626 return $this->addBlockInfoToQuery( $showBlockInfo );