MediaWiki master
ApiQueryBase.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Api;
24
29use stdClass;
35
45abstract class ApiQueryBase extends ApiBase {
47
48 private ApiQuery $mQueryModule;
49 private ?IReadableDatabase $mDb;
50
54 private $queryBuilder;
55
62 public function __construct( ApiQuery $queryModule, string $moduleName, $paramPrefix = '' ) {
63 parent::__construct( $queryModule->getMain(), $moduleName, $paramPrefix );
64 $this->mQueryModule = $queryModule;
65 $this->mDb = null;
66 $this->resetQueryParams();
67 }
68
69 /***************************************************************************/
70 // region Methods to implement
85 public function getCacheMode( $params ) {
86 return 'private';
87 }
88
99 public function requestExtraData( $pageSet ) {
100 }
101
102 // endregion -- end of methods to implement
103
104 /***************************************************************************/
105 // region Data access
112 public function getQuery() {
113 return $this->mQueryModule;
114 }
115
117 public function getParent() {
118 return $this->getQuery();
119 }
120
126 protected function getDB() {
127 $this->mDb ??= $this->getQuery()->getDB();
128
129 return $this->mDb;
130 }
131
137 protected function getPageSet() {
138 return $this->getQuery()->getPageSet();
139 }
140
141 // endregion -- end of data access
142
143 /***************************************************************************/
144 // region Querying
150 protected function resetQueryParams() {
151 $this->queryBuilder = null;
152 }
153
162 protected function getQueryBuilder() {
163 $this->queryBuilder ??= $this->getDB()->newSelectQueryBuilder();
164 return $this->queryBuilder;
165 }
166
174 protected function addTables( $tables, $alias = null ) {
175 if ( is_array( $tables ) ) {
176 if ( $alias !== null ) {
177 ApiBase::dieDebug( __METHOD__, 'Multiple table aliases not supported' );
178 }
179 $this->getQueryBuilder()->rawTables( $tables );
180 } else {
181 $this->getQueryBuilder()->table( $tables, $alias );
182 }
183 }
184
193 protected function addJoinConds( $join_conds ) {
194 if ( !is_array( $join_conds ) ) {
195 ApiBase::dieDebug( __METHOD__, 'Join conditions have to be arrays' );
196 }
197 $this->getQueryBuilder()->joinConds( $join_conds );
198 }
199
204 protected function addFields( $value ) {
205 $this->getQueryBuilder()->fields( $value );
206 }
207
214 protected function addFieldsIf( $value, $condition ) {
215 if ( $condition ) {
216 $this->addFields( $value );
217
218 return true;
219 }
220
221 return false;
222 }
223
237 protected function addWhere( $value ) {
238 if ( is_array( $value ) ) {
239 // Double check: don't insert empty arrays,
240 // Database::makeList() chokes on them
241 if ( count( $value ) ) {
242 $this->getQueryBuilder()->where( $value );
243 }
244 } else {
245 $this->getQueryBuilder()->where( $value );
246 }
247 }
248
255 protected function addWhereIf( $value, $condition ) {
256 if ( $condition ) {
257 $this->addWhere( $value );
258
259 return true;
260 }
261
262 return false;
263 }
264
274 protected function addWhereFld( $field, $value ) {
275 if ( $value !== null && !( is_array( $value ) && !$value ) ) {
276 $this->getQueryBuilder()->where( [ $field => $value ] );
277 }
278 }
279
301 protected function addWhereIDsFld( $table, $field, $ids ) {
302 // Use count() to its full documented capabilities to simultaneously
303 // test for null, empty array or empty countable object
304 if ( count( $ids ) ) {
305 $ids = $this->filterIDs( [ [ $table, $field ] ], $ids );
306
307 if ( $ids === [] ) {
308 // Return nothing, no IDs are valid
309 $this->getQueryBuilder()->where( '0 = 1' );
310 } else {
311 $this->getQueryBuilder()->where( [ $field => $ids ] );
312 }
313 }
314 return count( $ids );
315 }
316
329 protected function addWhereRange( $field, $dir, $start, $end, $sort = true ) {
330 $isDirNewer = ( $dir === 'newer' );
331 $after = ( $isDirNewer ? '>=' : '<=' );
332 $before = ( $isDirNewer ? '<=' : '>=' );
333 $db = $this->getDB();
334
335 if ( $start !== null ) {
336 $this->addWhere( $db->expr( $field, $after, $start ) );
337 }
338
339 if ( $end !== null ) {
340 $this->addWhere( $db->expr( $field, $before, $end ) );
341 }
342
343 if ( $sort ) {
344 $this->getQueryBuilder()->orderBy( $field, $isDirNewer ? null : 'DESC' );
345 }
346 }
347
358 protected function addTimestampWhereRange( $field, $dir, $start, $end, $sort = true ) {
359 $db = $this->getDB();
360 $this->addWhereRange( $field, $dir,
361 $db->timestampOrNull( $start ), $db->timestampOrNull( $end ), $sort );
362 }
363
370 protected function addOption( $name, $value = null ) {
371 $this->getQueryBuilder()->option( $name, $value );
372 }
373
391 protected function select( $method, $extraQuery = [], ?array &$hookData = null ) {
392 $queryBuilder = clone $this->getQueryBuilder();
393 if ( isset( $extraQuery['tables'] ) ) {
394 $queryBuilder->rawTables( (array)$extraQuery['tables'] );
395 }
396 if ( isset( $extraQuery['fields'] ) ) {
397 $queryBuilder->fields( (array)$extraQuery['fields'] );
398 }
399 if ( isset( $extraQuery['where'] ) ) {
400 $queryBuilder->where( (array)$extraQuery['where'] );
401 }
402 if ( isset( $extraQuery['options'] ) ) {
403 $queryBuilder->options( (array)$extraQuery['options'] );
404 }
405 if ( isset( $extraQuery['join_conds'] ) ) {
406 $queryBuilder->joinConds( (array)$extraQuery['join_conds'] );
407 }
408
409 if ( $hookData !== null && $this->getHookContainer()->isRegistered( 'ApiQueryBaseBeforeQuery' ) ) {
410 $info = $queryBuilder->getQueryInfo();
411 $this->getHookRunner()->onApiQueryBaseBeforeQuery(
412 $this, $info['tables'], $info['fields'], $info['conds'],
413 $info['options'], $info['join_conds'], $hookData
414 );
415 $queryBuilder = $this->getDB()->newSelectQueryBuilder()->queryInfo( $info );
416 }
417
418 $queryBuilder->caller( $method );
419 $res = $queryBuilder->fetchResultSet();
420
421 if ( $hookData !== null ) {
422 $this->getHookRunner()->onApiQueryBaseAfterQuery( $this, $res, $hookData );
423 }
424
425 return $res;
426 }
427
441 protected function processRow( $row, array &$data, array &$hookData ) {
442 return $this->getHookRunner()->onApiQueryBaseProcessRow( $this, $row, $data, $hookData );
443 }
444
445 // endregion -- end of querying
446
447 /***************************************************************************/
448 // region Utility methods
458 public static function addTitleInfo( &$arr, $title, $prefix = '' ) {
459 $arr[$prefix . 'ns'] = $title->getNamespace();
460 $arr[$prefix . 'title'] = $title->getPrefixedText();
461 }
462
469 protected function addPageSubItems( $pageId, $data ) {
470 $result = $this->getResult();
472
473 return $result->addValue( [ 'query', 'pages', (int)$pageId ],
474 $this->getModuleName(),
475 $data );
476 }
477
486 protected function addPageSubItem( $pageId, $item, $elemname = null ) {
487 $result = $this->getResult();
488 $fit = $result->addValue( [ 'query', 'pages', $pageId,
489 $this->getModuleName() ], null, $item );
490 if ( !$fit ) {
491 return false;
492 }
493 $result->addIndexedTagName(
494 [ 'query', 'pages', $pageId, $this->getModuleName() ],
495 $elemname ?? $this->getModulePrefix()
496 );
497
498 return true;
499 }
500
506 protected function setContinueEnumParameter( $paramName, $paramValue ) {
507 $this->getContinuationManager()->addContinueParam( $this, $paramName, $paramValue );
508 }
509
520 public function titlePartToKey( $titlePart, $namespace = NS_MAIN ) {
521 $t = Title::makeTitleSafe( $namespace, $titlePart . 'x' );
522 if ( !$t || $t->hasFragment() ) {
523 // Invalid title (e.g. bad chars) or contained a '#'.
524 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
525 }
526 if ( $namespace != $t->getNamespace() || $t->isExternal() ) {
527 // This can happen in two cases. First, if you call titlePartToKey with a title part
528 // that looks like a namespace, but with $defaultNamespace = NS_MAIN. It would be very
529 // difficult to handle such a case. Such cases cannot exist and are therefore treated
530 // as invalid user input. The second case is when somebody specifies a title interwiki
531 // prefix.
532 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
533 }
534
535 return substr( $t->getDBkey(), 0, -1 );
536 }
537
546 protected function parsePrefixedTitlePart( $titlePart, $defaultNamespace = NS_MAIN ) {
547 try {
548 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
549 $t = $titleParser->parseTitle( $titlePart . 'X', $defaultNamespace );
550 } catch ( MalformedTitleException $e ) {
551 $t = null;
552 }
553
554 if ( !$t || $t->hasFragment() || $t->isExternal() || $t->getDBkey() === 'X' ) {
555 // Invalid title (e.g. bad chars) or contained a '#'.
556 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
557 }
558
559 return new TitleValue( $t->getNamespace(), substr( $t->getDBkey(), 0, -1 ) );
560 }
561
566 public function validateSha1Hash( $hash ) {
567 return (bool)preg_match( '/^[a-f0-9]{40}$/', $hash );
568 }
569
574 public function validateSha1Base36Hash( $hash ) {
575 return (bool)preg_match( '/^[a-z0-9]{31}$/', $hash );
576 }
577
583 public function userCanSeeRevDel() {
584 return $this->getAuthority()->isAllowedAny(
585 'deletedhistory',
586 'deletedtext',
587 'deleterevision',
588 'suppressrevision',
589 'viewsuppressed'
590 );
591 }
592
603 IResultWrapper $res, $fname = __METHOD__, $fieldPrefix = 'page'
604 ) {
605 if ( !$res->numRows() ) {
606 return;
607 }
608
609 $services = MediaWikiServices::getInstance();
610 if ( !$services->getContentLanguage()->needsGenderDistinction() ) {
611 return;
612 }
613
614 $nsInfo = $services->getNamespaceInfo();
615 $namespaceField = $fieldPrefix . '_namespace';
616 $titleField = $fieldPrefix . '_title';
617
618 $usernames = [];
619 foreach ( $res as $row ) {
620 if ( $nsInfo->hasGenderDistinction( $row->$namespaceField ) ) {
621 $usernames[] = $row->$titleField;
622 }
623 }
624
625 if ( $usernames === [] ) {
626 return;
627 }
628
629 $genderCache = $services->getGenderCache();
630 $genderCache->doQuery( $usernames, $fname );
631 }
632
633 // endregion -- end of utility methods
634}
635
637class_alias( ApiQueryBase::class, 'ApiQueryBase' );
const NS_MAIN
Definition Defines.php:65
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:75
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1522
getModulePrefix()
Get parameter prefix (usually two letters or an empty string).
Definition ApiBase.php:566
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:557
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition ApiBase.php:781
getMain()
Get the main module.
Definition ApiBase.php:575
getResult()
Get the result object.
Definition ApiBase.php:696
filterIDs( $fields, array $ids)
Filter out-of-range values from a list of positive integer IDs.
Definition ApiBase.php:1397
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1759
getHookContainer()
Get a HookContainer, for running extension hooks or for hook metadata.
Definition ApiBase.php:766
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.
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:48
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:78
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:45
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.