Also for all sub-classes Remove simple doc-blocks without further information Change-Id: I981934efe32d44f52e5ab865a9b887be5bd0f41e
637 lines
19 KiB
PHP
637 lines
19 KiB
PHP
<?php
|
|
/**
|
|
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*
|
|
* @file
|
|
*/
|
|
|
|
namespace MediaWiki\Api;
|
|
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Title\MalformedTitleException;
|
|
use MediaWiki\Title\Title;
|
|
use MediaWiki\Title\TitleValue;
|
|
use stdClass;
|
|
use Wikimedia\Rdbms\IDatabase;
|
|
use Wikimedia\Rdbms\IExpression;
|
|
use Wikimedia\Rdbms\IReadableDatabase;
|
|
use Wikimedia\Rdbms\IResultWrapper;
|
|
use Wikimedia\Rdbms\SelectQueryBuilder;
|
|
|
|
/**
|
|
* This is a base class for all Query modules.
|
|
* It provides some common functionality such as constructing various SQL
|
|
* queries.
|
|
*
|
|
* @stable to extend
|
|
*
|
|
* @ingroup API
|
|
*/
|
|
abstract class ApiQueryBase extends ApiBase {
|
|
use ApiQueryBlockInfoTrait;
|
|
|
|
private ApiQuery $mQueryModule;
|
|
private ?IReadableDatabase $mDb;
|
|
|
|
/**
|
|
* @var SelectQueryBuilder|null
|
|
*/
|
|
private $queryBuilder;
|
|
|
|
/**
|
|
* @stable to call
|
|
* @param ApiQuery $queryModule
|
|
* @param string $moduleName
|
|
* @param string $paramPrefix
|
|
*/
|
|
public function __construct( ApiQuery $queryModule, string $moduleName, $paramPrefix = '' ) {
|
|
parent::__construct( $queryModule->getMain(), $moduleName, $paramPrefix );
|
|
$this->mQueryModule = $queryModule;
|
|
$this->mDb = null;
|
|
$this->resetQueryParams();
|
|
}
|
|
|
|
/***************************************************************************/
|
|
// region Methods to implement
|
|
/** @name Methods to implement */
|
|
|
|
/**
|
|
* Get the cache mode for the data generated by this module. Override
|
|
* this in the module subclass. For possible return values and other
|
|
* details about cache modes, see ApiMain::setCacheMode()
|
|
*
|
|
* Public caching will only be allowed if *all* the modules that supply
|
|
* data for a given request return a cache mode of public.
|
|
*
|
|
* @stable to override
|
|
* @param array $params
|
|
* @return string
|
|
*/
|
|
public function getCacheMode( $params ) {
|
|
return 'private';
|
|
}
|
|
|
|
/**
|
|
* Override this method to request extra fields from the pageSet
|
|
* using $pageSet->requestField('fieldName')
|
|
*
|
|
* Note this only makes sense for 'prop' modules, as 'list' and 'meta'
|
|
* modules should not be using the pageset.
|
|
*
|
|
* @stable to override
|
|
* @param ApiPageSet $pageSet
|
|
*/
|
|
public function requestExtraData( $pageSet ) {
|
|
}
|
|
|
|
// endregion -- end of methods to implement
|
|
|
|
/***************************************************************************/
|
|
// region Data access
|
|
/** @name Data access */
|
|
|
|
/**
|
|
* Get the main Query module
|
|
* @return ApiQuery
|
|
*/
|
|
public function getQuery() {
|
|
return $this->mQueryModule;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function getParent() {
|
|
return $this->getQuery();
|
|
}
|
|
|
|
/**
|
|
* Get the Query database connection (read-only)
|
|
* @stable to override
|
|
* @return IReadableDatabase
|
|
*/
|
|
protected function getDB() {
|
|
$this->mDb ??= $this->getQuery()->getDB();
|
|
|
|
return $this->mDb;
|
|
}
|
|
|
|
/**
|
|
* Get the PageSet object to work on
|
|
* @stable to override
|
|
* @return ApiPageSet
|
|
*/
|
|
protected function getPageSet() {
|
|
return $this->getQuery()->getPageSet();
|
|
}
|
|
|
|
// endregion -- end of data access
|
|
|
|
/***************************************************************************/
|
|
// region Querying
|
|
/** @name Querying */
|
|
|
|
/**
|
|
* Blank the internal arrays with query parameters
|
|
*/
|
|
protected function resetQueryParams() {
|
|
$this->queryBuilder = null;
|
|
}
|
|
|
|
/**
|
|
* Get the SelectQueryBuilder.
|
|
*
|
|
* This is lazy initialised since getDB() fails in ApiQueryAllImages if it
|
|
* is called before the constructor completes.
|
|
*
|
|
* @return SelectQueryBuilder
|
|
*/
|
|
protected function getQueryBuilder() {
|
|
$this->queryBuilder ??= $this->getDB()->newSelectQueryBuilder();
|
|
return $this->queryBuilder;
|
|
}
|
|
|
|
/**
|
|
* Add a set of tables to the internal array
|
|
* @param string|array $tables Table name or array of table names
|
|
* or nested arrays for joins using parentheses for grouping
|
|
* @param string|null $alias Table alias, or null for no alias. Cannot be
|
|
* used with multiple tables
|
|
*/
|
|
protected function addTables( $tables, $alias = null ) {
|
|
if ( is_array( $tables ) ) {
|
|
if ( $alias !== null ) {
|
|
ApiBase::dieDebug( __METHOD__, 'Multiple table aliases not supported' );
|
|
}
|
|
$this->getQueryBuilder()->rawTables( $tables );
|
|
} else {
|
|
$this->getQueryBuilder()->table( $tables, $alias );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a set of JOIN conditions to the internal array
|
|
*
|
|
* JOIN conditions are formatted as [ tablename => [ jointype, conditions ] ]
|
|
* e.g. [ 'page' => [ 'LEFT JOIN', 'page_id=rev_page' ] ].
|
|
* Conditions may be a string or an addWhere()-style array.
|
|
* @param array $join_conds JOIN conditions
|
|
*/
|
|
protected function addJoinConds( $join_conds ) {
|
|
if ( !is_array( $join_conds ) ) {
|
|
ApiBase::dieDebug( __METHOD__, 'Join conditions have to be arrays' );
|
|
}
|
|
$this->getQueryBuilder()->joinConds( $join_conds );
|
|
}
|
|
|
|
/**
|
|
* Add a set of fields to select to the internal array
|
|
* @param array|string $value Field name or array of field names
|
|
*/
|
|
protected function addFields( $value ) {
|
|
$this->getQueryBuilder()->fields( $value );
|
|
}
|
|
|
|
/**
|
|
* Same as addFields(), but add the fields only if a condition is met
|
|
* @param array|string $value See addFields()
|
|
* @param bool $condition If false, do nothing
|
|
* @return bool
|
|
*/
|
|
protected function addFieldsIf( $value, $condition ) {
|
|
if ( $condition ) {
|
|
$this->addFields( $value );
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add a set of WHERE clauses to the internal array.
|
|
*
|
|
* The array should be appropriate for passing as $conds to
|
|
* IDatabase::select(). Arrays from multiple calls are merged with
|
|
* array_merge(). A string is treated as a single-element array.
|
|
*
|
|
* When passing `'field' => $arrayOfIDs` where the IDs are taken from user
|
|
* input, consider using addWhereIDsFld() instead.
|
|
*
|
|
* @see IDatabase::select()
|
|
* @param string|array|IExpression $value
|
|
*/
|
|
protected function addWhere( $value ) {
|
|
if ( is_array( $value ) ) {
|
|
// Double check: don't insert empty arrays,
|
|
// Database::makeList() chokes on them
|
|
if ( count( $value ) ) {
|
|
$this->getQueryBuilder()->where( $value );
|
|
}
|
|
} else {
|
|
$this->getQueryBuilder()->where( $value );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Same as addWhere(), but add the WHERE clauses only if a condition is met
|
|
* @param string|array|IExpression $value
|
|
* @param bool $condition If false, do nothing
|
|
* @return bool
|
|
*/
|
|
protected function addWhereIf( $value, $condition ) {
|
|
if ( $condition ) {
|
|
$this->addWhere( $value );
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Equivalent to addWhere( [ $field => $value ] )
|
|
*
|
|
* When $value is an array of integer IDs taken from user input,
|
|
* consider using addWhereIDsFld() instead.
|
|
*
|
|
* @param string $field Field name
|
|
* @param int|string|(string|int|null)[] $value Value; ignored if null or empty array
|
|
*/
|
|
protected function addWhereFld( $field, $value ) {
|
|
if ( $value !== null && !( is_array( $value ) && !$value ) ) {
|
|
$this->getQueryBuilder()->where( [ $field => $value ] );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Like addWhereFld for an integer list of IDs
|
|
*
|
|
* When passed wildly out-of-range values for integer comparison,
|
|
* the database may choose a poor query plan. This method validates the
|
|
* passed IDs against the range of values in the database to omit
|
|
* out-of-range values.
|
|
*
|
|
* This should be used when the IDs are derived from arbitrary user input;
|
|
* it is not necessary if the IDs are already known to be within a sensible
|
|
* range.
|
|
*
|
|
* This should not be used when there is not a suitable index on $field to
|
|
* quickly retrieve the minimum and maximum values.
|
|
*
|
|
* @since 1.33
|
|
* @param string $table Table name
|
|
* @param string $field Field name
|
|
* @param int[] $ids
|
|
* @return int Count of IDs actually included
|
|
*/
|
|
protected function addWhereIDsFld( $table, $field, $ids ) {
|
|
// Use count() to its full documented capabilities to simultaneously
|
|
// test for null, empty array or empty countable object
|
|
if ( count( $ids ) ) {
|
|
$ids = $this->filterIDs( [ [ $table, $field ] ], $ids );
|
|
|
|
if ( $ids === [] ) {
|
|
// Return nothing, no IDs are valid
|
|
$this->getQueryBuilder()->where( '0 = 1' );
|
|
} else {
|
|
$this->getQueryBuilder()->where( [ $field => $ids ] );
|
|
}
|
|
}
|
|
return count( $ids );
|
|
}
|
|
|
|
/**
|
|
* Add a WHERE clause corresponding to a range, and an ORDER BY
|
|
* clause to sort in the right direction
|
|
* @param string $field Field name
|
|
* @param string $dir If 'newer', sort in ascending order, otherwise
|
|
* sort in descending order
|
|
* @param string|int|null $start Value to start the list at. If $dir == 'newer'
|
|
* this is the lower boundary, otherwise it's the upper boundary
|
|
* @param string|int|null $end Value to end the list at. If $dir == 'newer' this
|
|
* is the upper boundary, otherwise it's the lower boundary
|
|
* @param bool $sort If false, don't add an ORDER BY clause
|
|
*/
|
|
protected function addWhereRange( $field, $dir, $start, $end, $sort = true ) {
|
|
$isDirNewer = ( $dir === 'newer' );
|
|
$after = ( $isDirNewer ? '>=' : '<=' );
|
|
$before = ( $isDirNewer ? '<=' : '>=' );
|
|
$db = $this->getDB();
|
|
|
|
if ( $start !== null ) {
|
|
$this->addWhere( $db->expr( $field, $after, $start ) );
|
|
}
|
|
|
|
if ( $end !== null ) {
|
|
$this->addWhere( $db->expr( $field, $before, $end ) );
|
|
}
|
|
|
|
if ( $sort ) {
|
|
$this->getQueryBuilder()->orderBy( $field, $isDirNewer ? null : 'DESC' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a WHERE clause corresponding to a range, similar to addWhereRange,
|
|
* but converts $start and $end to database timestamps.
|
|
* @see addWhereRange
|
|
* @param string $field
|
|
* @param string $dir
|
|
* @param string|int|null $start
|
|
* @param string|int|null $end
|
|
* @param bool $sort
|
|
*/
|
|
protected function addTimestampWhereRange( $field, $dir, $start, $end, $sort = true ) {
|
|
$db = $this->getDB();
|
|
$this->addWhereRange( $field, $dir,
|
|
$db->timestampOrNull( $start ), $db->timestampOrNull( $end ), $sort );
|
|
}
|
|
|
|
/**
|
|
* Add an option such as LIMIT or USE INDEX. If an option was set
|
|
* before, the old value will be overwritten
|
|
* @param string $name Option name
|
|
* @param mixed $value The option value, or null for a boolean option
|
|
*/
|
|
protected function addOption( $name, $value = null ) {
|
|
$this->getQueryBuilder()->option( $name, $value );
|
|
}
|
|
|
|
/**
|
|
* Execute a SELECT query based on the values in the internal arrays
|
|
* @param string $method Function the query should be attributed to.
|
|
* You should usually use __METHOD__ here
|
|
* @param array $extraQuery Query data to add but not store in the object
|
|
* Format is [
|
|
* 'tables' => ...,
|
|
* 'fields' => ...,
|
|
* 'where' => ...,
|
|
* 'options' => ...,
|
|
* 'join_conds' => ...
|
|
* ]
|
|
* @param array|null &$hookData If set, the ApiQueryBaseBeforeQuery and
|
|
* ApiQueryBaseAfterQuery hooks will be called, and the
|
|
* ApiQueryBaseProcessRow hook will be expected.
|
|
* @return IResultWrapper
|
|
*/
|
|
protected function select( $method, $extraQuery = [], ?array &$hookData = null ) {
|
|
$queryBuilder = clone $this->getQueryBuilder();
|
|
if ( isset( $extraQuery['tables'] ) ) {
|
|
$queryBuilder->rawTables( (array)$extraQuery['tables'] );
|
|
}
|
|
if ( isset( $extraQuery['fields'] ) ) {
|
|
$queryBuilder->fields( (array)$extraQuery['fields'] );
|
|
}
|
|
if ( isset( $extraQuery['where'] ) ) {
|
|
$queryBuilder->where( (array)$extraQuery['where'] );
|
|
}
|
|
if ( isset( $extraQuery['options'] ) ) {
|
|
$queryBuilder->options( (array)$extraQuery['options'] );
|
|
}
|
|
if ( isset( $extraQuery['join_conds'] ) ) {
|
|
$queryBuilder->joinConds( (array)$extraQuery['join_conds'] );
|
|
}
|
|
|
|
if ( $hookData !== null && $this->getHookContainer()->isRegistered( 'ApiQueryBaseBeforeQuery' ) ) {
|
|
$info = $queryBuilder->getQueryInfo();
|
|
$this->getHookRunner()->onApiQueryBaseBeforeQuery(
|
|
$this, $info['tables'], $info['fields'], $info['conds'],
|
|
$info['options'], $info['join_conds'], $hookData
|
|
);
|
|
$queryBuilder = $this->getDB()->newSelectQueryBuilder()->queryInfo( $info );
|
|
}
|
|
|
|
$queryBuilder->caller( $method );
|
|
$res = $queryBuilder->fetchResultSet();
|
|
|
|
if ( $hookData !== null ) {
|
|
$this->getHookRunner()->onApiQueryBaseAfterQuery( $this, $res, $hookData );
|
|
}
|
|
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* Call the ApiQueryBaseProcessRow hook
|
|
*
|
|
* Generally, a module that passed $hookData to self::select() will call
|
|
* this just before calling ApiResult::addValue(), and treat a false return
|
|
* here in the same way it treats a false return from addValue().
|
|
*
|
|
* @since 1.28
|
|
* @param stdClass $row Database row
|
|
* @param array &$data Data to be added to the result
|
|
* @param array &$hookData Hook data from ApiQueryBase::select() @phan-output-reference
|
|
* @return bool Return false if row processing should end with continuation
|
|
*/
|
|
protected function processRow( $row, array &$data, array &$hookData ) {
|
|
return $this->getHookRunner()->onApiQueryBaseProcessRow( $this, $row, $data, $hookData );
|
|
}
|
|
|
|
// endregion -- end of querying
|
|
|
|
/***************************************************************************/
|
|
// region Utility methods
|
|
/** @name Utility methods */
|
|
|
|
/**
|
|
* Add information (title and namespace) about a Title object to a
|
|
* result array
|
|
* @param array &$arr Result array à la ApiResult
|
|
* @param Title $title
|
|
* @param string $prefix Module prefix
|
|
*/
|
|
public static function addTitleInfo( &$arr, $title, $prefix = '' ) {
|
|
$arr[$prefix . 'ns'] = $title->getNamespace();
|
|
$arr[$prefix . 'title'] = $title->getPrefixedText();
|
|
}
|
|
|
|
/**
|
|
* Add a sub-element under the page element with the given page ID
|
|
* @param int $pageId
|
|
* @param array $data Data array à la ApiResult
|
|
* @return bool Whether the element fit in the result
|
|
*/
|
|
protected function addPageSubItems( $pageId, $data ) {
|
|
$result = $this->getResult();
|
|
ApiResult::setIndexedTagName( $data, $this->getModulePrefix() );
|
|
|
|
return $result->addValue( [ 'query', 'pages', (int)$pageId ],
|
|
$this->getModuleName(),
|
|
$data );
|
|
}
|
|
|
|
/**
|
|
* Same as addPageSubItems(), but one element of $data at a time
|
|
* @param int $pageId
|
|
* @param mixed $item Data à la ApiResult
|
|
* @param string|null $elemname XML element name. If null, getModuleName()
|
|
* is used
|
|
* @return bool Whether the element fit in the result
|
|
*/
|
|
protected function addPageSubItem( $pageId, $item, $elemname = null ) {
|
|
$result = $this->getResult();
|
|
$fit = $result->addValue( [ 'query', 'pages', $pageId,
|
|
$this->getModuleName() ], null, $item );
|
|
if ( !$fit ) {
|
|
return false;
|
|
}
|
|
$result->addIndexedTagName(
|
|
[ 'query', 'pages', $pageId, $this->getModuleName() ],
|
|
$elemname ?? $this->getModulePrefix()
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set a query-continue value
|
|
* @param string $paramName Parameter name
|
|
* @param int|string|array $paramValue Parameter value
|
|
*/
|
|
protected function setContinueEnumParameter( $paramName, $paramValue ) {
|
|
$this->getContinuationManager()->addContinueParam( $this, $paramName, $paramValue );
|
|
}
|
|
|
|
/**
|
|
* Convert an input title or title prefix into a dbkey.
|
|
*
|
|
* $namespace should always be specified in order to handle per-namespace
|
|
* capitalization settings.
|
|
*
|
|
* @param string $titlePart
|
|
* @param int $namespace Namespace of the title
|
|
* @return string DBkey (no namespace prefix)
|
|
*/
|
|
public function titlePartToKey( $titlePart, $namespace = NS_MAIN ) {
|
|
$t = Title::makeTitleSafe( $namespace, $titlePart . 'x' );
|
|
if ( !$t || $t->hasFragment() ) {
|
|
// Invalid title (e.g. bad chars) or contained a '#'.
|
|
$this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
|
|
}
|
|
if ( $namespace != $t->getNamespace() || $t->isExternal() ) {
|
|
// This can happen in two cases. First, if you call titlePartToKey with a title part
|
|
// that looks like a namespace, but with $defaultNamespace = NS_MAIN. It would be very
|
|
// difficult to handle such a case. Such cases cannot exist and are therefore treated
|
|
// as invalid user input. The second case is when somebody specifies a title interwiki
|
|
// prefix.
|
|
$this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
|
|
}
|
|
|
|
return substr( $t->getDBkey(), 0, -1 );
|
|
}
|
|
|
|
/**
|
|
* Convert an input title or title prefix into a TitleValue.
|
|
*
|
|
* @since 1.35
|
|
* @param string $titlePart
|
|
* @param int $defaultNamespace Default namespace if none is given
|
|
* @return TitleValue
|
|
*/
|
|
protected function parsePrefixedTitlePart( $titlePart, $defaultNamespace = NS_MAIN ) {
|
|
try {
|
|
$titleParser = MediaWikiServices::getInstance()->getTitleParser();
|
|
$t = $titleParser->parseTitle( $titlePart . 'X', $defaultNamespace );
|
|
} catch ( MalformedTitleException $e ) {
|
|
$t = null;
|
|
}
|
|
|
|
if ( !$t || $t->hasFragment() || $t->isExternal() || $t->getDBkey() === 'X' ) {
|
|
// Invalid title (e.g. bad chars) or contained a '#'.
|
|
$this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titlePart ) ] );
|
|
}
|
|
|
|
return new TitleValue( $t->getNamespace(), substr( $t->getDBkey(), 0, -1 ) );
|
|
}
|
|
|
|
/**
|
|
* @param string $hash
|
|
* @return bool
|
|
*/
|
|
public function validateSha1Hash( $hash ) {
|
|
return (bool)preg_match( '/^[a-f0-9]{40}$/', $hash );
|
|
}
|
|
|
|
/**
|
|
* @param string $hash
|
|
* @return bool
|
|
*/
|
|
public function validateSha1Base36Hash( $hash ) {
|
|
return (bool)preg_match( '/^[a-z0-9]{31}$/', $hash );
|
|
}
|
|
|
|
/**
|
|
* Check whether the current user has permission to view revision-deleted
|
|
* fields.
|
|
* @return bool
|
|
*/
|
|
public function userCanSeeRevDel() {
|
|
return $this->getAuthority()->isAllowedAny(
|
|
'deletedhistory',
|
|
'deletedtext',
|
|
'deleterevision',
|
|
'suppressrevision',
|
|
'viewsuppressed'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Preprocess the result set to fill the GenderCache with the necessary information
|
|
* before using self::addTitleInfo
|
|
*
|
|
* @param IResultWrapper $res Result set to work on.
|
|
* The result set must have _namespace and _title fields with the provided field prefix
|
|
* @param string $fname The caller function name, always use __METHOD__ @phan-mandatory-param
|
|
* @param string $fieldPrefix Prefix for fields to check gender for
|
|
*/
|
|
protected function executeGenderCacheFromResultWrapper(
|
|
IResultWrapper $res, $fname = __METHOD__, $fieldPrefix = 'page'
|
|
) {
|
|
if ( !$res->numRows() ) {
|
|
return;
|
|
}
|
|
|
|
$services = MediaWikiServices::getInstance();
|
|
if ( !$services->getContentLanguage()->needsGenderDistinction() ) {
|
|
return;
|
|
}
|
|
|
|
$nsInfo = $services->getNamespaceInfo();
|
|
$namespaceField = $fieldPrefix . '_namespace';
|
|
$titleField = $fieldPrefix . '_title';
|
|
|
|
$usernames = [];
|
|
foreach ( $res as $row ) {
|
|
if ( $nsInfo->hasGenderDistinction( $row->$namespaceField ) ) {
|
|
$usernames[] = $row->$titleField;
|
|
}
|
|
}
|
|
|
|
if ( $usernames === [] ) {
|
|
return;
|
|
}
|
|
|
|
$genderCache = $services->getGenderCache();
|
|
$genderCache->doQuery( $usernames, $fname );
|
|
}
|
|
|
|
// endregion -- end of utility methods
|
|
}
|
|
|
|
/** @deprecated class alias since 1.43 */
|
|
class_alias( ApiQueryBase::class, 'ApiQueryBase' );
|