Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 153
0.00% covered (danger)
0.00%
0 / 35
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryBase
0.00% covered (danger)
0.00%
0 / 152
0.00% covered (danger)
0.00%
0 / 35
6320
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getCacheMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 requestExtraData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getParent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDB
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 setVirtualDomain
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 resetVirtualDomain
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 updateQueryBuilderConnection
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getPageSet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 resetQueryParams
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryBuilder
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addTables
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 addJoinConds
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 addFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addFieldsIf
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 addWhere
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 addWhereIf
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 addWhereFld
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
20
 addWhereIDsFld
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 addWhereRange
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
 addTimestampWhereRange
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 addOption
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 select
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
90
 processRow
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addTitleInfo
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addPageSubItems
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 addPageSubItem
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 setContinueEnumParameter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 titlePartToKey
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 parsePrefixedTitlePart
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
 validateSha1Hash
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateSha1Base36Hash
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 userCanSeeRevDel
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 executeGenderCacheFromResultWrapper
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Title\MalformedTitleException;
13use MediaWiki\Title\Title;
14use MediaWiki\Title\TitleValue;
15use stdClass;
16use Wikimedia\Rdbms\IDatabase;
17use Wikimedia\Rdbms\IExpression;
18use Wikimedia\Rdbms\IReadableDatabase;
19use Wikimedia\Rdbms\IResultWrapper;
20use Wikimedia\Rdbms\SelectQueryBuilder;
21
22/**
23 * This is a base class for all Query modules.
24 * It provides some common functionality such as constructing various SQL
25 * queries.
26 *
27 * @stable to extend
28 *
29 * @ingroup API
30 */
31abstract class ApiQueryBase extends ApiBase {
32    use ApiQueryBlockInfoTrait;
33
34    private ApiQuery $mQueryModule;
35    private ?IReadableDatabase $mDb;
36    /** @var array<string,IReadableDatabase> */
37    private array $virtualDBs;
38    private string|false $currentDomain;
39
40    /**
41     * @var SelectQueryBuilder|null
42     */
43    private $queryBuilder;
44
45    /**
46     * @stable to call
47     * @param ApiQuery $queryModule
48     * @param string $moduleName
49     * @param string $paramPrefix
50     */
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
62    /** @name   Methods to implement */
63
64    /**
65     * Get the cache mode for the data generated by this module. Override
66     * this in the module subclass. For possible return values and other
67     * details about cache modes, see ApiMain::setCacheMode()
68     *
69     * Public caching will only be allowed if *all* the modules that supply
70     * data for a given request return a cache mode of public.
71     *
72     * @stable to override
73     * @param array $params
74     * @return string
75     */
76    public function getCacheMode( $params ) {
77        return 'private';
78    }
79
80    /**
81     * Override this method to request extra fields from the pageSet
82     * using $pageSet->requestField('fieldName')
83     *
84     * Note this only makes sense for 'prop' modules, as 'list' and 'meta'
85     * modules should not be using the pageset.
86     *
87     * @stable to override
88     * @param ApiPageSet $pageSet
89     */
90    public function requestExtraData( $pageSet ) {
91    }
92
93    // endregion -- end of methods to implement
94
95    /***************************************************************************/
96    // region   Data access
97    /** @name   Data access */
98
99    /**
100     * Get the main Query module
101     * @return ApiQuery
102     */
103    public function getQuery() {
104        return $this->mQueryModule;
105    }
106
107    /** @inheritDoc */
108    public function getParent() {
109        return $this->getQuery();
110    }
111
112    /**
113     * Get the Query database connection (read-only). This will return the
114     * virtual database if a virtual domain is set, otherwise the main database.
115     *
116     * @stable to override
117     * @return IReadableDatabase
118     */
119    protected function getDB() {
120        if ( $this->currentDomain ) {
121            if ( !isset( $this->virtualDBs[$this->currentDomain] ) ) {
122                $db = MediaWikiServices::getInstance()
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
135    /**
136     * Set the Query database connection (read-only)
137     *
138     * @stable to override
139     * @param string|false $virtualDomain
140     */
141    protected function setVirtualDomain( string|false $virtualDomain ) {
142        $this->currentDomain = $virtualDomain;
143        $this->updateQueryBuilderConnection();
144    }
145
146    /**
147     * Reset the virtual domain to the main database
148     *
149     * @stable to override
150     */
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
162    /**
163     * Get the PageSet object to work on
164     * @stable to override
165     * @return ApiPageSet
166     */
167    protected function getPageSet() {
168        return $this->getQuery()->getPageSet();
169    }
170
171    // endregion -- end of data access
172
173    /***************************************************************************/
174    // region   Querying
175    /** @name   Querying */
176
177    /**
178     * Blank the internal arrays with query parameters
179     */
180    protected function resetQueryParams() {
181        $this->queryBuilder = null;
182    }
183
184    /**
185     * Get the SelectQueryBuilder.
186     *
187     * This is lazy initialised since getDB() fails in ApiQueryAllImages if it
188     * is called before the constructor completes.
189     *
190     * @return SelectQueryBuilder
191     */
192    protected function getQueryBuilder() {
193        $this->queryBuilder ??= $this->getDB()->newSelectQueryBuilder();
194        return $this->queryBuilder;
195    }
196
197    /**
198     * Add a set of tables to the internal array
199     * @param string|array $tables Table name or array of table names
200     *  or nested arrays for joins using parentheses for grouping
201     * @param string|null $alias Table alias, or null for no alias. Cannot be
202     *  used with multiple tables
203     */
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
215    /**
216     * Add a set of JOIN conditions to the internal array
217     *
218     * JOIN conditions are formatted as [ tablename => [ jointype, conditions ] ]
219     * e.g. [ 'page' => [ 'LEFT JOIN', 'page_id=rev_page' ] ].
220     * Conditions may be a string or an addWhere()-style array.
221     * @param array $join_conds JOIN conditions
222     */
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
230    /**
231     * Add a set of fields to select to the internal array
232     * @param array|string $value Field name or array of field names
233     */
234    protected function addFields( $value ) {
235        $this->getQueryBuilder()->fields( $value );
236    }
237
238    /**
239     * Same as addFields(), but add the fields only if a condition is met
240     * @param array|string $value See addFields()
241     * @param bool $condition If false, do nothing
242     * @return bool
243     */
244    protected function addFieldsIf( $value, $condition ) {
245        if ( $condition ) {
246            $this->addFields( $value );
247
248            return true;
249        }
250
251        return false;
252    }
253
254    /**
255     * Add a set of WHERE clauses to the internal array.
256     *
257     * The array should be appropriate for passing as $conds to
258     * IDatabase::select(). Arrays from multiple calls are merged with
259     * array_merge(). A string is treated as a single-element array.
260     *
261     * When passing `'field' => $arrayOfIDs` where the IDs are taken from user
262     * input, consider using addWhereIDsFld() instead.
263     *
264     * @see IDatabase::select()
265     * @param string|array|IExpression $value
266     */
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
279    /**
280     * Same as addWhere(), but add the WHERE clauses only if a condition is met
281     * @param string|array|IExpression $value
282     * @param bool $condition If false, do nothing
283     * @return bool
284     */
285    protected function addWhereIf( $value, $condition ) {
286        if ( $condition ) {
287            $this->addWhere( $value );
288
289            return true;
290        }
291
292        return false;
293    }
294
295    /**
296     * Equivalent to addWhere( [ $field => $value ] )
297     *
298     * When $value is an array of integer IDs taken from user input,
299     * consider using addWhereIDsFld() instead.
300     *
301     * @param string $field Field name
302     * @param int|string|(string|int|null)[] $value Value; ignored if null or empty array
303     */
304    protected function addWhereFld( $field, $value ) {
305        if ( $value !== null && !( is_array( $value ) && !$value ) ) {
306            $this->getQueryBuilder()->where( [ $field => $value ] );
307        }
308    }
309
310    /**
311     * Like addWhereFld for an integer list of IDs
312     *
313     * When passed wildly out-of-range values for integer comparison,
314     * the database may choose a poor query plan. This method validates the
315     * passed IDs against the range of values in the database to omit
316     * out-of-range values.
317     *
318     * This should be used when the IDs are derived from arbitrary user input;
319     * it is not necessary if the IDs are already known to be within a sensible
320     * range.
321     *
322     * This should not be used when there is not a suitable index on $field to
323     * quickly retrieve the minimum and maximum values.
324     *
325     * @since 1.33
326     * @param string $table Table name
327     * @param string $field Field name
328     * @param int[] $ids
329     * @return int Count of IDs actually included
330     */
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
347    /**
348     * Add a WHERE clause corresponding to a range, and an ORDER BY
349     * clause to sort in the right direction
350     * @param string $field Field name
351     * @param string $dir If 'newer', sort in ascending order, otherwise
352     *  sort in descending order
353     * @param string|int|null $start Value to start the list at. If $dir == 'newer'
354     *  this is the lower boundary, otherwise it's the upper boundary
355     * @param string|int|null $end Value to end the list at. If $dir == 'newer' this
356     *  is the upper boundary, otherwise it's the lower boundary
357     * @param bool $sort If false, don't add an ORDER BY clause
358     */
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
378    /**
379     * Add a WHERE clause corresponding to a range, similar to addWhereRange,
380     * but converts $start and $end to database timestamps.
381     * @see addWhereRange
382     * @param string $field
383     * @param string $dir
384     * @param string|int|null $start
385     * @param string|int|null $end
386     * @param bool $sort
387     */
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
394    /**
395     * Add an option such as LIMIT or USE INDEX. If an option was set
396     * before, the old value will be overwritten
397     * @param string $name Option name
398     * @param mixed $value The option value, or null for a boolean option
399     */
400    protected function addOption( $name, $value = null ) {
401        $this->getQueryBuilder()->option( $name, $value );
402    }
403
404    /**
405     * Execute a SELECT query based on the values in the internal arrays
406     * @param string $method Function the query should be attributed to.
407     *  You should usually use __METHOD__ here
408     * @param array $extraQuery Query data to add but not store in the object
409     *  Format is [
410     *    'tables' => ...,
411     *    'fields' => ...,
412     *    'where' => ...,
413     *    'options' => ...,
414     *    'join_conds' => ...
415     *  ]
416     * @param array|null &$hookData If set, the ApiQueryBaseBeforeQuery and
417     *  ApiQueryBaseAfterQuery hooks will be called, and the
418     *  ApiQueryBaseProcessRow hook will be expected.
419     * @return IResultWrapper
420     */
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
458    /**
459     * Call the ApiQueryBaseProcessRow hook
460     *
461     * Generally, a module that passed $hookData to self::select() will call
462     * this just before calling ApiResult::addValue(), and treat a false return
463     * here in the same way it treats a false return from addValue().
464     *
465     * @since 1.28
466     * @param stdClass $row Database row
467     * @param array &$data Data to be added to the result
468     * @param array &$hookData Hook data from ApiQueryBase::select() @phan-output-reference
469     * @return bool Return false if row processing should end with continuation
470     */
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
479    /** @name   Utility methods */
480
481    /**
482     * Add information (title and namespace) about a Title object to a
483     * result array
484     * @param array &$arr Result array à la ApiResult
485     * @param Title $title
486     * @param string $prefix Module prefix
487     */
488    public static function addTitleInfo( &$arr, $title, $prefix = '' ) {
489        $arr[$prefix . 'ns'] = $title->getNamespace();
490        $arr[$prefix . 'title'] = $title->getPrefixedText();
491    }
492
493    /**
494     * Add a sub-element under the page element with the given page ID
495     * @param int $pageId
496     * @param array $data Data array à la ApiResult
497     * @return bool Whether the element fit in the result
498     */
499    protected function addPageSubItems( $pageId, $data ) {
500        $result = $this->getResult();
501        ApiResult::setIndexedTagName( $data, $this->getModulePrefix() );
502
503        return $result->addValue( [ 'query', 'pages', (int)$pageId ],
504            $this->getModuleName(),
505            $data );
506    }
507
508    /**
509     * Same as addPageSubItems(), but one element of $data at a time
510     * @param int $pageId
511     * @param mixed $item Data à la ApiResult
512     * @param string|null $elemname XML element name. If null, getModuleName()
513     *  is used
514     * @return bool Whether the element fit in the result
515     */
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
531    /**
532     * Set a query-continue value
533     * @param string $paramName Parameter name
534     * @param int|string|array $paramValue Parameter value
535     */
536    protected function setContinueEnumParameter( $paramName, $paramValue ) {
537        $this->getContinuationManager()->addContinueParam( $this, $paramName, $paramValue );
538    }
539
540    /**
541     * Convert an input title or title prefix into a dbkey.
542     *
543     * $namespace should always be specified in order to handle per-namespace
544     * capitalization settings.
545     *
546     * @param string $titlePart
547     * @param int $namespace Namespace of the title
548     * @return string DBkey (no namespace prefix)
549     */
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
568    /**
569     * Convert an input title or title prefix into a TitleValue.
570     *
571     * @since 1.35
572     * @param string $titlePart
573     * @param int $defaultNamespace Default namespace if none is given
574     * @return TitleValue
575     */
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
592    /**
593     * @param string $hash
594     * @return bool
595     */
596    public function validateSha1Hash( $hash ) {
597        return (bool)preg_match( '/^[a-f0-9]{40}$/', $hash );
598    }
599
600    /**
601     * @param string $hash
602     * @return bool
603     */
604    public function validateSha1Base36Hash( $hash ) {
605        return (bool)preg_match( '/^[a-z0-9]{31}$/', $hash );
606    }
607
608    /**
609     * Check whether the current user has permission to view revision-deleted
610     * fields.
611     * @return bool
612     */
613    public function userCanSeeRevDel() {
614        return $this->getAuthority()->isAllowedAny(
615            'deletedhistory',
616            'deletedtext',
617            'deleterevision',
618            'suppressrevision',
619            'viewsuppressed'
620        );
621    }
622
623    /**
624     * Preprocess the result set to fill the GenderCache with the necessary information
625     * before using self::addTitleInfo
626     *
627     * @param IResultWrapper $res Result set to work on.
628     *  The result set must have _namespace and _title fields with the provided field prefix
629     * @param string $fname The caller function name, always use __METHOD__ @phan-mandatory-param
630     * @param string $fieldPrefix Prefix for fields to check gender for
631     */
632    protected function executeGenderCacheFromResultWrapper(
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
666/** @deprecated class alias since 1.43 */
667class_alias( ApiQueryBase::class, 'ApiQueryBase' );