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 ApiQuery $mQueryModule;
35 private ?IReadableDatabase $mDb;
37 private array $virtualDBs;
38 private string|false $currentDomain;
39
43 private $queryBuilder;
44
51 public function __construct( ApiQuery $queryModule, string $moduleName, $paramPrefix = '' ) {
52 parent::__construct( $queryModule->getMain(), $moduleName, $paramPrefix );
53 $this->mQueryModule = $queryModule;
54 $this->mDb = null;
55 $this->virtualDBs = [];
56 $this->currentDomain = false;
57 $this->resetQueryParams();
58 }
59
60 /***************************************************************************/
61 // region Methods to implement
76 public function getCacheMode( $params ) {
77 return 'private';
78 }
79
90 public function requestExtraData( $pageSet ) {
91 }
92
93 // endregion -- end of methods to implement
94
95 /***************************************************************************/
96 // region Data access
103 public function getQuery() {
104 return $this->mQueryModule;
105 }
106
108 public function getParent() {
109 return $this->getQuery();
110 }
111
119 protected function getDB() {
120 if ( $this->currentDomain ) {
121 if ( !isset( $this->virtualDBs[$this->currentDomain] ) ) {
123 ->getConnectionProvider()
124 ->getReplicaDatabase( $this->currentDomain );
125 $this->virtualDBs[$this->currentDomain] = $db;
126 }
127 return $this->virtualDBs[$this->currentDomain];
128 }
129
130 $this->mDb ??= $this->getQuery()->getDB();
131
132 return $this->mDb;
133 }
134
141 protected function setVirtualDomain( string|false $virtualDomain ) {
142 $this->currentDomain = $virtualDomain;
143 $this->updateQueryBuilderConnection();
144 }
145
151 protected function resetVirtualDomain() {
152 $this->currentDomain = false;
153 $this->updateQueryBuilderConnection();
154 }
155
156 private function updateQueryBuilderConnection() {
157 if ( $this->queryBuilder ) {
158 $this->queryBuilder->connection( $this->getDB() );
159 }
160 }
161
167 protected function getPageSet() {
168 return $this->getQuery()->getPageSet();
169 }
170
171 // endregion -- end of data access
172
173 /***************************************************************************/
174 // region Querying
180 protected function resetQueryParams() {
181 $this->queryBuilder = null;
182 }
183
192 protected function getQueryBuilder() {
193 $this->queryBuilder ??= $this->getDB()->newSelectQueryBuilder();
194 return $this->queryBuilder;
195 }
196
204 protected function addTables( $tables, $alias = null ) {
205 if ( is_array( $tables ) ) {
206 if ( $alias !== null ) {
207 ApiBase::dieDebug( __METHOD__, 'Multiple table aliases not supported' );
208 }
209 $this->getQueryBuilder()->rawTables( $tables );
210 } else {
211 $this->getQueryBuilder()->table( $tables, $alias );
212 }
213 }
214
223 protected function addJoinConds( $join_conds ) {
224 if ( !is_array( $join_conds ) ) {
225 ApiBase::dieDebug( __METHOD__, 'Join conditions have to be arrays' );
226 }
227 $this->getQueryBuilder()->joinConds( $join_conds );
228 }
229
234 protected function addFields( $value ) {
235 $this->getQueryBuilder()->fields( $value );
236 }
237
244 protected function addFieldsIf( $value, $condition ) {
245 if ( $condition ) {
246 $this->addFields( $value );
247
248 return true;
249 }
250
251 return false;
252 }
253
267 protected function addWhere( $value ) {
268 if ( is_array( $value ) ) {
269 // Double check: don't insert empty arrays,
270 // Database::makeList() chokes on them
271 if ( count( $value ) ) {
272 $this->getQueryBuilder()->where( $value );
273 }
274 } else {
275 $this->getQueryBuilder()->where( $value );
276 }
277 }
278
285 protected function addWhereIf( $value, $condition ) {
286 if ( $condition ) {
287 $this->addWhere( $value );
288
289 return true;
290 }
291
292 return false;
293 }
294
304 protected function addWhereFld( $field, $value ) {
305 if ( $value !== null && !( is_array( $value ) && !$value ) ) {
306 $this->getQueryBuilder()->where( [ $field => $value ] );
307 }
308 }
309
331 protected function addWhereIDsFld( $table, $field, $ids ) {
332 // Use count() to its full documented capabilities to simultaneously
333 // test for null, empty array or empty countable object
334 if ( count( $ids ) ) {
335 $ids = $this->filterIDs( [ [ $table, $field ] ], $ids );
336
337 if ( $ids === [] ) {
338 // Return nothing, no IDs are valid
339 $this->getQueryBuilder()->where( '0 = 1' );
340 } else {
341 $this->getQueryBuilder()->where( [ $field => $ids ] );
342 }
343 }
344 return count( $ids );
345 }
346
359 protected function addWhereRange( $field, $dir, $start, $end, $sort = true ) {
360 $isDirNewer = ( $dir === 'newer' );
361 $after = ( $isDirNewer ? '>=' : '<=' );
362 $before = ( $isDirNewer ? '<=' : '>=' );
363 $db = $this->getDB();
364
365 if ( $start !== null ) {
366 $this->addWhere( $db->expr( $field, $after, $start ) );
367 }
368
369 if ( $end !== null ) {
370 $this->addWhere( $db->expr( $field, $before, $end ) );
371 }
372
373 if ( $sort ) {
374 $this->getQueryBuilder()->orderBy( $field, $isDirNewer ? null : 'DESC' );
375 }
376 }
377
388 protected function addTimestampWhereRange( $field, $dir, $start, $end, $sort = true ) {
389 $db = $this->getDB();
390 $this->addWhereRange( $field, $dir,
391 $db->timestampOrNull( $start ), $db->timestampOrNull( $end ), $sort );
392 }
393
400 protected function addOption( $name, $value = null ) {
401 $this->getQueryBuilder()->option( $name, $value );
402 }
403
421 protected function select( $method, $extraQuery = [], ?array &$hookData = null ) {
422 $queryBuilder = clone $this->getQueryBuilder();
423 if ( isset( $extraQuery['tables'] ) ) {
424 $queryBuilder->rawTables( (array)$extraQuery['tables'] );
425 }
426 if ( isset( $extraQuery['fields'] ) ) {
427 $queryBuilder->fields( (array)$extraQuery['fields'] );
428 }
429 if ( isset( $extraQuery['where'] ) ) {
430 $queryBuilder->where( (array)$extraQuery['where'] );
431 }
432 if ( isset( $extraQuery['options'] ) ) {
433 $queryBuilder->options( (array)$extraQuery['options'] );
434 }
435 if ( isset( $extraQuery['join_conds'] ) ) {
436 $queryBuilder->joinConds( (array)$extraQuery['join_conds'] );
437 }
438
439 if ( $hookData !== null && $this->getHookContainer()->isRegistered( 'ApiQueryBaseBeforeQuery' ) ) {
440 $info = $queryBuilder->getQueryInfo();
441 $this->getHookRunner()->onApiQueryBaseBeforeQuery(
442 $this, $info['tables'], $info['fields'], $info['conds'],
443 $info['options'], $info['join_conds'], $hookData
444 );
445 $queryBuilder = $this->getDB()->newSelectQueryBuilder()->queryInfo( $info );
446 }
447
448 $queryBuilder->caller( $method );
449 $res = $queryBuilder->fetchResultSet();
450
451 if ( $hookData !== null ) {
452 $this->getHookRunner()->onApiQueryBaseAfterQuery( $this, $res, $hookData );
453 }
454
455 return $res;
456 }
457
471 protected function processRow( $row, array &$data, array &$hookData ) {
472 return $this->getHookRunner()->onApiQueryBaseProcessRow( $this, $row, $data, $hookData );
473 }
474
475 // endregion -- end of querying
476
477 /***************************************************************************/
478 // region Utility methods
488 public static function addTitleInfo( &$arr, $title, $prefix = '' ) {
489 $arr[$prefix . 'ns'] = $title->getNamespace();
490 $arr[$prefix . 'title'] = $title->getPrefixedText();
491 }
492
499 protected function addPageSubItems( $pageId, $data ) {
500 $result = $this->getResult();
502
503 return $result->addValue( [ 'query', 'pages', (int)$pageId ],
504 $this->getModuleName(),
505 $data );
506 }
507
516 protected function addPageSubItem( $pageId, $item, $elemname = null ) {
517 $result = $this->getResult();
518 $fit = $result->addValue( [ 'query', 'pages', $pageId,
519 $this->getModuleName() ], null, $item );
520 if ( !$fit ) {
521 return false;
522 }
523 $result->addIndexedTagName(
524 [ 'query', 'pages', $pageId, $this->getModuleName() ],
525 $elemname ?? $this->getModulePrefix()
526 );
527
528 return true;
529 }
530
536 protected function setContinueEnumParameter( $paramName, $paramValue ) {
537 $this->getContinuationManager()->addContinueParam( $this, $paramName, $paramValue );
538 }
539
550 public function titlePartToKey( $titlePart, $namespace = NS_MAIN ) {
551 $t = Title::makeTitleSafe( $namespace, $titlePart . 'x' );
552 if ( !$t || $t->hasFragment() ) {
553 // Invalid title (e.g. bad chars) or contained a '#'.
554 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
555 }
556 if ( $namespace != $t->getNamespace() || $t->isExternal() ) {
557 // This can happen in two cases. First, if you call titlePartToKey with a title part
558 // that looks like a namespace, but with $defaultNamespace = NS_MAIN. It would be very
559 // difficult to handle such a case. Such cases cannot exist and are therefore treated
560 // as invalid user input. The second case is when somebody specifies a title interwiki
561 // prefix.
562 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
563 }
564
565 return substr( $t->getDBkey(), 0, -1 );
566 }
567
576 protected function parsePrefixedTitlePart( $titlePart, $defaultNamespace = NS_MAIN ) {
577 try {
578 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
579 $t = $titleParser->parseTitle( $titlePart . 'X', $defaultNamespace );
580 } catch ( MalformedTitleException ) {
581 $t = null;
582 }
583
584 if ( !$t || $t->hasFragment() || $t->isExternal() || $t->getDBkey() === 'X' ) {
585 // Invalid title (e.g. bad chars) or contained a '#'.
586 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
587 }
588
589 return new TitleValue( $t->getNamespace(), substr( $t->getDBkey(), 0, -1 ) );
590 }
591
596 public function validateSha1Hash( $hash ) {
597 return (bool)preg_match( '/^[a-f0-9]{40}$/', $hash );
598 }
599
604 public function validateSha1Base36Hash( $hash ) {
605 return (bool)preg_match( '/^[a-z0-9]{31}$/', $hash );
606 }
607
613 public function userCanSeeRevDel() {
614 return $this->getAuthority()->isAllowedAny(
615 'deletedhistory',
616 'deletedtext',
617 'deleterevision',
618 'suppressrevision',
619 'viewsuppressed'
620 );
621 }
622
633 IResultWrapper $res, $fname = __METHOD__, $fieldPrefix = 'page'
634 ) {
635 if ( !$res->numRows() ) {
636 return;
637 }
638
639 $services = MediaWikiServices::getInstance();
640 if ( !$services->getContentLanguage()->needsGenderDistinction() ) {
641 return;
642 }
643
644 $nsInfo = $services->getNamespaceInfo();
645 $namespaceField = $fieldPrefix . '_namespace';
646 $titleField = $fieldPrefix . '_title';
647
648 $usernames = [];
649 foreach ( $res as $row ) {
650 if ( $nsInfo->hasGenderDistinction( $row->$namespaceField ) ) {
651 $usernames[] = $row->$titleField;
652 }
653 }
654
655 if ( $usernames === [] ) {
656 return;
657 }
658
659 $genderCache = $services->getGenderCache();
660 $genderCache->doQuery( $usernames, $fname );
661 }
662
663 // endregion -- end of utility methods
664}
665
667class_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:61
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1511
getModulePrefix()
Get parameter prefix (usually two letters or an empty string).
Definition ApiBase.php:552
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:543
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition ApiBase.php:767
getMain()
Get the main module.
Definition ApiBase.php:561
getResult()
Get the result object.
Definition ApiBase.php:682
filterIDs( $fields, array $ids)
Filter out-of-range values from a list of positive integer IDs.
Definition ApiBase.php:1386
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1748
getHookContainer()
Get a HookContainer, for running extension hooks or for hook metadata.
Definition ApiBase.php:752
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.
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.
__construct(ApiQuery $queryModule, string $moduleName, $paramPrefix='')
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.