Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
51.86% covered (warning)
51.86%
349 / 673
47.44% covered (danger)
47.44%
37 / 78
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiBase
51.86% covered (warning)
51.86%
349 / 673
47.44% covered (danger)
47.44%
37 / 78
7110.67
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 execute
n/a
0 / 0
n/a
0 / 0
0
 getModuleManager
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCustomPrinter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 shouldCheckMaxlag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isReadMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mustBePosted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDeprecated
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isInternal
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWebUITokenSalt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getConditionalRequestData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getModuleName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getModulePrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMain
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isMain
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 dieIfMain
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 lacksSameOriginSecurity
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getModulePath
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 getModuleFromPath
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
56
 getResult
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getErrorFormatter
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDB
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getContinuationManager
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setContinuationManager
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getPermissionManager
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHookContainer
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getHookRunner
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 dynamicParameterDocumentation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 encodeParamName
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
2.86
 extractRequestParams
96.77% covered (success)
96.77%
60 / 62
0.00% covered (danger)
0.00%
0 / 1
21
 getParameter
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 requireOnlyOneParameter
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
3
 requireMaxOneParameter
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 requireAtLeastOneParameter
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 requirePostedParameters
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 parameterNotEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getTitleOrPageId
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
9
 getTitleFromTitleOrPageId
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getParameterFromSettings
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 handleParamNormalization
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validateToken
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 getWatchlistUser
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 makeMessage
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
5.20
 errorArrayToStatus
89.47% covered (warning)
89.47%
17 / 19
0.00% covered (danger)
0.00%
0 / 1
7.06
 addBlockInfoToStatus
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 useTransactionalTimeLimit
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 clearCacheForTest
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 filterIDs
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 addWarning
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addDeprecation
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
3.04
 addError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addMessagesFromStatus
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 dieWithError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dieWithException
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 dieBlocked
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 dieStatus
89.47% covered (warning)
89.47%
17 / 19
0.00% covered (danger)
0.00%
0 / 1
10.12
 dieReadOnly
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 checkUserRightsAny
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 checkTitleUserPermissions
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 dieWithErrorOrDebug
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 parseContinueParamOrDie
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
 dieContinueUsageIf
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 dieDebug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 logFeatureUsage
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
2
 getSummaryMessage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExtendedDescription
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getFinalSummary
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getFinalDescription
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 getFinalParams
33.33% covered (danger)
33.33%
5 / 15
0.00% covered (danger)
0.00%
0 / 1
5.67
 getFinalParamDescription
47.00% covered (danger)
47.00%
47 / 100
0.00% covered (danger)
0.00%
0 / 1
135.53
 getHelpFlags
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 getModuleSourceInfo
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
240
 modifyHelp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2006, 2010 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23use MediaWiki\Api\ApiHookRunner;
24use MediaWiki\Api\Validator\SubmoduleDef;
25use MediaWiki\Block\Block;
26use MediaWiki\Context\ContextSource;
27use MediaWiki\Context\IContextSource;
28use MediaWiki\HookContainer\HookContainer;
29use MediaWiki\Language\RawMessage;
30use MediaWiki\MainConfigNames;
31use MediaWiki\MediaWikiServices;
32use MediaWiki\Message\Message;
33use MediaWiki\Page\PageIdentity;
34use MediaWiki\ParamValidator\TypeDef\NamespaceDef;
35use MediaWiki\Permissions\Authority;
36use MediaWiki\Permissions\PermissionManager;
37use MediaWiki\Permissions\PermissionStatus;
38use MediaWiki\Specials\SpecialVersion;
39use MediaWiki\Status\Status;
40use MediaWiki\Title\Title;
41use MediaWiki\User\User;
42use MediaWiki\User\UserRigorOptions;
43use Wikimedia\ParamValidator\ParamValidator;
44use Wikimedia\ParamValidator\TypeDef\EnumDef;
45use Wikimedia\ParamValidator\TypeDef\IntegerDef;
46use Wikimedia\ParamValidator\TypeDef\StringDef;
47use Wikimedia\Rdbms\IReadableDatabase;
48use Wikimedia\Timestamp\TimestampException;
49
50/**
51 * This abstract class implements many basic API functions, and is the base of
52 * all API classes.
53 *
54 * The class functions are divided into several areas of functionality:
55 *
56 * Module parameters: Derived classes can define getAllowedParams() to specify
57 *  which parameters to expect, how to parse and validate them.
58 *
59 * Self-documentation: code to allow the API to document its own state
60 *
61 * @stable to extend
62 *
63 * @ingroup API
64 */
65abstract class ApiBase extends ContextSource {
66
67    use ApiBlockInfoTrait;
68
69    /** @var HookContainer */
70    private $hookContainer;
71
72    /** @var ApiHookRunner */
73    private $hookRunner;
74
75    /**
76     * @name   Old constants for ::getAllowedParams() arrays
77     * @{
78     */
79
80    /**
81     * @deprecated since 1.35, use ParamValidator::PARAM_DEFAULT instead
82     */
83    public const PARAM_DFLT = ParamValidator::PARAM_DEFAULT;
84    /**
85     * @deprecated since 1.35, use ParamValidator::PARAM_ISMULTI instead
86     */
87    public const PARAM_ISMULTI = ParamValidator::PARAM_ISMULTI;
88    /**
89     * @deprecated since 1.35, use ParamValidator::PARAM_TYPE instead
90     */
91    public const PARAM_TYPE = ParamValidator::PARAM_TYPE;
92    /**
93     * @deprecated since 1.35, use IntegerDef::PARAM_MAX instead
94     */
95    public const PARAM_MAX = IntegerDef::PARAM_MAX;
96    /**
97     * @deprecated since 1.35, use IntegerDef::PARAM_MAX2 instead
98     */
99    public const PARAM_MAX2 = IntegerDef::PARAM_MAX2;
100    /**
101     * @deprecated since 1.35, use IntegerDef::PARAM_MIN instead
102     */
103    public const PARAM_MIN = IntegerDef::PARAM_MIN;
104    /**
105     * @deprecated since 1.35, use ParamValidator::PARAM_ALLOW_DUPLICATES instead
106     */
107    public const PARAM_ALLOW_DUPLICATES = ParamValidator::PARAM_ALLOW_DUPLICATES;
108    /**
109     * @deprecated since 1.35, use ParamValidator::PARAM_DEPRECATED instead
110     */
111    public const PARAM_DEPRECATED = ParamValidator::PARAM_DEPRECATED;
112    /**
113     * @deprecated since 1.35, use ParamValidator::PARAM_REQUIRED instead
114     */
115    public const PARAM_REQUIRED = ParamValidator::PARAM_REQUIRED;
116    /**
117     * @deprecated since 1.35, use SubmoduleDef::PARAM_SUBMODULE_MAP instead
118     */
119    public const PARAM_SUBMODULE_MAP = SubmoduleDef::PARAM_SUBMODULE_MAP;
120    /**
121     * @deprecated since 1.35, use SubmoduleDef::PARAM_SUBMODULE_PARAM_PREFIX instead
122     */
123    public const PARAM_SUBMODULE_PARAM_PREFIX = SubmoduleDef::PARAM_SUBMODULE_PARAM_PREFIX;
124    /**
125     * @deprecated since 1.35, use ParamValidator::PARAM_ALL instead
126     */
127    public const PARAM_ALL = ParamValidator::PARAM_ALL;
128    /**
129     * @deprecated since 1.35, use NamespaceDef::PARAM_EXTRA_NAMESPACES instead
130     */
131    public const PARAM_EXTRA_NAMESPACES = NamespaceDef::PARAM_EXTRA_NAMESPACES;
132    /**
133     * @deprecated since 1.35, use ParamValidator::PARAM_SENSITIVE instead
134     */
135    public const PARAM_SENSITIVE = ParamValidator::PARAM_SENSITIVE;
136    /**
137     * @deprecated since 1.35, use EnumDef::PARAM_DEPRECATED_VALUES instead
138     */
139    public const PARAM_DEPRECATED_VALUES = EnumDef::PARAM_DEPRECATED_VALUES;
140    /**
141     * @deprecated since 1.35, use ParamValidator::PARAM_ISMULTI_LIMIT1 instead
142     */
143    public const PARAM_ISMULTI_LIMIT1 = ParamValidator::PARAM_ISMULTI_LIMIT1;
144    /**
145     * @deprecated since 1.35, use ParamValidator::PARAM_ISMULTI_LIMIT2 instead
146     */
147    public const PARAM_ISMULTI_LIMIT2 = ParamValidator::PARAM_ISMULTI_LIMIT2;
148    /**
149     * @deprecated since 1.35, use StringDef::PARAM_MAX_BYTES instead
150     */
151    public const PARAM_MAX_BYTES = StringDef::PARAM_MAX_BYTES;
152    /**
153     * @deprecated since 1.35, use StringDef::PARAM_MAX_CHARS instead
154     */
155    public const PARAM_MAX_CHARS = StringDef::PARAM_MAX_CHARS;
156    /** @} */
157
158    /**
159     * (boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
160     * @deprecated since 1.35
161     */
162    public const PARAM_RANGE_ENFORCE = 'api-param-range-enforce';
163
164    // region   API-specific constants for ::getAllowedParams() arrays
165    /** @name   API-specific constants for ::getAllowedParams() arrays */
166
167    /**
168     * (string|array|Message) Specify an alternative i18n documentation message
169     * for this parameter. Default is apihelp-{$path}-param-{$param}.
170     * @since 1.25
171     */
172    public const PARAM_HELP_MSG = 'api-param-help-msg';
173
174    /**
175     * ((string|array|Message)[]) Specify additional i18n messages to append to
176     * the normal message for this parameter.
177     * @since 1.25
178     */
179    public const PARAM_HELP_MSG_APPEND = 'api-param-help-msg-append';
180
181    /**
182     * (array) Specify additional information tags for the parameter.
183     * The value is an array of arrays, with the first member being the 'tag' for the info
184     * and the remaining members being the values. In the help, this is
185     * formatted using apihelp-{$path}-paraminfo-{$tag}, which is passed
186     * $1 = count, $2 = comma-joined list of values, $3 = module prefix.
187     * @since 1.25
188     */
189    public const PARAM_HELP_MSG_INFO = 'api-param-help-msg-info';
190
191    /**
192     * Deprecated and unused.
193     * @since 1.25
194     * @deprecated since 1.35
195     */
196    public const PARAM_VALUE_LINKS = 'api-param-value-links';
197
198    /**
199     * ((string|array|Message)[]) When PARAM_TYPE is an array, or 'string'
200     * with PARAM_ISMULTI, this is an array mapping parameter values to help
201     * message specifiers (to be passed to ApiBase::makeMessage()) about
202     * those values.
203     *
204     * When PARAM_TYPE is an array, any value not having a mapping will use
205     * the apihelp-{$path}-paramvalue-{$param}-{$value} message. (This means
206     * you can use an empty array to use the default message key for all
207     * values.)
208     *
209     * @since 1.25
210     * @note Use with PARAM_TYPE = 'string' is allowed since 1.40.
211     */
212    public const PARAM_HELP_MSG_PER_VALUE = 'api-param-help-msg-per-value';
213
214    /**
215     * (array) Indicate that this is a templated parameter, and specify replacements. Keys are the
216     * placeholders in the parameter name and values are the names of (unprefixed) parameters from
217     * which the replacement values are taken.
218     *
219     * For example, a parameter "foo-{ns}-{title}" could be defined with
220     * PARAM_TEMPLATE_VARS => [ 'ns' => 'namespaces', 'title' => 'titles' ]. Then a query for
221     * namespaces=0|1&titles=X|Y would support parameters foo-0-X, foo-0-Y, foo-1-X, and foo-1-Y.
222     *
223     * All placeholders must be present in the parameter's name. Each target parameter must have
224     * PARAM_ISMULTI true. If a target is itself a templated parameter, its PARAM_TEMPLATE_VARS must
225     * be a subset of the referring parameter's, mapping the same placeholders to the same targets.
226     * A parameter cannot target itself.
227     *
228     * @since 1.32
229     */
230    public const PARAM_TEMPLATE_VARS = 'param-template-vars';
231
232    // endregion -- end of API-specific constants for ::getAllowedParams() arrays
233
234    public const ALL_DEFAULT_STRING = '*';
235
236    /** Fast query, standard limit. */
237    public const LIMIT_BIG1 = 500;
238    /** Fast query, apihighlimits limit. */
239    public const LIMIT_BIG2 = 5000;
240    /** Slow query, standard limit. */
241    public const LIMIT_SML1 = 50;
242    /** Slow query, apihighlimits limit. */
243    public const LIMIT_SML2 = 500;
244
245    /**
246     * getAllowedParams() flag: When this is set, the result could take longer to generate,
247     * but should be more thorough. E.g. get the list of generators for ApiSandBox extension
248     * @since 1.21
249     */
250    public const GET_VALUES_FOR_HELP = 1;
251
252    /** @var array Maps extension paths to info arrays */
253    private static $extensionInfo = null;
254
255    /** @var stdClass[][] Cache for self::filterIDs() */
256    private static $filterIDsCache = [];
257
258    /** @var array Map of web UI block messages which magically gain machine-readable block info */
259    private const BLOCK_CODE_MAP = [
260        'blockedtext' => true,
261        'blockedtext-partial' => true,
262        'autoblockedtext' => true,
263        'systemblockedtext' => true,
264        'blockedtext-composite' => true,
265        'blockedtext-tempuser' => true,
266        'autoblockedtext-tempuser' => true,
267    ];
268
269    /** @var array Map of web UI block messages to corresponding API messages and codes */
270    private const MESSAGE_CODE_MAP = [
271        'actionthrottled' => [ 'apierror-ratelimited', 'ratelimited' ],
272        'actionthrottledtext' => [ 'apierror-ratelimited', 'ratelimited' ],
273    ];
274
275    /** @var ApiMain */
276    private $mMainModule;
277
278    // Adding inline type hints for these two fields is non-trivial because
279    // of tests that create mocks for ApiBase subclasses and use
280    // disableOriginalConstructor(): in those cases the constructor here is never
281    // hit and thus these will be empty and any uses will raise a "Typed property
282    // must not be accessed before initialization" error.
283    /** @var string */
284    private $mModuleName;
285    /** @var string */
286    private $mModulePrefix;
287
288    private $mReplicaDB = null;
289    /**
290     * @var array
291     */
292    private $mParamCache = [];
293    /** @var array|null|false */
294    private $mModuleSource = false;
295
296    /**
297     * @stable to call
298     * @param ApiMain $mainModule
299     * @param string $moduleName Name of this module
300     * @param string $modulePrefix Prefix to use for parameter names
301     */
302    public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
303        $this->mMainModule = $mainModule;
304        $this->mModuleName = $moduleName;
305        $this->mModulePrefix = $modulePrefix;
306
307        if ( !$this->isMain() ) {
308            $this->setContext( $mainModule->getContext() );
309        }
310    }
311
312    /***************************************************************************/
313    // region   Methods to implement
314    /** @name   Methods to implement */
315
316    /**
317     * Evaluates the parameters, performs the requested query, and sets up
318     * the result. Concrete implementations of ApiBase must override this
319     * method to provide whatever functionality their module offers.
320     * Implementations must not produce any output on their own and are not
321     * expected to handle any errors.
322     *
323     * The execute() method will be invoked directly by ApiMain immediately
324     * before the result of the module is output. Aside from the
325     * constructor, implementations should assume that no other methods
326     * will be called externally on the module before the result is
327     * processed.
328     *
329     * The result data should be stored in the ApiResult object available
330     * through getResult().
331     */
332    abstract public function execute();
333
334    /**
335     * Get the module manager, or null if this module has no submodules.
336     *
337     * @since 1.21
338     * @stable to override
339     * @return ApiModuleManager|null
340     */
341    public function getModuleManager() {
342        return null;
343    }
344
345    /**
346     * If the module may only be used with a certain format module,
347     * it should override this method to return an instance of that formatter.
348     * A value of null means the default format will be used.
349     *
350     * @note Do not use this just because you don't want to support non-json
351     * formats. This should be used only when there is a fundamental
352     * requirement for a specific format.
353     *
354     * @stable to override
355     * @return ApiFormatBase|null An instance of a class derived from ApiFormatBase, or null
356     */
357    public function getCustomPrinter() {
358        return null;
359    }
360
361    /**
362     * Returns usage examples for this module.
363     *
364     * Return value has query strings as keys, with values being either strings
365     * (message key), arrays (message key + parameter), or Message objects.
366     *
367     * Do not call this base class implementation when overriding this method.
368     *
369     * @since 1.25
370     * @stable to override
371     * @return array
372     */
373    protected function getExamplesMessages() {
374        return [];
375    }
376
377    /**
378     * Return links to more detailed help pages about the module.
379     *
380     * @since 1.25, returning boolean false is deprecated
381     * @stable to override
382     * @return string|array
383     */
384    public function getHelpUrls() {
385        return [];
386    }
387
388    /**
389     * Returns an array of allowed parameters (parameter name) => (default
390     * value) or (parameter name) => (array with PARAM_* constants as keys)
391     * Don't call this function directly: use getFinalParams() to allow
392     * hooks to modify parameters as needed.
393     *
394     * Some derived classes may choose to handle an integer $flags parameter
395     * in the overriding methods. Callers of this method can pass zero or
396     * more OR-ed flags like GET_VALUES_FOR_HELP.
397     *
398     * @stable to override
399     * @return array
400     */
401    protected function getAllowedParams( /* $flags = 0 */ ) {
402        // $flags is not declared because it causes "Strict standards"
403        // warning. Most derived classes do not implement it.
404        return [];
405    }
406
407    /**
408     * Indicates if this module needs maxlag to be checked.
409     *
410     * @stable to override
411     * @return bool
412     */
413    public function shouldCheckMaxlag() {
414        return true;
415    }
416
417    /**
418     * Indicates whether this module requires read rights.
419     *
420     * @stable to override
421     * @return bool
422     */
423    public function isReadMode() {
424        return true;
425    }
426
427    /**
428     * Indicates whether this module requires write mode.
429     *
430     * This should return true for modules that may require synchronous database writes.
431     * Modules that do not need such writes should also not rely on primary database access,
432     * since only read queries are needed and each primary DB is a single point of failure.
433     * Additionally, requests that only need replica DBs can be efficiently routed to any
434     * datacenter via the Promise-Non-Write-API-Action header.
435     *
436     * @stable to override
437     * @return bool
438     */
439    public function isWriteMode() {
440        return false;
441    }
442
443    /**
444     * Indicates whether this module must be called with a POST request.
445     *
446     * @stable to override
447     * @return bool
448     */
449    public function mustBePosted() {
450        return $this->needsToken() !== false;
451    }
452
453    /**
454     * Indicates whether this module is deprecated.
455     *
456     * @since 1.25
457     * @stable to override
458     * @return bool
459     */
460    public function isDeprecated() {
461        return false;
462    }
463
464    /**
465     * Indicates whether this module is considered to be "internal".
466     *
467     * Internal API modules are not (yet) intended for 3rd party use and may be unstable.
468     *
469     * @since 1.25
470     * @stable to override
471     * @return bool
472     */
473    public function isInternal() {
474        return false;
475    }
476
477    /**
478     * Returns the token type this module requires in order to execute.
479     *
480     * Modules are strongly encouraged to use the core 'csrf' type unless they
481     * have specialized security needs. If the token type is not one of the
482     * core types, you must use the ApiQueryTokensRegisterTypes hook to
483     * register it.
484     *
485     * Returning a non-falsey value here will force the addition of an
486     * appropriate 'token' parameter in self::getFinalParams(). Also,
487     * self::mustBePosted() must return true when tokens are used.
488     *
489     * In previous versions of MediaWiki, true was a valid return value.
490     * Returning true will generate errors indicating that the API module needs
491     * updating.
492     *
493     * @stable to override
494     * @return string|false
495     */
496    public function needsToken() {
497        return false;
498    }
499
500    /**
501     * Fetch the salt used in the Web UI corresponding to this module.
502     *
503     * Only override this if the Web UI uses a token with a non-constant salt.
504     *
505     * @since 1.24
506     * @param array $params All supplied parameters for the module
507     * @stable to override
508     * @return string|array|null
509     */
510    protected function getWebUITokenSalt( array $params ) {
511        return null;
512    }
513
514    /**
515     * Returns data for HTTP conditional request mechanisms.
516     *
517     * @since 1.26
518     * @stable to override
519     * @param string $condition Condition being queried:
520     *  - last-modified: Return a timestamp representing the maximum of the
521     *    last-modified dates for all resources involved in the request. See
522     *    RFC 7232 § 2.2 for semantics.
523     *  - etag: Return an entity-tag representing the state of all resources involved
524     *    in the request. Quotes must be included. See RFC 7232 § 2.3 for semantics.
525     * @return string|bool|null As described above, or null if no value is available.
526     */
527    public function getConditionalRequestData( $condition ) {
528        return null;
529    }
530
531    // endregion -- end of methods to implement
532
533    /***************************************************************************/
534    // region   Data access methods
535    /** @name   Data access methods */
536
537    /**
538     * Get the name of the module being executed by this instance.
539     *
540     * @return string
541     */
542    public function getModuleName() {
543        return $this->mModuleName;
544    }
545
546    /**
547     * Get parameter prefix (usually two letters or an empty string).
548     *
549     * @return string
550     */
551    public function getModulePrefix() {
552        return $this->mModulePrefix;
553    }
554
555    /**
556     * Get the main module.
557     *
558     * @return ApiMain
559     */
560    public function getMain() {
561        return $this->mMainModule;
562    }
563
564    /**
565     * Returns true if this module is the main module ($this === $this->mMainModule),
566     * false otherwise.
567     *
568     * @return bool
569     */
570    public function isMain() {
571        return $this === $this->mMainModule;
572    }
573
574    /**
575     * Get the parent of this module.
576     *
577     * @stable to override
578     * @since 1.25
579     * @return ApiBase|null
580     */
581    public function getParent() {
582        return $this->isMain() ? null : $this->getMain();
583    }
584
585    /**
586     * Used to avoid infinite loops - the ApiMain class should override some
587     * methods, if it doesn't and uses the default ApiBase implementation, which
588     * just calls the same method for the ApiMain instance, it'll lead to an infinite loop
589     *
590     * @param string $methodName used for debug messages
591     */
592    private function dieIfMain( string $methodName ) {
593        if ( $this->isMain() ) {
594            self::dieDebug( $methodName, 'base method was called on main module.' );
595        }
596    }
597
598    /**
599     * Returns true if the current request breaks the same-origin policy.
600     *
601     * For example, json with callbacks.
602     *
603     * https://en.wikipedia.org/wiki/Same-origin_policy
604     *
605     * @since 1.25
606     * @return bool
607     */
608    public function lacksSameOriginSecurity() {
609        // The Main module has this method overridden, avoid infinite loops
610        $this->dieIfMain( __METHOD__ );
611
612        return $this->getMain()->lacksSameOriginSecurity();
613    }
614
615    /**
616     * Get the path to this module.
617     *
618     * @since 1.25
619     * @return string
620     */
621    public function getModulePath() {
622        if ( $this->isMain() ) {
623            return 'main';
624        }
625
626        if ( $this->getParent()->isMain() ) {
627            return $this->getModuleName();
628        }
629
630        return $this->getParent()->getModulePath() . '+' . $this->getModuleName();
631    }
632
633    /**
634     * Get a module from its module path.
635     *
636     * @since 1.25
637     * @param string $path
638     * @return ApiBase|null
639     * @throws ApiUsageException
640     */
641    public function getModuleFromPath( $path ) {
642        $module = $this->getMain();
643        if ( $path === 'main' ) {
644            return $module;
645        }
646
647        $parts = explode( '+', $path );
648        if ( count( $parts ) === 1 ) {
649            // In case the '+' was typed into URL, it resolves as a space
650            $parts = explode( ' ', $path );
651        }
652
653        foreach ( $parts as $i => $v ) {
654            $parent = $module;
655            $manager = $parent->getModuleManager();
656            if ( $manager === null ) {
657                $errorPath = implode( '+', array_slice( $parts, 0, $i ) );
658                $this->dieWithError( [ 'apierror-badmodule-nosubmodules', $errorPath ], 'badmodule' );
659            }
660            $module = $manager->getModule( $v );
661
662            if ( $module === null ) {
663                $errorPath = $i
664                    ? implode( '+', array_slice( $parts, 0, $i ) )
665                    : $parent->getModuleName();
666                $this->dieWithError(
667                    [ 'apierror-badmodule-badsubmodule', $errorPath, wfEscapeWikiText( $v ) ],
668                    'badmodule'
669                );
670            }
671        }
672
673        return $module;
674    }
675
676    /**
677     * Get the result object.
678     *
679     * @return ApiResult
680     */
681    public function getResult() {
682        // The Main module has this method overridden, avoid infinite loops
683        $this->dieIfMain( __METHOD__ );
684
685        return $this->getMain()->getResult();
686    }
687
688    /**
689     * @stable to override
690     * @return ApiErrorFormatter
691     */
692    public function getErrorFormatter() {
693        // The Main module has this method overridden, avoid infinite loops
694        $this->dieIfMain( __METHOD__ );
695
696        return $this->getMain()->getErrorFormatter();
697    }
698
699    /**
700     * Gets a default replica DB connection object.
701     *
702     * @stable to override
703     * @return IReadableDatabase
704     */
705    protected function getDB() {
706        if ( !isset( $this->mReplicaDB ) ) {
707            $this->mReplicaDB = MediaWikiServices::getInstance()
708                ->getConnectionProvider()
709                ->getReplicaDatabase( false, 'api' );
710        }
711
712        return $this->mReplicaDB;
713    }
714
715    /**
716     * @return ApiContinuationManager|null
717     */
718    public function getContinuationManager() {
719        // The Main module has this method overridden, avoid infinite loops
720        $this->dieIfMain( __METHOD__ );
721
722        return $this->getMain()->getContinuationManager();
723    }
724
725    /**
726     * @param ApiContinuationManager|null $manager
727     */
728    public function setContinuationManager( ApiContinuationManager $manager = null ) {
729        // The Main module has this method overridden, avoid infinite loops
730        $this->dieIfMain( __METHOD__ );
731
732        $this->getMain()->setContinuationManager( $manager );
733    }
734
735    /**
736     * Obtain a PermissionManager instance that subclasses may use in their authorization checks.
737     *
738     * @since 1.34
739     * @return PermissionManager
740     */
741    protected function getPermissionManager(): PermissionManager {
742        return MediaWikiServices::getInstance()->getPermissionManager();
743    }
744
745    /**
746     * Get a HookContainer, for running extension hooks or for hook metadata.
747     *
748     * @since 1.35
749     * @return HookContainer
750     */
751    protected function getHookContainer() {
752        if ( !$this->hookContainer ) {
753            $this->hookContainer = MediaWikiServices::getInstance()->getHookContainer();
754        }
755        return $this->hookContainer;
756    }
757
758    /**
759     * Get an ApiHookRunner for running core API hooks.
760     *
761     * @internal This is for use by core only. Hook interfaces may be removed
762     *  without notice.
763     * @since 1.35
764     * @return ApiHookRunner
765     */
766    protected function getHookRunner() {
767        if ( !$this->hookRunner ) {
768            $this->hookRunner = new ApiHookRunner( $this->getHookContainer() );
769        }
770        return $this->hookRunner;
771    }
772
773    // endregion -- end of data access methods
774
775    /***************************************************************************/
776    // region   Parameter handling
777    /** @name   Parameter handling */
778
779    /**
780     * Indicate if the module supports dynamically-determined parameters that
781     * cannot be included in self::getAllowedParams().
782     * @stable to override
783     * @return string|array|Message|null Return null if the module does not
784     *  support additional dynamic parameters, otherwise return a message
785     *  describing them.
786     */
787    public function dynamicParameterDocumentation() {
788        return null;
789    }
790
791    /**
792     * This method mangles parameter name based on the prefix supplied to the constructor.
793     * Override this method to change parameter name during runtime.
794     *
795     * @param string|string[] $paramName Parameter name
796     * @return string|string[] Prefixed parameter name
797     * @since 1.29 accepts an array of strings
798     */
799    public function encodeParamName( $paramName ) {
800        if ( is_array( $paramName ) ) {
801            return array_map( function ( $name ) {
802                return $this->mModulePrefix . $name;
803            }, $paramName );
804        }
805
806        return $this->mModulePrefix . $paramName;
807    }
808
809    /**
810     * Using getAllowedParams(), this function makes an array of the values
811     * provided by the user, with the key being the name of the variable, and
812     * value - validated value from user or default. limits will not be
813     * parsed if $parseLimit is set to false; use this when the max
814     * limit is not definitive yet, e.g. when getting revisions.
815     * @param bool|array $options If a boolean, uses that as the value for 'parseLimit'
816     *  - parseLimit: (bool, default true) Whether to parse the 'max' value for limit types
817     *  - safeMode: (bool, default false) If true, avoid throwing for parameter validation errors.
818     *    Returned parameter values might be ApiUsageException instances.
819     * @return array
820     */
821    public function extractRequestParams( $options = [] ) {
822        if ( is_bool( $options ) ) {
823            $options = [ 'parseLimit' => $options ];
824        }
825        $options += [
826            'parseLimit' => true,
827            'safeMode' => false,
828        ];
829
830        // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
831        $parseLimit = (bool)$options['parseLimit'];
832        $cacheKey = (int)$parseLimit;
833
834        // Cache parameters, for performance and to avoid T26564.
835        if ( !isset( $this->mParamCache[$cacheKey] ) ) {
836            $params = $this->getFinalParams() ?: [];
837            $results = [];
838            $warned = [];
839
840            // Process all non-templates and save templates for secondary
841            // processing.
842            $toProcess = [];
843            foreach ( $params as $paramName => $paramSettings ) {
844                if ( isset( $paramSettings[self::PARAM_TEMPLATE_VARS] ) ) {
845                    $toProcess[] = [ $paramName, $paramSettings[self::PARAM_TEMPLATE_VARS], $paramSettings ];
846                } else {
847                    try {
848                        $results[$paramName] = $this->getParameterFromSettings(
849                            $paramName, $paramSettings, $parseLimit
850                        );
851                    } catch ( ApiUsageException $ex ) {
852                        $results[$paramName] = $ex;
853                    }
854                }
855            }
856
857            // Now process all the templates by successively replacing the
858            // placeholders with all client-supplied values.
859            // This bit duplicates JavaScript logic in
860            // ApiSandbox.PageLayout.prototype.updateTemplatedParams().
861            // If you update this, see if that needs updating too.
862            while ( $toProcess ) {
863                [ $name, $targets, $settings ] = array_shift( $toProcess );
864
865                foreach ( $targets as $placeholder => $target ) {
866                    if ( !array_key_exists( $target, $results ) ) {
867                        // The target wasn't processed yet, try the next one.
868                        // If all hit this case, the parameter has no expansions.
869                        continue;
870                    }
871                    if ( !is_array( $results[$target] ) || !$results[$target] ) {
872                        // The target was processed but has no (valid) values.
873                        // That means it has no expansions.
874                        break;
875                    }
876
877                    // Expand this target in the name and all other targets,
878                    // then requeue if there are more targets left or put in
879                    // $results if all are done.
880                    unset( $targets[$placeholder] );
881                    $placeholder = '{' . $placeholder . '}';
882                    // @phan-suppress-next-line PhanTypeNoAccessiblePropertiesForeach
883                    foreach ( $results[$target] as $value ) {
884                        if ( !preg_match( '/^[^{}]*$/', $value ) ) {
885                            // Skip values that make invalid parameter names.
886                            $encTargetName = $this->encodeParamName( $target );
887                            if ( !isset( $warned[$encTargetName][$value] ) ) {
888                                $warned[$encTargetName][$value] = true;
889                                $this->addWarning( [
890                                    'apiwarn-ignoring-invalid-templated-value',
891                                    wfEscapeWikiText( $encTargetName ),
892                                    wfEscapeWikiText( $value ),
893                                ] );
894                            }
895                            continue;
896                        }
897
898                        $newName = str_replace( $placeholder, $value, $name );
899                        if ( !$targets ) {
900                            try {
901                                $results[$newName] = $this->getParameterFromSettings(
902                                    $newName,
903                                    $settings,
904                                    $parseLimit
905                                );
906                            } catch ( ApiUsageException $ex ) {
907                                $results[$newName] = $ex;
908                            }
909                        } else {
910                            $newTargets = [];
911                            foreach ( $targets as $k => $v ) {
912                                $newTargets[$k] = str_replace( $placeholder, $value, $v );
913                            }
914                            $toProcess[] = [ $newName, $newTargets, $settings ];
915                        }
916                    }
917                    break;
918                }
919            }
920
921            $this->mParamCache[$cacheKey] = $results;
922        }
923
924        $ret = $this->mParamCache[$cacheKey];
925        if ( !$options['safeMode'] ) {
926            foreach ( $ret as $v ) {
927                if ( $v instanceof ApiUsageException ) {
928                    throw $v;
929                }
930            }
931        }
932
933        return $this->mParamCache[$cacheKey];
934    }
935
936    /**
937     * Get a value for the given parameter.
938     *
939     * @param string $paramName Parameter name
940     * @param bool $parseLimit See extractRequestParams()
941     * @return mixed Parameter value
942     */
943    protected function getParameter( $paramName, $parseLimit = true ) {
944        $ret = $this->extractRequestParams( [
945            'parseLimit' => $parseLimit,
946            'safeMode' => true,
947        ] )[$paramName];
948        if ( $ret instanceof ApiUsageException ) {
949            throw $ret;
950        }
951        return $ret;
952    }
953
954    /**
955     * Die if 0 or more than one of a certain set of parameters is set and not false.
956     *
957     * @param array $params User provided parameter set, as from $this->extractRequestParams()
958     * @param string ...$required Names of parameters of which exactly one must be set
959     */
960    public function requireOnlyOneParameter( $params, ...$required ) {
961        $intersection = array_intersect( array_keys( array_filter( $params,
962            [ $this, 'parameterNotEmpty' ] ) ), $required );
963
964        if ( count( $intersection ) > 1 ) {
965            $this->dieWithError( [
966                'apierror-invalidparammix',
967                Message::listParam( array_map(