Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 157
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 / 156
0.00% covered (danger)
0.00%
0 / 35
7482
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
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 / 15
0.00% covered (danger)
0.00%
0 / 1
210
 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 ?IReadableDatabase $mDb;
35    /** @var array<string,IReadableDatabase> */
36    private array $virtualDBs;
37    private string|false $currentDomain;
38
39    /**
40     * @var SelectQueryBuilder|null
41     */
42    private $queryBuilder;
43
44    /**
45     * @stable to call
46     * @param ApiQuery $mQueryModule
47     * @param string $moduleName
48     * @param string $paramPrefix
49     */
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
64    /** @name   Methods to implement */
65
66    /**
67     * Get the cache mode for the data generated by this module. Override
68     * this in the module subclass. For possible return values and other
69     * details about cache modes, see ApiMain::setCacheMode()
70     *
71     * Public caching will only be allowed if *all* the modules that supply
72     * data for a given request return a cache mode of public.
73     *
74     * @stable to override
75     * @param array $params
76     * @return string
77     */
78    public function getCacheMode( $params ) {
79        return 'private';
80    }
81
82    /**
83     * Override this method to request extra fields from the pageSet
84     * using $pageSet->requestField('fieldName')
85     *
86     * Note this only makes sense for 'prop' modules, as 'list' and 'meta'
87     * modules should not be using the pageset.
88     *
89     * @stable to override
90     * @param ApiPageSet $pageSet
91     */
92    public function requestExtraData( $pageSet ) {
93    }
94
95    // endregion -- end of methods to implement
96
97    /***************************************************************************/
98    // region   Data access
99    /** @name   Data access */
100
101    /**
102     * Get the main Query module
103     * @return ApiQuery
104     */
105    public function getQuery() {
106        return $this->mQueryModule;
107    }
108
109    /** @inheritDoc */
110    public function getParent() {
111        return $this->getQuery();
112    }
113
114    /**
115     * Get the Query database connection (read-only). This will return the
116     * virtual database if a virtual domain is set, otherwise the main database.
117     *
118     * @stable to override
119     * @return IReadableDatabase
120     */
121    protected function getDB() {
122        if ( $this->currentDomain ) {
123            if ( !isset( $this->virtualDBs[$this->currentDomain] ) ) {
124                $db = MediaWikiServices::getInstance()
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
137    /**
138     * Set the Query database connection (read-only)
139     *
140     * @stable to override
141     * @param string|false $virtualDomain
142     */
143    protected function setVirtualDomain( string|false $virtualDomain ) {
144        $this->currentDomain = $virtualDomain;
145        $this->updateQueryBuilderConnection();
146    }
147
148    /**
149     * Reset the virtual domain to the main database
150     *
151     * @stable to override
152     */
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
164    /**
165     * Get the PageSet object to work on
166     * @stable to override
167     * @return ApiPageSet
168     */
169    protected function getPageSet() {
170        return $this->getQuery()->getPageSet();
171    }
172
173    // endregion -- end of data access
174
175    /***************************************************************************/
176    // region   Querying
177    /** @name   Querying */
178
179    /**
180     * Blank the internal arrays with query parameters
181     */
182    protected function resetQueryParams() {
183        $this->queryBuilder = null;
184    }
185
186    /**
187     * Get the SelectQueryBuilder.
188     *
189     * This is lazy initialised since getDB() fails in ApiQueryAllImages if it
190     * is called before the constructor completes.
191     *
192     * @return SelectQueryBuilder
193     */
194    protected function getQueryBuilder() {
195        $this->queryBuilder ??= $this->getDB()->newSelectQueryBuilder();
196        return $this->queryBuilder;
197    }
198
199    /**
200     * Add a set of tables to the internal array
201     * @param string|array $tables Table name or array of table names
202     *  or nested arrays for joins using parentheses for grouping
203     * @param string|null $alias Table alias, or null for no alias. Cannot be
204     *  used with multiple tables
205     */
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
217    /**
218     * Add a set of JOIN conditions to the internal array
219     *
220     * JOIN conditions are formatted as [ tablename => [ jointype, conditions ] ]
221     * e.g. [ 'page' => [ 'LEFT JOIN', 'page_id=rev_page' ] ].
222     * Conditions may be a string or an addWhere()-style array.
223     * @param array $join_conds JOIN conditions
224     */
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
232    /**
233     * Add a set of fields to select to the internal array
234     * @param array|string $value Field name or array of field names
235     */
236    protected function addFields( $value ) {
237        $this->getQueryBuilder()->fields( $value );
238    }
239
240    /**
241     * Same as addFields(), but add the fields only if a condition is met
242     * @param array|string $value See addFields()
243     * @param bool $condition If false, do nothing
244     * @return bool
245     */
246    protected function addFieldsIf( $value, $condition ) {
247        if ( $condition ) {
248            $this->addFields( $value );
249
250            return true;
251        }
252
253        return false;
254    }
255
256    /**
257     * Add a set of WHERE clauses to the internal array.
258     *
259     * The array should be appropriate for passing as $conds to
260     * IDatabase::select(). Arrays from multiple calls are merged with
261     * array_merge(). A string is treated as a single-element array.
262     *
263     * When passing `'field' => $arrayOfIDs` where the IDs are taken from user
264     * input, consider using addWhereIDsFld() instead.
265     *
266     * @see IDatabase::select()
267     * @param string|array|IExpression $value
268     */
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
281    /**
282     * Same as addWhere(), but add the WHERE clauses only if a condition is met
283     * @param string|array|IExpression $value
284     * @param bool $condition If false, do nothing
285     * @return bool
286     */
287    protected function addWhereIf( $value, $condition ) {
288        if ( $condition ) {
289            $this->addWhere( $value );
290
291            return true;
292        }
293
294        return false;
295    }
296
297    /**
298     * Equivalent to addWhere( [ $field => $value ] )
299     *
300     * When $value is an array of integer IDs taken from user input,
301     * consider using addWhereIDsFld() instead.
302     *
303     * @param string $field Field name
304     * @param int|string|(string|int|null)[] $value Value; ignored if null or empty array
305     */
306    protected function addWhereFld( $field, $value ) {
307        if ( $value !== null && !( is_array( $value ) && !$value ) ) {
308            $this->getQueryBuilder()->where( [ $field => $value ] );
309        }
310    }
311
312    /**
313     * Like addWhereFld for an integer list of IDs
314     *
315     * When passed wildly out-of-range values for integer comparison,
316     * the database may choose a poor query plan. This method validates the
317     * passed IDs against the range of values in the database to omit
318     * out-of-range values.
319     *
320     * This should be used when the IDs are derived from arbitrary user input;
321     * it is not necessary if the IDs are already known to be within a sensible
322     * range.
323     *
324     * This should not be used when there is not a suitable index on $field to
325     * quickly retrieve the minimum and maximum values.
326     *
327     * @since 1.33
328     * @param string $table Table name
329     * @param string $field Field name
330     * @param int[] $ids
331     * @return int Count of IDs actually included
332     */
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
349    /**
350     * Add a WHERE clause corresponding to a range, and an ORDER BY
351     * clause to sort in the right direction
352     * @param string $field Field name
353     * @param string $dir If 'newer', sort in ascending order, otherwise
354     *  sort in descending order
355     * @param string|int|null $start Value to start the list at. If $dir == 'newer'
356     *  this is the lower boundary, otherwise it's the upper boundary
357     * @param string|int|null $end Value to end the list at. If $dir == 'newer' this
358     *  is the upper boundary, otherwise it's the lower boundary
359     * @param bool $sort If false, don't add an ORDER BY clause
360     */
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        if ( // Warn if the range is impossible due to reversed boundaries
380            ( is_numeric( $start ) && is_numeric( $end ) ) && (
381            ( $isDirNewer && $start > $end ) ||
382            ( !$isDirNewer && $start < $end )
383        ) ) {
384            // If $isDirNewer:  $field >= $start AND $field <= $end
385            // If !$isDirNewer: $field <= $start AND $field >= $end
386            $op = $isDirNewer ? '<' : '>';
387            $this->addWarning( [ 'apiwarn-invalidrange', "$start $op $end" ] );
388        }
389    }
390
391    /**
392     * Add a WHERE clause corresponding to a range, similar to addWhereRange,
393     * but converts $start and $end to database timestamps.
394     * @see addWhereRange
395     * @param string $field
396     * @param string $dir
397     * @param string|int|null $start
398     * @param string|int|null $end
399     * @param bool $sort
400     */
401    protected function addTimestampWhereRange( $field, $dir, $start, $end, $sort = true ) {
402        $db = $this->getDB();
403        $this->addWhereRange( $field, $dir,
404            $db->timestampOrNull( $start ), $db->timestampOrNull( $end ), $sort );
405    }
406
407    /**
408     * Add an option such as LIMIT or USE INDEX. If an option was set
409     * before, the old value will be overwritten
410     * @param string $name Option name
411     * @param mixed $value The option value, or null for a boolean option
412     */
413    protected function addOption( $name, $value = null ) {
414        $this->getQueryBuilder()->option( $name, $value );
415    }
416
417    /**
418     * Execute a SELECT query based on the values in the internal arrays
419     * @param string $method Function the query should be attributed to.
420     *  You should usually use __METHOD__ here
421     * @param array $extraQuery Query data to add but not store in the object
422     *  Format is [
423     *    'tables' => ...,
424     *    'fields' => ...,
425     *    'where' => ...,
426     *    'options' => ...,
427     *    'join_conds' => ...
428     *  ]
429     * @param array|null &$hookData If set, the ApiQueryBaseBeforeQuery and
430     *  ApiQueryBaseAfterQuery hooks will be called, and the
431     *  ApiQueryBaseProcessRow hook will be expected.
432     * @return IResultWrapper
433     */
434    protected function select( $method, $extraQuery = [], ?array &$hookData = null ) {
435        $queryBuilder = clone $this->getQueryBuilder();
436        if ( isset( $extraQuery['tables'] ) ) {
437            $queryBuilder->rawTables( (array)$extraQuery['tables'] );
438        }
439        if ( isset( $extraQuery['fields'] ) ) {
440            $queryBuilder->fields( (array)$extraQuery['fields'] );
441        }
442        if ( isset( $extraQuery['where'] ) ) {
443            $queryBuilder->where( (array)$extraQuery['where'] );
444        }
445        if ( isset( $extraQuery['options'] ) ) {
446            $queryBuilder->options( (array)$extraQuery['options'] );
447        }
448        if ( isset( $extraQuery['join_conds'] ) ) {
449            $queryBuilder->joinConds( (array)$extraQuery['join_conds'] );
450        }
451
452        if ( $hookData !== null && $this->getHookContainer()->isRegistered( 'ApiQueryBaseBeforeQuery' ) ) {
453            $info = $queryBuilder->getQueryInfo();
454            $this->getHookRunner()->onApiQueryBaseBeforeQuery(
455                $this, $info['tables'], $info['fields'], $info['conds'],
456                $info['options'], $info['join_conds'], $hookData
457            );
458            $queryBuilder = $this->getDB()->newSelectQueryBuilder()->queryInfo( $info );
459        }
460
461        $queryBuilder->caller( $method );
462        $res = $queryBuilder->fetchResultSet();
463
464        if ( $hookData !== null ) {
465            $this->getHookRunner()->onApiQueryBaseAfterQuery( $this, $res, $hookData );
466        }
467
468        return $res;
469    }
470
471    /**
472     * Call the ApiQueryBaseProcessRow hook
473     *
474     * Generally, a module that passed $hookData to self::select() will call
475     * this just before calling ApiResult::addValue(), and treat a false return
476     * here in the same way it treats a false return from addValue().
477     *
478     * @since 1.28
479     * @param stdClass $row Database row
480     * @param array &$data Data to be added to the result
481     * @param array &$hookData Hook data from ApiQueryBase::select() @phan-output-reference
482     * @return bool Return false if row processing should end with continuation
483     */
484    protected function processRow( $row, array &$data, array &$hookData ) {
485        return $this->getHookRunner()->onApiQueryBaseProcessRow( $this, $row, $data, $hookData );
486    }
487
488    // endregion -- end of querying
489
490    /***************************************************************************/
491    // region   Utility methods
492    /** @name   Utility methods */
493
494    /**
495     * Add information (title and namespace) about a Title object to a
496     * result array
497     * @param array &$arr Result array à la ApiResult
498     * @param Title $title
499     * @param string $prefix Module prefix
500     */
501    public static function addTitleInfo( &$arr, $title, $prefix = '' ) {
502        $arr[$prefix . 'ns'] = $title->getNamespace();
503        $arr[$prefix . 'title'] = $title->getPrefixedText();
504    }
505
506    /**
507     * Add a sub-element under the page element with the given page ID
508     * @param int $pageId
509     * @param array $data Data array à la ApiResult
510     * @return bool Whether the element fit in the result
511     */
512    protected function addPageSubItems( $pageId, $data ) {
513        $result = $this->getResult();
514        ApiResult::setIndexedTagName( $data, $this->getModulePrefix() );
515
516        return $result->addValue( [ 'query', 'pages', (int)$pageId ],
517            $this->getModuleName(),
518            $data );
519    }
520
521    /**
522     * Same as addPageSubItems(), but one element of $data at a time
523     * @param int $pageId
524     * @param mixed $item Data à la ApiResult
525     * @param string|null $elemname XML element name. If null, getModuleName()
526     *  is used
527     * @return bool Whether the element fit in the result
528     */
529    protected function addPageSubItem( $pageId, $item, $elemname = null ) {
530        $result = $this->getResult();
531        $fit = $result->addValue( [ 'query', 'pages', $pageId,
532            $this->getModuleName() ], null, $item );
533        if ( !$fit ) {
534            return false;
535        }
536        $result->addIndexedTagName(
537            [ 'query', 'pages', $pageId, $this->getModuleName() ],
538            $elemname ?? $this->getModulePrefix()
539        );
540
541        return true;
542    }
543
544    /**
545     * Set a query-continue value
546     * @param string $paramName Parameter name
547     * @param int|string|array $paramValue Parameter value
548     */
549    protected function setContinueEnumParameter( $paramName, $paramValue ) {
550        $this->getContinuationManager()->addContinueParam( $this, $paramName, $paramValue );
551    }
552
553    /**
554     * Convert an input title or title prefix into a dbkey.
555     *
556     * $namespace should always be specified in order to handle per-namespace
557     * capitalization settings.
558     *
559     * @param string $titlePart
560     * @param int $namespace Namespace of the title
561     * @return string DBkey (no namespace prefix)
562     */
563    public function titlePartToKey( $titlePart, $namespace = NS_MAIN ) {
564        $t = Title::makeTitleSafe( $namespace, $titlePart . 'x' );
565        if ( !$t || $t->hasFragment() ) {
566            // Invalid title (e.g. bad chars) or contained a '#'.
567            $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
568        }
569        if ( $namespace != $t->getNamespace() || $t->isExternal() ) {
570            // This can happen in two cases. First, if you call titlePartToKey with a title part
571            // that looks like a namespace, but with $defaultNamespace = NS_MAIN. It would be very
572            // difficult to handle such a case. Such cases cannot exist and are therefore treated
573            // as invalid user input. The second case is when somebody specifies a title interwiki
574            // prefix.
575            $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
576        }
577
578        return substr( $t->getDBkey(), 0, -1 );
579    }
580
581    /**
582     * Convert an input title or title prefix into a TitleValue.
583     *
584     * @since 1.35
585     * @param string $titlePart
586     * @param int $defaultNamespace Default namespace if none is given
587     * @return TitleValue
588     */
589    protected function parsePrefixedTitlePart( $titlePart, $defaultNamespace = NS_MAIN ) {
590        try {
591            $titleParser = MediaWikiServices::getInstance()->getTitleParser();
592            $t = $titleParser->parseTitle( $titlePart . 'X', $defaultNamespace );
593        } catch ( MalformedTitleException ) {
594            $t = null;
595        }
596
597        if ( !$t || $t->hasFragment() || $t->isExternal() || $t->getDBkey() === 'X' ) {
598            // Invalid title (e.g. bad chars) or contained a '#'.
599            $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
600        }
601
602        return new TitleValue( $t->getNamespace(), substr( $t->getDBkey(), 0, -1 ) );
603    }
604
605    /**
606     * @param string $hash
607     * @return bool
608     */
609    public function validateSha1Hash( $hash ) {
610        return (bool)preg_match( '/^[a-f0-9]{40}$/', $hash );
611    }
612
613    /**
614     * @param string $hash
615     * @return bool
616     */
617    public function validateSha1Base36Hash( $hash ) {
618        return (bool)preg_match( '/^[a-z0-9]{31}$/', $hash );
619    }
620
621    /**
622     * Check whether the current user has permission to view revision-deleted
623     * fields.
624     * @return bool
625     */
626    public function userCanSeeRevDel() {
627        return $this->getAuthority()->isAllowedAny(
628            'deletedhistory',
629            'deletedtext',
630            'deleterevision',
631            'suppressrevision',
632            'viewsuppressed'
633        );
634    }
635
636    /**
637     * Preprocess the result set to fill the GenderCache with the necessary information
638     * before using self::addTitleInfo
639     *
640     * @param IResultWrapper $res Result set to work on.
641     *  The result set must have _namespace and _title fields with the provided field prefix
642     * @param string $fname The caller function name, always use __METHOD__ @phan-mandatory-param
643     * @param string $fieldPrefix Prefix for fields to check gender for
644     */
645    protected function executeGenderCacheFromResultWrapper(
646        IResultWrapper $res, $fname = __METHOD__, $fieldPrefix = 'page'
647    ) {
648        if ( !$res->numRows() ) {
649            return;
650        }
651
652        $services = MediaWikiServices::getInstance();
653        if ( !$services->getContentLanguage()->needsGenderDistinction() ) {
654            return;
655        }
656
657        $nsInfo = $services->getNamespaceInfo();
658        $namespaceField = $fieldPrefix . '_namespace';
659        $titleField = $fieldPrefix . '_title';
660
661        $usernames = [];
662        foreach ( $res as $row ) {
663            if ( $nsInfo->hasGenderDistinction( $row->$namespaceField ) ) {
664                $usernames[] = $row->$titleField;
665            }
666        }
667
668        if ( $usernames === [] ) {
669            return;
670        }
671
672        $genderCache = $services->getGenderCache();
673        $genderCache->doQuery( $usernames, $fname );
674    }
675
676    // endregion -- end of utility methods
677}
678
679/** @deprecated class alias since 1.43 */
680class_alias( ApiQueryBase::class, 'ApiQueryBase' );