MediaWiki master
ApiQueryBase.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Api;
10
15use stdClass;
21
31abstract class ApiQueryBase extends ApiBase {
33
34 private ?IReadableDatabase $mDb;
36 private array $virtualDBs;
37 private string|false $currentDomain;
38
42 private $queryBuilder;
43
50 public function __construct(
51 private readonly ApiQuery $mQueryModule,
52 string $moduleName,
53 $paramPrefix = '',
54 ) {
55 parent::__construct( $mQueryModule->getMain(), $moduleName, $paramPrefix );
56 $this->mDb = null;
57 $this->virtualDBs = [];
58 $this->currentDomain = false;
59 $this->resetQueryParams();
60 }
61
62 /***************************************************************************/
63 // region Methods to implement
78 public function getCacheMode( $params ) {
79 return 'private';
80 }
81
92 public function requestExtraData( $pageSet ) {
93 }
94
95 // endregion -- end of methods to implement
96
97 /***************************************************************************/
98 // region Data access
105 public function getQuery() {
106 return $this->mQueryModule;
107 }
108
110 public function getParent() {
111 return $this->getQuery();
112 }
113
121 protected function getDB() {
122 if ( $this->currentDomain ) {
123 if ( !isset( $this->virtualDBs[$this->currentDomain] ) ) {
125 ->getConnectionProvider()
126 ->getReplicaDatabase( $this->currentDomain );
127 $this->virtualDBs[$this->currentDomain] = $db;
128 }
129 return $this->virtualDBs[$this->currentDomain];
130 }
131
132 $this->mDb ??= $this->getQuery()->getDB();
133
134 return $this->mDb;
135 }
136
143 protected function setVirtualDomain( string|false $virtualDomain ) {
144 $this->currentDomain = $virtualDomain;
145 $this->updateQueryBuilderConnection();
146 }
147
153 protected function resetVirtualDomain() {
154 $this->currentDomain = false;
155 $this->updateQueryBuilderConnection();
156 }
157
158 private function updateQueryBuilderConnection() {
159 if ( $this->queryBuilder ) {
160 $this->queryBuilder->connection( $this->getDB() );
161 }
162 }
163
169 protected function getPageSet() {
170 return $this->getQuery()->getPageSet();
171 }
172
173 // endregion -- end of data access
174
175 /***************************************************************************/
176 // region Querying
182 protected function resetQueryParams() {
183 $this->queryBuilder = null;
184 }
185
194 protected function getQueryBuilder() {
195 $this->queryBuilder ??= $this->getDB()->newSelectQueryBuilder();
196 return $this->queryBuilder;
197 }
198
206 protected function addTables( $tables, $alias = null ) {
207 if ( is_array( $tables ) ) {
208 if ( $alias !== null ) {
209 ApiBase::dieDebug( __METHOD__, 'Multiple table aliases not supported' );
210 }
211 $this->getQueryBuilder()->rawTables( $tables );
212 } else {
213 $this->getQueryBuilder()->table( $tables, $alias );
214 }
215 }
216
225 protected function addJoinConds( $join_conds ) {
226 if ( !is_array( $join_conds ) ) {
227 ApiBase::dieDebug( __METHOD__, 'Join conditions have to be arrays' );
228 }
229 $this->getQueryBuilder()->joinConds( $join_conds );
230 }
231
236 protected function addFields( $value ) {
237 $this->getQueryBuilder()->fields( $value );
238 }
239
246 protected function addFieldsIf( $value, $condition ) {
247 if ( $condition ) {
248 $this->addFields( $value );
249
250 return true;
251 }
252
253 return false;
254 }
255
269 protected function addWhere( $value ) {
270 if ( is_array( $value ) ) {
271 // Double check: don't insert empty arrays,
272 // Database::makeList() chokes on them
273 if ( count( $value ) ) {
274 $this->getQueryBuilder()->where( $value );
275 }
276 } else {
277 $this->getQueryBuilder()->where( $value );
278 }
279 }
280
287 protected function addWhereIf( $value, $condition ) {
288 if ( $condition ) {
289 $this->addWhere( $value );
290
291 return true;
292 }
293
294 return false;
295 }
296
306 protected function addWhereFld( $field, $value ) {
307 if ( $value !== null && !( is_array( $value ) && !$value ) ) {
308 $this->getQueryBuilder()->where( [ $field => $value ] );
309 }
310 }
311
333 protected function addWhereIDsFld( $table, $field, $ids ) {
334 // Use count() to its full documented capabilities to simultaneously
335 // test for null, empty array or empty countable object
336 if ( count( $ids ) ) {
337 $ids = $this->filterIDs( [ [ $table, $field ] ], $ids );
338
339 if ( $ids === [] ) {
340 // Return nothing, no IDs are valid
341 $this->getQueryBuilder()->where( '0 = 1' );
342 } else {
343 $this->getQueryBuilder()->where( [ $field => $ids ] );
344 }
345 }
346 return count( $ids );
347 }
348
361 protected function addWhereRange( $field, $dir, $start, $end, $sort = true ) {
362 $isDirNewer = ( $dir === 'newer' );
363 $after = ( $isDirNewer ? '>=' : '<=' );
364 $before = ( $isDirNewer ? '<=' : '>=' );
365 $db = $this->getDB();
366
367 if ( $start !== null ) {
368 $this->addWhere( $db->expr( $field, $after, $start ) );
369 }
370
371 if ( $end !== null ) {
372 $this->addWhere( $db->expr( $field, $before, $end ) );
373 }
374
375 if ( $sort ) {
376 $this->getQueryBuilder()->orderBy( $field, $isDirNewer ? null : 'DESC' );
377 }
378 }
379
390 protected function addTimestampWhereRange( $field, $dir, $start, $end, $sort = true ) {
391 $db = $this->getDB();
392 $this->addWhereRange( $field, $dir,
393 $db->timestampOrNull( $start ), $db->timestampOrNull( $end ), $sort );
394 }
395
402 protected function addOption( $name, $value = null ) {
403 $this->getQueryBuilder()->option( $name, $value );
404 }
405
423 protected function select( $method, $extraQuery = [], ?array &$hookData = null ) {
424 $queryBuilder = clone $this->getQueryBuilder();
425 if ( isset( $extraQuery['tables'] ) ) {
426 $queryBuilder->rawTables( (array)$extraQuery['tables'] );
427 }
428 if ( isset( $extraQuery['fields'] ) ) {
429 $queryBuilder->fields( (array)$extraQuery['fields'] );
430 }
431 if ( isset( $extraQuery['where'] ) ) {
432 $queryBuilder->where( (array)$extraQuery['where'] );
433 }
434 if ( isset( $extraQuery['options'] ) ) {
435 $queryBuilder->options( (array)$extraQuery['options'] );
436 }
437 if ( isset( $extraQuery['join_conds'] ) ) {
438 $queryBuilder->joinConds( (array)$extraQuery['join_conds'] );
439 }
440
441 if ( $hookData !== null && $this->getHookContainer()->isRegistered( 'ApiQueryBaseBeforeQuery' ) ) {
442 $info = $queryBuilder->getQueryInfo();
443 $this->getHookRunner()->onApiQueryBaseBeforeQuery(
444 $this, $info['tables'], $info['fields'], $info['conds'],
445 $info['options'], $info['join_conds'], $hookData
446 );
447 $queryBuilder = $this->getDB()->newSelectQueryBuilder()->queryInfo( $info );
448 }
449
450 $queryBuilder->caller( $method );
451 $res = $queryBuilder->fetchResultSet();
452
453 if ( $hookData !== null ) {
454 $this->getHookRunner()->onApiQueryBaseAfterQuery( $this, $res, $hookData );
455 }
456
457 return $res;
458 }
459
473 protected function processRow( $row, array &$data, array &$hookData ) {
474 return $this->getHookRunner()->onApiQueryBaseProcessRow( $this, $row, $data, $hookData );
475 }
476
477 // endregion -- end of querying
478
479 /***************************************************************************/
480 // region Utility methods
490 public static function addTitleInfo( &$arr, $title, $prefix = '' ) {
491 $arr[$prefix . 'ns'] = $title->getNamespace();
492 $arr[$prefix . 'title'] = $title->getPrefixedText();
493 }
494
501 protected function addPageSubItems( $pageId, $data ) {
502 $result = $this->getResult();
504
505 return $result->addValue( [ 'query', 'pages', (int)$pageId ],
506 $this->getModuleName(),
507 $data );
508 }
509
518 protected function addPageSubItem( $pageId, $item, $elemname = null ) {
519 $result = $this->getResult();
520 $fit = $result->addValue( [ 'query', 'pages', $pageId,
521 $this->getModuleName() ], null, $item );
522 if ( !$fit ) {
523 return false;
524 }
525 $result->addIndexedTagName(
526 [ 'query', 'pages', $pageId, $this->getModuleName() ],
527 $elemname ?? $this->getModulePrefix()
528 );
529
530 return true;
531 }
532
538 protected function setContinueEnumParameter( $paramName, $paramValue ) {
539 $this->getContinuationManager()->addContinueParam( $this, $paramName, $paramValue );
540 }
541
552 public function titlePartToKey( $titlePart, $namespace = NS_MAIN ) {
553 $t = Title::makeTitleSafe( $namespace, $titlePart . 'x' );
554 if ( !$t || $t->hasFragment() ) {
555 // Invalid title (e.g. bad chars) or contained a '#'.
556 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
557 }
558 if ( $namespace != $t->getNamespace() || $t->isExternal() ) {
559 // This can happen in two cases. First, if you call titlePartToKey with a title part
560 // that looks like a namespace, but with $defaultNamespace = NS_MAIN. It would be very
561 // difficult to handle such a case. Such cases cannot exist and are therefore treated
562 // as invalid user input. The second case is when somebody specifies a title interwiki
563 // prefix.
564 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
565 }
566
567 return substr( $t->getDBkey(), 0, -1 );
568 }
569
578 protected function parsePrefixedTitlePart( $titlePart, $defaultNamespace = NS_MAIN ) {
579 try {
580 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
581 $t = $titleParser->parseTitle( $titlePart . 'X', $defaultNamespace );
582 } catch ( MalformedTitleException ) {
583 $t = null;
584 }
585
586 if ( !$t || $t->hasFragment() || $t->isExternal() || $t->getDBkey() === 'X' ) {
587 // Invalid title (e.g. bad chars) or contained a '#'.
588 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
589 }
590
591 return new TitleValue( $t->getNamespace(), substr( $t->getDBkey(), 0, -1 ) );
592 }
593
598 public function validateSha1Hash( $hash ) {
599 return (bool)preg_match( '/^[a-f0-9]{40}$/', $hash );
600 }
601
606 public function validateSha1Base36Hash( $hash ) {
607 return (bool)preg_match( '/^[a-z0-9]{31}$/', $hash );
608 }
609
615 public function userCanSeeRevDel() {
616 return $this->getAuthority()->isAllowedAny(
617 'deletedhistory',
618 'deletedtext',
619 'deleterevision',
620 'suppressrevision',
621 'viewsuppressed'
622 );
623 }
624
635 IResultWrapper $res, $fname = __METHOD__, $fieldPrefix = 'page'
636 ) {
637 if ( !$res->numRows() ) {
638 return;
639 }
640
641 $services = MediaWikiServices::getInstance();
642 if ( !$services->getContentLanguage()->needsGenderDistinction() ) {
643 return;
644 }
645
646 $nsInfo = $services->getNamespaceInfo();
647 $namespaceField = $fieldPrefix . '_namespace';
648 $titleField = $fieldPrefix . '_title';
649
650 $usernames = [];
651 foreach ( $res as $row ) {
652 if ( $nsInfo->hasGenderDistinction( $row->$namespaceField ) ) {
653 $usernames[] = $row->$titleField;
654 }
655 }
656
657 if ( $usernames === [] ) {
658 return;
659 }
660
661 $genderCache = $services->getGenderCache();
662 $genderCache->doQuery( $usernames, $fname );
663 }
664
665 // endregion -- end of utility methods
666}
667
669class_alias( ApiQueryBase::class, 'ApiQueryBase' );
const NS_MAIN
Definition Defines.php:51
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:60
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1506
getModulePrefix()
Get parameter prefix (usually two letters or an empty string).
Definition ApiBase.php:551
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:542
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition ApiBase.php:766
getResult()
Get the result object.
Definition ApiBase.php:681
filterIDs( $fields, array $ids)
Filter out-of-range values from a list of positive integer IDs.
Definition ApiBase.php:1381
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1743
getHookContainer()
Get a HookContainer, for running extension hooks or for hook metadata.
Definition ApiBase.php:751
This is a base class for all Query modules.
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
getParent()
Get the parent of this module.to override 1.25 ApiBase|null
addFieldsIf( $value, $condition)
Same as addFields(), but add the fields only if a condition is met.
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
addWhereIf( $value, $condition)
Same as addWhere(), but add the WHERE clauses only if a condition is met.
addPageSubItems( $pageId, $data)
Add a sub-element under the page element with the given page ID.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
addPageSubItem( $pageId, $item, $elemname=null)
Same as addPageSubItems(), but one element of $data at a time.
addJoinConds( $join_conds)
Add a set of JOIN conditions to the internal array.
addWhereIDsFld( $table, $field, $ids)
Like addWhereFld for an integer list of IDs.
resetVirtualDomain()
Reset the virtual domain to the main database.
setVirtualDomain(string|false $virtualDomain)
Set the Query database connection (read-only)
getDB()
Get the Query database connection (read-only).
requestExtraData( $pageSet)
Override this method to request extra fields from the pageSet using $pageSet->requestField('fieldName...
select( $method, $extraQuery=[], ?array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
titlePartToKey( $titlePart, $namespace=NS_MAIN)
Convert an input title or title prefix into a dbkey.
addWhere( $value)
Add a set of WHERE clauses to the internal array.
executeGenderCacheFromResultWrapper(IResultWrapper $res, $fname=__METHOD__, $fieldPrefix='page')
Preprocess the result set to fill the GenderCache with the necessary information before using self::a...
parsePrefixedTitlePart( $titlePart, $defaultNamespace=NS_MAIN)
Convert an input title or title prefix into a TitleValue.
getPageSet()
Get the PageSet object to work on.
__construct(private readonly ApiQuery $mQueryModule, string $moduleName, $paramPrefix='',)
getCacheMode( $params)
Get the cache mode for the data generated by this module.
getQuery()
Get the main Query module.
addTimestampWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, similar to addWhereRange, but converts $start and $end t...
getQueryBuilder()
Get the SelectQueryBuilder.
userCanSeeRevDel()
Check whether the current user has permission to view revision-deleted fields.
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.
processRow( $row, array &$data, array &$hookData)
Call the ApiQueryBaseProcessRow hook.
resetQueryParams()
Blank the internal arrays with query parameters.
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
addFields( $value)
Add a set of fields to select to the internal array.
addWhereRange( $field, $dir, $start, $end, $sort=true)
Add a WHERE clause corresponding to a range, and an ORDER BY clause to sort in the right direction.
This is the main query class.
Definition ApiQuery.php:36
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:69
Build SELECT queries with a fluent interface.
getQueryInfo( $joinsName='join_conds')
Get an associative array describing the query in terms of its raw parameters to IReadableDatabase::se...
rawTables( $tables)
Given a table or table array as might be passed to IReadableDatabase::select(), append it to the exis...
fetchResultSet()
Run the constructed SELECT query and return all results.
queryInfo( $info)
Set the query parameters to the given values, appending to the values which were already set.
options(array $options)
Manually set multiple options in the $options array to be passed to IReadableDatabase::select().
caller( $fname)
Set the method name to be included in an SQL comment.
joinConds(array $joinConds)
Manually append to the $join_conds array which will be passed to IReadableDatabase::select().
fields( $fields)
Add a field or an array of fields to the query.
where( $conds)
Add conditions to the query.
Interface to a relational database.
Definition IDatabase.php:31
A database connection without write operations.
Result wrapper for grabbing data queried from an IDatabase object.
numRows()
Get the number of rows in a result object.