Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.01% covered (warning)
84.01%
788 / 938
57.89% covered (warning)
57.89%
33 / 57
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiMain
84.10% covered (warning)
84.10%
788 / 937
57.89% covered (warning)
57.89%
33 / 57
598.51
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
66 / 66
100.00% covered (success)
100.00%
1 / 1
13
 isInternalMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getResult
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 lacksSameOriginSecurity
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
7.02
 getErrorFormatter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContinuationManager
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setContinuationManager
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getParamValidator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getModule
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStatsFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrinter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setCacheMaxAge
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setCacheMode
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 getCacheMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCacheControl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createPrinterByName
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
2.50
 execute
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 executeActionWithErrorHandling
93.33% covered (success)
93.33%
28 / 30
0.00% covered (danger)
0.00%
0 / 1
10.03
 handleException
93.33% covered (success)
93.33%
42 / 45
0.00% covered (danger)
0.00%
0 / 1
11.04
 handleApiBeforeMainException
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 handleCORS
42.03% covered (danger)
42.03%
29 / 69
0.00% covered (danger)
0.00%
0 / 1
97.93
 matchRequestedHeaders
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 sendCacheHeaders
69.23% covered (warning)
69.23%
36 / 52
0.00% covered (danger)
0.00%
0 / 1
40.78
 createErrorPrinter
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 errorMessagesFromException
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
7.07
 substituteResultWithError
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
1 / 1
12
 addRequestedFields
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
7
 setupExecuteAction
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setupModule
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 getMaxLag
57.14% covered (warning)
57.14%
12 / 21
0.00% covered (danger)
0.00%
0 / 1
3.71
 checkMaxLag
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 checkConditionalRequestHeaders
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
1 / 1
21
 checkExecutePermissions
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 checkReadOnly
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
5.93
 checkBotReadOnly
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 checkAsserts
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
11
 setupExternalResponse
44.00% covered (danger)
44.00%
11 / 25
0.00% covered (danger)
0.00%
0 / 1
37.29
 executeAction
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
6
 setRequestExpectations
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
4.10
 logRequest
88.46% covered (warning)
88.46%
46 / 52
0.00% covered (danger)
0.00%
0 / 1
10.15
 encodeRequestLogValue
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getParamsUsed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 markParamsUsed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSensitiveParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 markParamsSensitive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVal
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getCheck
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getUpload
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 reportUnusedParams
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 printResult
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 isReadMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 modifyHelp
100.00% covered (success)
100.00%
97 / 97
100.00% covered (success)
100.00%
1 / 1
12
 canApiHighLimits
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getModuleManager
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUserAgent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Copyright Â© 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @defgroup API API
8 */
9
10namespace MediaWiki\Api;
11
12use LogicException;
13use MediaWiki;
14use MediaWiki\Api\Validator\ApiParamValidator;
15use MediaWiki\Context\DerivativeContext;
16use MediaWiki\Context\IContextSource;
17use MediaWiki\Context\RequestContext;
18use MediaWiki\Debug\MWDebug;
19use MediaWiki\Exception\ILocalizedException;
20use MediaWiki\Exception\MWExceptionHandler;
21use MediaWiki\Exception\MWExceptionRenderer;
22use MediaWiki\Html\Html;
23use MediaWiki\Logger\LoggerFactory;
24use MediaWiki\MainConfigNames;
25use MediaWiki\MediaWikiServices;
26use MediaWiki\Message\Message;
27use MediaWiki\ParamValidator\TypeDef\UserDef;
28use MediaWiki\Profiler\ProfilingContext;
29use MediaWiki\Request\FauxRequest;
30use MediaWiki\Request\WebRequest;
31use MediaWiki\Request\WebRequestUpload;
32use MediaWiki\Rest\HeaderParser\Origin;
33use MediaWiki\StubObject\StubGlobalUser;
34use MediaWiki\User\UserRigorOptions;
35use MediaWiki\WikiMap\WikiMap;
36use Profiler;
37use Throwable;
38use UnexpectedValueException;
39use Wikimedia\AtEase\AtEase;
40use Wikimedia\Message\ListType;
41use Wikimedia\Message\MessageSpecifier;
42use Wikimedia\ParamValidator\ParamValidator;
43use Wikimedia\ParamValidator\TypeDef\IntegerDef;
44use Wikimedia\ScopedCallback;
45use Wikimedia\Stats\StatsFactory;
46use Wikimedia\Timestamp\ConvertibleTimestamp;
47use Wikimedia\Timestamp\TimestampException;
48
49/**
50 * This is the main API class, used for both external and internal processing.
51 * When executed, it will create the requested formatter object,
52 * instantiate and execute an object associated with the needed action,
53 * and use formatter to print results.
54 * In case of an exception, an error message will be printed using the same formatter.
55 *
56 * To use API from another application, run it using MediaWiki\Request\FauxRequest object, in which
57 * case any internal exceptions will not be handled but passed up to the caller.
58 * After successful execution, use getResult() for the resulting data.
59 *
60 * @newable
61 * @note marked as newable in 1.35 for lack of a better alternative,
62 *       but should use a factory in the future.
63 * @ingroup API
64 */
65class ApiMain extends ApiBase {
66    /**
67     * When no format parameter is given, this format will be used
68     */
69    private const API_DEFAULT_FORMAT = 'jsonfm';
70
71    /**
72     * When no uselang parameter is given, this language will be used
73     */
74    private const API_DEFAULT_USELANG = 'user';
75
76    /**
77     * List of available modules: action name => module class
78     */
79    private const MODULES = [
80        'login' => [
81            'class' => ApiLogin::class,
82            'services' => [
83                'AuthManager',
84                'UserIdentityUtils'
85            ],
86        ],
87        'clientlogin' => [
88            'class' => ApiClientLogin::class,
89            'services' => [
90                'AuthManager',
91                'UrlUtils',
92            ],
93        ],
94        'logout' => [
95            'class' => ApiLogout::class,
96        ],
97        'createaccount' => [
98            'class' => ApiAMCreateAccount::class,
99            'services' => [
100                'AuthManager',
101                'UrlUtils',
102            ],
103        ],
104        'linkaccount' => [
105            'class' => ApiLinkAccount::class,
106            'services' => [
107                'AuthManager',
108                'UrlUtils',
109            ],
110        ],
111        'unlinkaccount' => [
112            'class' => ApiRemoveAuthenticationData::class,
113            'services' => [
114                'AuthManager',
115            ],
116        ],
117        'changeauthenticationdata' => [
118            'class' => ApiChangeAuthenticationData::class,
119            'services' => [
120                'AuthManager',
121            ],
122        ],
123        'removeauthenticationdata' => [
124            'class' => ApiRemoveAuthenticationData::class,
125            'services' => [
126                'AuthManager',
127            ],
128        ],
129        'resetpassword' => [
130            'class' => ApiResetPassword::class,
131            'services' => [
132                'PasswordReset',
133            ]
134        ],
135        'query' => [
136            'class' => ApiQuery::class,
137            'services' => [
138                'ObjectFactory',
139                'WikiExporterFactory',
140                'TitleFormatter',
141                'TitleFactory',
142            ]
143        ],
144        'expandtemplates' => [
145            'class' => ApiExpandTemplates::class,
146            'services' => [
147                'RevisionStore',
148                'ParserFactory',
149            ]
150        ],
151        'parse' => [
152            'class' => ApiParse::class,
153            'services' => [
154                'RevisionLookup',
155                'SkinFactory',
156                'LanguageNameUtils',
157                'LinkBatchFactory',
158                'LinkCache',
159                'ContentHandlerFactory',
160                'ParserFactory',
161                'WikiPageFactory',
162                'ContentRenderer',
163                'ContentTransformer',
164                'CommentFormatter',
165                'TempUserCreator',
166                'UserFactory',
167                'UrlUtils',
168                'TitleFormatter',
169                'JsonCodec',
170            ]
171        ],
172        'stashedit' => [
173            'class' => ApiStashEdit::class,
174            'services' => [
175                'ContentHandlerFactory',
176                'PageEditStash',
177                'RevisionLookup',
178                'StatsFactory',
179                'WikiPageFactory',
180                'TempUserCreator',
181                'UserFactory',
182            ]
183        ],
184        'opensearch' => [
185            'class' => ApiOpenSearch::class,
186            'services' => [
187                'LinkBatchFactory',
188                'SearchEngineConfig',
189                'SearchEngineFactory',
190                'UrlUtils',
191            ]
192        ],
193        'feedcontributions' => [
194            'class' => ApiFeedContributions::class,
195            'services' => [
196                'RevisionStore',
197                'LinkRenderer',
198                'LinkBatchFactory',
199                'HookContainer',
200                'DBLoadBalancerFactory',
201                'NamespaceInfo',
202                'UserFactory',
203                'CommentFormatter',
204            ]
205        ],
206        'feedrecentchanges' => [
207            'class' => ApiFeedRecentChanges::class,
208            'services' => [
209                'SpecialPageFactory',
210                'TempUserConfig',
211            ]
212        ],
213        'feedwatchlist' => [
214            'class' => ApiFeedWatchlist::class,
215            'services' => [
216                'ParserFactory',
217            ]
218        ],
219        'help' => [
220            'class' => ApiHelp::class,
221            'services' => [
222                'SkinFactory',
223            ]
224        ],
225        'paraminfo' => [
226            'class' => ApiParamInfo::class,
227            'services' => [
228                'UserFactory',
229            ],
230        ],
231        'rsd' => [
232            'class' => ApiRsd::class,
233        ],
234        'compare' => [
235            'class' => ApiComparePages::class,
236            'services' => [
237                'RevisionStore',
238                'ArchivedRevisionLookup',
239                'SlotRoleRegistry',
240                'ContentHandlerFactory',
241                'ContentTransformer',
242                'CommentFormatter',
243                'TempUserCreator',
244                'UserFactory',
245            ]
246        ],
247        'checktoken' => [
248            'class' => ApiCheckToken::class,
249        ],
250        'cspreport' => [
251            'class' => ApiCSPReport::class,
252            'services' => [
253                'UrlUtils',
254            ]
255        ],
256        'validatepassword' => [
257            'class' => ApiValidatePassword::class,
258            'services' => [
259                'AuthManager',
260                'UserFactory',
261            ]
262        ],
263
264        // Write modules
265        'purge' => [
266            'class' => ApiPurge::class,
267            'services' => [
268                'WikiPageFactory',
269                'TitleFormatter',
270            ],
271        ],
272        'setnotificationtimestamp' => [
273            'class' => ApiSetNotificationTimestamp::class,
274            'services' => [
275                'DBLoadBalancerFactory',
276                'RevisionStore',
277                'WatchedItemStore',
278                'TitleFormatter',
279                'TitleFactory',
280            ]
281        ],
282        'rollback' => [
283            'class' => ApiRollback::class,
284            'services' => [
285                'RollbackPageFactory',
286                'WatchlistManager',
287                'WatchedItemStore',
288                'UserOptionsLookup',
289            ]
290        ],
291        'delete' => [
292            'class' => ApiDelete::class,
293            'services' => [
294                'RepoGroup',
295                'WatchlistManager',
296                'WatchedItemStore',
297                'UserOptionsLookup',
298                'DeletePageFactory',
299            ]
300        ],
301        'undelete' => [
302            'class' => ApiUndelete::class,
303            'services' => [
304                'WatchlistManager',
305                'WatchedItemStore',
306                'UserOptionsLookup',
307                'UndeletePageFactory',
308                'WikiPageFactory',
309            ]
310        ],
311        'protect' => [
312            'class' => ApiProtect::class,
313            'services' => [
314                'WatchlistManager',
315                'WatchedItemStore',
316                'UserOptionsLookup',
317                'RestrictionStore',
318            ]
319        ],
320        'block' => [
321            'class' => ApiBlock::class,
322            'services' => [
323                'BlockPermissionCheckerFactory',
324                'BlockUserFactory',
325                'TitleFactory',
326                'UserIdentityLookup',
327                'WatchedItemStore',
328                'BlockTargetFactory',
329                'BlockActionInfo',
330                'DatabaseBlockStore',
331                'WatchlistManager',
332                'UserOptionsLookup',
333            ]
334        ],
335        'unblock' => [
336            'class' => ApiUnblock::class,
337            'services' => [
338                'BlockPermissionCheckerFactory',
339                'UnblockUserFactory',
340                'UserIdentityLookup',
341                'WatchedItemStore',
342                'WatchlistManager',
343                'UserOptionsLookup',
344                'DatabaseBlockStore',
345                'BlockTargetFactory',
346            ]
347        ],
348        'move' => [
349            'class' => ApiMove::class,
350            'services' => [
351                'MovePageFactory',
352                'RepoGroup',
353                'WatchlistManager',
354                'WatchedItemStore',
355                'UserOptionsLookup',
356            ]
357        ],
358        'edit' => [
359            'class' => ApiEditPage::class,
360            'services' => [
361                'ContentHandlerFactory',
362                'RevisionLookup',
363                'WatchedItemStore',
364                'WikiPageFactory',
365                'WatchlistManager',
366                'UserOptionsLookup',
367                'RedirectLookup',
368                'TempUserCreator',
369                'UserFactory',
370            ]
371        ],
372        'upload' => [
373            'class' => ApiUpload::class,
374            'services' => [
375                'JobQueueGroup',
376                'WatchlistManager',
377                'WatchedItemStore',
378                'UserOptionsLookup',
379            ]
380        ],
381        'filerevert' => [
382            'class' => ApiFileRevert::class,
383            'services' => [
384                'RepoGroup',
385            ]
386        ],
387        'emailuser' => [
388            'class' => ApiEmailUser::class,
389            'services' => [
390                'EmailUserFactory',
391                'UserFactory',
392            ]
393        ],
394        'watch' => [
395            'class' => ApiWatch::class,
396            'services' => [
397                'WatchlistManager',
398                'TitleFormatter',
399            ]
400        ],
401        'patrol' => [
402            'class' => ApiPatrol::class,
403            'services' => [
404                'RevisionStore',
405                'PatrolManager',
406                'RecentChangeLookup',
407            ]
408        ],
409        'import' => [
410            'class' => ApiImport::class,
411            'services' => [
412                'WikiImporterFactory',
413            ]
414        ],
415        'clearhasmsg' => [
416            'class' => ApiClearHasMsg::class,
417            'services' => [
418                'TalkPageNotificationManager',
419            ]
420        ],
421        'userrights' => [
422            'class' => ApiUserrights::class,
423            'services' => [
424                'UserGroupManager',
425                'WatchedItemStore',
426                'WatchlistManager',
427                'UserOptionsLookup',
428                'UserGroupAssignmentService',
429                'MultiFormatUserIdentityLookup',
430            ]
431        ],
432        'options' => [
433            'class' => ApiOptions::class,
434            'services' => [
435                'UserOptionsManager',
436                'PreferencesFactory',
437            ],
438        ],
439        'imagerotate' => [
440            'class' => ApiImageRotate::class,
441            'services' => [
442                'RepoGroup',
443                'TempFSFileFactory',
444                'TitleFactory',
445            ]
446        ],
447        'revisiondelete' => [
448            'class' => ApiRevisionDelete::class,
449        ],
450        'managetags' => [
451            'class' => ApiManageTags::class,
452        ],
453        'tag' => [
454            'class' => ApiTag::class,
455            'services' => [
456                'DBLoadBalancerFactory',
457                'RevisionStore',
458                'ChangeTagsStore',
459                'RecentChangeLookup',
460            ]
461        ],
462        'mergehistory' => [
463            'class' => ApiMergeHistory::class,
464            'services' => [
465                'MergeHistoryFactory',
466            ],
467        ],
468        'setpagelanguage' => [
469            'class' => ApiSetPageLanguage::class,
470            'services' => [
471                'DBLoadBalancerFactory',
472                'LanguageNameUtils',
473            ]
474        ],
475        'changecontentmodel' => [
476            'class' => ApiChangeContentModel::class,
477            'services' => [
478                'ContentHandlerFactory',
479                'ContentModelChangeFactory',
480            ]
481        ],
482        'acquiretempusername' => [
483            'class' => ApiAcquireTempUserName::class,
484            'services' => [
485                'TempUserCreator',
486            ]
487        ],
488    ];
489
490    /**
491     * List of available formats: format name => format class
492     */
493    private const FORMATS = [
494        'json' => [
495            'class' => ApiFormatJson::class,
496        ],
497        'jsonfm' => [
498            'class' => ApiFormatJson::class,
499        ],
500        'php' => [
501            'class' => ApiFormatPhp::class,
502        ],
503        'phpfm' => [
504            'class' => ApiFormatPhp::class,
505        ],
506        'xml' => [
507            'class' => ApiFormatXml::class,
508        ],
509        'xmlfm' => [
510            'class' => ApiFormatXml::class,
511        ],
512        'rawfm' => [
513            'class' => ApiFormatJson::class,
514        ],
515        'none' => [
516            'class' => ApiFormatNone::class,
517        ],
518    ];
519
520    /**
521     * List of user roles that are specifically relevant to the API.
522     * [ 'right' => [ 'msg'    => 'Some message with a $1',
523     *                'params' => [ $someVarToSubst ] ],
524     * ];
525     */
526    private const RIGHTS_MAP = [
527        'apihighlimits' => [
528            'msg' => 'api-help-right-apihighlimits',
529            'params' => [ ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ]
530        ]
531    ];
532
533    /** @var ApiFormatBase|null */
534    private $mPrinter;
535
536    /** @var ApiModuleManager */
537    private $mModuleMgr;
538
539    /** @var ApiResult */
540    private $mResult;
541
542    /** @var ApiErrorFormatter */
543    private $mErrorFormatter;
544
545    /** @var ApiParamValidator */
546    private $mParamValidator;
547
548    /** @var ApiContinuationManager|null */
549    private $mContinuationManager;
550
551    /** @var string|null */
552    private $mAction;
553
554    /** @var bool */
555    private $mEnableWrite;
556
557    /** @var bool */
558    private $mInternalMode;
559
560    /** @var ApiBase */
561    private $mModule;
562
563    /** @var string */
564    private $mCacheMode = 'private';
565
566    /** @var array */
567    private $mCacheControl = [];
568
569    /** @var array */
570    private $mParamsUsed = [];
571
572    /** @var array */
573    private $mParamsSensitive = [];
574
575    /** @var bool|null Cached return value from self::lacksSameOriginSecurity() */
576    private $lacksSameOriginSecurity = null;
577
578    /** @var StatsFactory */
579    private $statsFactory;
580
581    /**
582     * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
583     *
584     * @stable to call
585     * @param IContextSource|WebRequest|null $context If this is an instance of
586     *    MediaWiki\Request\FauxRequest, errors are thrown and no printing occurs
587     * @param bool $enableWrite Should be set to true if the api may modify data
588     * @param bool|null $internal Whether the API request is an internal faux
589     *        request. If null or not given, the request is assumed to be internal
590     *        if $context contains a FauxRequest.
591     */
592    public function __construct( $context = null, $enableWrite = false, $internal = null ) {
593        if ( $context === null ) {
594            $context = RequestContext::getMain();
595        } elseif ( $context instanceof WebRequest ) {
596            // BC for pre-1.19
597            $request = $context;
598            $context = RequestContext::getMain();
599        }
600        // We set a derivative context so we can change stuff later
601        $derivativeContext = new DerivativeContext( $context );
602        $this->setContext( $derivativeContext );
603
604        if ( isset( $request ) ) {
605            $derivativeContext->setRequest( $request );
606        } else {
607            $request = $this->getRequest();
608        }
609
610        $this->mInternalMode = $internal ?? ( $request instanceof FauxRequest );
611
612        // Special handling for the main module: $parent === $this
613        parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
614
615        $config = $this->getConfig();
616        // TODO inject stuff, see T265644
617        $services = MediaWikiServices::getInstance();
618
619        if ( !$this->mInternalMode ) {
620            // If we're in a mode that breaks the same-origin policy, strip
621            // user credentials for security.
622            if ( $this->lacksSameOriginSecurity() ) {
623                wfDebug( "API: stripping user credentials when the same-origin policy is not applied" );
624                $user = $services->getUserFactory()->newAnonymous();
625                StubGlobalUser::setUser( $user );
626                $derivativeContext->setUser( $user );
627                $request->response()->header( 'MediaWiki-Login-Suppressed: true' );
628            }
629        }
630
631        $this->mParamValidator = new ApiParamValidator(
632            $this,
633            $services->getObjectFactory()
634        );
635
636        $this->statsFactory = $services->getStatsFactory();
637
638        $this->mResult =
639            new ApiResult( $this->getConfig()->get( MainConfigNames::APIMaxResultSize ) );
640
641        // Setup uselang. This doesn't use $this->getParameter()
642        // because we're not ready to handle errors yet.
643        // Optimisation: Avoid slow getVal(), this isn't user-generated content.
644        $uselang = $request->getRawVal( 'uselang' ) ?? self::API_DEFAULT_USELANG;
645        if ( $uselang === 'user' ) {
646            // Assume the parent context is going to return the user language
647            // for uselang=user (see T85635).
648        } else {
649            if ( $uselang === 'content' ) {
650                $uselang = $services->getContentLanguageCode()->toString();
651            }
652            $code = RequestContext::sanitizeLangCode( $uselang );
653            $derivativeContext->setLanguage( $code );
654            if ( !$this->mInternalMode ) {
655                // phpcs:disable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
656                global $wgLang;
657                $wgLang = $derivativeContext->getLanguage();
658                RequestContext::getMain()->setLanguage( $wgLang );
659                // phpcs:enable
660            }
661        }
662
663        // Set up the error formatter. This doesn't use $this->getParameter()
664        // because we're not ready to handle errors yet.
665        // Optimisation: Avoid slow getVal(), this isn't user-generated content.
666        $errorFormat = $request->getRawVal( 'errorformat' ) ?? 'bc';
667        $errorLangCode = $request->getRawVal( 'errorlang' ) ?? 'uselang';
668        $errorsUseDB = $request->getCheck( 'errorsuselocal' );
669        if ( in_array( $errorFormat, [ 'plaintext', 'wikitext', 'html', 'raw', 'none' ], true ) ) {
670            if ( $errorLangCode === 'uselang' ) {
671                $errorLang = $this->getLanguage();
672            } elseif ( $errorLangCode === 'content' ) {
673                $errorLang = $services->getContentLanguage();
674            } else {
675                $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode );
676                $errorLang = $services->getLanguageFactory()->getLanguage( $errorLangCode );
677            }
678            $this->mErrorFormatter = new ApiErrorFormatter(
679                $this->mResult,
680                $errorLang,
681                $errorFormat,
682                $errorsUseDB
683            );
684        } else {
685            $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
686        }
687        $this->mResult->setErrorFormatter( $this->getErrorFormatter() );
688
689        $this->mModuleMgr = new ApiModuleManager(
690            $this,
691            $services->getObjectFactory()
692        );
693        $this->mModuleMgr->addModules( self::MODULES, 'action' );
694        $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIModules ), 'action' );
695        $this->mModuleMgr->addModules( self::FORMATS, 'format' );
696        $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIFormatModules ), 'format' );
697
698        $this->getHookRunner()->onApiMain__moduleManager( $this->mModuleMgr );
699
700        $this->mContinuationManager = null;
701        $this->mEnableWrite = $enableWrite;
702    }
703
704    /**
705     * Return true if the API was started by other PHP code using MediaWiki\Request\FauxRequest
706     * @return bool
707     */
708    public function isInternalMode() {
709        return $this->mInternalMode;
710    }
711
712    /**
713     * Get the ApiResult object associated with current request
714     *
715     * @return ApiResult
716     */
717    public function getResult() {
718        return $this->mResult;
719    }
720
721    /**
722     * Get the security flag for the current request
723     * @return bool
724     */
725    public function lacksSameOriginSecurity() {
726        if ( $this->lacksSameOriginSecurity !== null ) {
727            return $this->lacksSameOriginSecurity;
728        }
729
730        $request = $this->getRequest();
731
732        // JSONP mode
733        if ( $request->getCheck( 'callback' ) ||
734            // Anonymous CORS
735            $request->getRawVal( 'origin' ) === '*' ||
736            // Header to be used from XMLHTTPRequest when the request might
737            // otherwise be used for XSS.
738            $request->getHeader( 'Treat-as-Untrusted' ) !== false ||
739            (
740                // Authenticated CORS with unsupported session provider (including preflight request)
741                $request->getCheck( 'crossorigin' ) &&
742                !$request->getSession()->getProvider()->safeAgainstCsrf()
743            )
744        ) {
745            $this->lacksSameOriginSecurity = true;
746            return true;
747        }
748
749        // Allow extensions to override.
750        $this->lacksSameOriginSecurity = !$this->getHookRunner()
751            ->onRequestHasSameOriginSecurity( $request );
752        return $this->lacksSameOriginSecurity;
753    }
754
755    /**
756     * Get the ApiErrorFormatter object associated with current request
757     * @return ApiErrorFormatter
758     */
759    public function getErrorFormatter() {
760        return $this->mErrorFormatter;
761    }
762
763    /**
764     * @return ApiContinuationManager|null
765     */
766    public function getContinuationManager() {
767        return $this->mContinuationManager;
768    }
769
770    /**
771     * @param ApiContinuationManager|null $manager
772     */
773    public function setContinuationManager( ?ApiContinuationManager $manager = null ) {
774        if ( $manager !== null && $this->mContinuationManager !== null ) {
775            throw new UnexpectedValueException(
776                __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
777                ' when a manager is already set from ' . $this->mContinuationManager->getSource()
778            );
779        }
780        $this->mContinuationManager = $manager;
781    }
782
783    public function getParamValidator(): ApiParamValidator {
784        return $this->mParamValidator;
785    }
786
787    /**
788     * Get the API module object. Only works after executeAction()
789     *
790     * @return ApiBase
791     */
792    public function getModule() {
793        return $this->mModule;
794    }
795
796    /**
797     * Get the stats factory.
798     *
799     * @return StatsFactory
800     */
801    public function getStatsFactory() {
802        return $this->getMain()->statsFactory;
803    }
804
805    /**
806     * Get the result formatter object. Only works after setupExecuteAction()
807     *
808     * @return ApiFormatBase
809     */
810    public function getPrinter() {
811        return $this->mPrinter;
812    }
813
814    /**
815     * Set how long the response should be cached.
816     *
817     * @param int $maxage
818     */
819    public function setCacheMaxAge( $maxage ) {
820        $this->setCacheControl( [
821            'max-age' => $maxage,
822            's-maxage' => $maxage
823        ] );
824    }
825
826    /**
827     * Set the type of caching headers which will be sent.
828     *
829     * @param string $mode One of:
830     *    - 'public':     Cache this object in public caches, if the maxage or smaxage
831     *         parameter is set, or if setCacheMaxAge() was called. If a maximum age is
832     *         not provided by any of these means, the object will be private.
833     *    - 'private':    Cache this object only in private client-side caches.
834     *    - 'anon-public-user-private': Make this object cacheable for logged-out
835     *         users, but private for logged-in users. IMPORTANT: If this is set, it must be
836     *         set consistently for a given URL, it cannot be set differently depending on
837     *         things like the contents of the database, or whether the user is logged in.
838     *
839     *  If the wiki does not allow anonymous users to read it, the mode set here
840     *  will be ignored, and private caching headers will always be sent. In other words,
841     *  the "public" mode is equivalent to saying that the data sent is as public as a page
842     *  view.
843     *
844     *  For user-dependent data, the private mode should generally be used. The
845     *  anon-public-user-private mode should only be used where there is a particularly
846     *  good performance reason for caching the anonymous response, but where the
847     *  response to logged-in users may differ, or may contain private data.
848     *
849     *  If this function is never called, then the default will be the private mode.
850     */
851    public function setCacheMode( $mode ) {
852        if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
853            wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"" );
854
855            // Ignore for forwards-compatibility
856            return;
857        }
858
859        if ( !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) ) {
860            // Private wiki, only private headers
861            if ( $mode !== 'private' ) {
862                wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki" );
863
864                return;
865            }
866        }
867
868        if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
869            // User language is used for i18n, so we don't want to publicly
870            // cache. Anons are ok, because if they have non-default language
871            // then there's an appropriate Vary header set by whatever set
872            // their non-default language.
873            wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
874                "'anon-public-user-private' due to uselang=user" );
875            $mode = 'anon-public-user-private';
876        }
877
878        wfDebug( __METHOD__ . ": setting cache mode $mode" );
879        $this->mCacheMode = $mode;
880    }
881
882    /** @inheritDoc */
883    public function getCacheMode() {
884        return $this->mCacheMode;
885    }
886
887    /**
888     * Set directives (key/value pairs) for the Cache-Control header.
889     * Boolean values will be formatted as such, by including or omitting
890     * without an equals sign.
891     *
892     * Cache control values set here will only be used if the cache mode is not
893     * private, see setCacheMode().
894     *
895     * @param array $directives
896     */
897    public function setCacheControl( $directives ) {
898        $this->mCacheControl = $directives + $this->mCacheControl;
899    }
900
901    /**
902     * Create an instance of an output formatter by its name
903     *
904     * @param string $format
905     *
906     * @return ApiFormatBase
907     */
908    public function createPrinterByName( $format ) {
909        $printer = $this->mModuleMgr->getModule( $format, 'format', /* $ignoreCache */ true );
910        if ( $printer === null ) {
911            $this->dieWithError(
912                [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
913            );
914        }
915
916        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
917        return $printer;
918    }
919
920    /**
921     * Execute api request. Any errors will be handled if the API was called by the remote client.
922     */
923    public function execute() {
924        if ( $this->mInternalMode ) {
925            $this->executeAction();
926        } else {
927            $this->executeActionWithErrorHandling();
928        }
929    }
930
931    /**
932     * Execute an action, and in case of an error, erase whatever partial results
933     * have been accumulated, and replace it with an error message and a help screen.
934     */
935    protected function executeActionWithErrorHandling() {
936        // Verify the CORS header before executing the action
937        if ( !$this->handleCORS() ) {
938            // handleCORS() has sent a 403, abort
939            return;
940        }
941
942        // Exit here if the request method was OPTIONS
943        // (assume there will be a followup GET or POST)
944        if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
945            return;
946        }
947
948        // In case an error occurs during data output,
949        // clear the output buffer and print just the error information
950        $obLevel = ob_get_level();
951        ob_start();
952
953        $t = microtime( true );
954        $isError = false;
955        try {
956            $this->executeAction();
957            $runTime = microtime( true ) - $t;
958            $this->logRequest( $runTime );
959
960            $this->statsFactory->getTiming( 'api_executeTiming_seconds' )
961                ->setLabel( 'module', $this->mModule->getModuleName() )
962                ->observe( 1000 * $runTime );
963
964            if ( !$this->mModule || $this->mModule->getModuleName() !== 'query' ) {
965                // Skip query module metrics; we will record them in the query module itself.
966                $this->recordUnifiedMetrics( $runTime );
967            }
968        } catch ( Throwable $e ) {
969            // If executeAction threw before the time was set, reset it
970            $runTime ??= microtime( true ) - $t;
971            $this->handleException( $e, $runTime );
972            $this->logRequest( microtime( true ) - $t, $e );
973            $isError = true;
974        }
975
976        // Disable the client cache on the output so that BlockManager::trackBlockWithCookie is executed
977        // as part of MediaWiki::preOutputCommit().
978        if (
979            $this->mCacheMode === 'private'
980            || (
981                $this->mCacheMode === 'anon-public-user-private'
982                && $this->getRequest()->getSession()->isPersistent()
983            )
984        ) {
985            $this->getContext()->getOutput()->disableClientCache();
986            $this->getContext()->getOutput()->considerCacheSettingsFinal();
987        }
988
989        // Commit DBs and send any related cookies and headers
990        MediaWiki::preOutputCommit( $this->getContext() );
991
992        // Send cache headers after any code which might generate an error, to
993        // avoid sending public cache headers for errors.
994        $this->sendCacheHeaders( $isError );
995
996        // Executing the action might have already messed with the output
997        // buffers.
998        while ( ob_get_level() > $obLevel ) {
999            ob_end_flush();
1000        }
1001    }
1002
1003    /**
1004     * Handle a throwable as an API response
1005     *
1006     * @since 1.23
1007     * @param Throwable $e
1008     * @param float $latency Optional value for process runtime, in microseconds, for metrics
1009     */
1010    protected function handleException( Throwable $e, $latency = 0 ) {
1011        $statsModuleName = $this->mModule ? $this->mModule->getModuleName() : 'main';
1012
1013        // Collect stats on errors (T396613).
1014        // NOTE: We only count fatal errors, a mere call to addError() or
1015        // addWarning() does not count towards these states. That could
1016        // be added in the future, but should use a different stats key.
1017        $stats = $this->statsFactory->getCounter( 'api_errors' )
1018            ->setLabel( 'module', $statsModuleName );
1019
1020        // T65145: Rollback any open database transactions
1021        if ( !$e instanceof ApiUsageException ) {
1022            // ApiUsageExceptions are intentional, so don't rollback if that's the case
1023            MWExceptionHandler::rollbackPrimaryChangesAndLog(
1024                $e,
1025                MWExceptionHandler::CAUGHT_BY_ENTRYPOINT
1026            );
1027            $stats->setLabel( 'exception_cause', 'server-error' );
1028        } else {
1029            $stats->setLabel( 'exception_cause', 'client-error' );
1030        }
1031
1032        // Allow extra cleanup and logging
1033        $this->getHookRunner()->onApiMain__onException( $this, $e );
1034
1035        // Handle any kind of exception by outputting properly formatted error message.
1036        // If this fails, an unhandled exception should be thrown so that global error
1037        // handler will process and log it.
1038
1039        $errCodes = $this->substituteResultWithError( $e );
1040        sort( $errCodes );
1041
1042        // Error results should not be cached
1043        $this->setCacheMode( 'private' );
1044
1045        $response = $this->getRequest()->response();
1046        $headerStr = 'MediaWiki-API-Error: ' . implode( ', ', $errCodes );
1047        $response->header( $headerStr );
1048
1049        // Reset and print just the error message
1050        ob_clean();
1051
1052        // Printer may not be initialized if the extractRequestParams() fails for the main module
1053        $this->createErrorPrinter();
1054
1055        $stats->setLabel( 'error_code', implode( '_', $errCodes ) );
1056        $stats->increment();
1057
1058        // Unified metrics
1059        if ( !$this->mModule || $this->mModule->getModuleName() !== 'query' ) {
1060            // Skip query module metrics; we will record them in the query module itself.
1061            $this->recordUnifiedMetrics(
1062                $latency,
1063                [
1064                    'status' => implode( '_', $errCodes ), // Failure codes
1065                ]
1066            );
1067
1068        }
1069
1070        // Get desired HTTP code from an ApiUsageException. Don't use codes from other
1071        // exception types, as they are unlikely to be intended as an HTTP code.
1072        $httpCode = $e instanceof ApiUsageException ? $e->getCode() : 0;
1073
1074        $failed = false;
1075        try {
1076            $this->printResult( $httpCode );
1077        } catch ( ApiUsageException $ex ) {
1078            // The error printer itself is failing. Try suppressing its request
1079            // parameters and redo.
1080            $failed = true;
1081            $this->addWarning( 'apiwarn-errorprinterfailed' );
1082            foreach ( $ex->getStatusValue()->getMessages() as $error ) {
1083                try {
1084                    $this->mPrinter->addWarning( $error );
1085                } catch ( Throwable ) {
1086                    // WTF?
1087                    $this->addWarning( $error );
1088                }
1089            }
1090        }
1091        if ( $failed ) {
1092            $this->mPrinter = null;
1093            $this->createErrorPrinter();
1094            // @phan-suppress-next-line PhanNonClassMethodCall False positive
1095            $this->mPrinter->forceDefaultParams();
1096            if ( $httpCode ) {
1097                $response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200
1098            }
1099            $this->printResult( $httpCode );
1100        }
1101    }
1102
1103    /**
1104     * Handle a throwable from the ApiBeforeMain hook.
1105     *
1106     * This tries to print the throwable as an API response, to be more
1107     * friendly to clients. If it fails, it will rethrow the throwable.
1108     *
1109     * @since 1.23
1110     * @param Throwable $e
1111     * @throws Throwable
1112     */
1113    public static function handleApiBeforeMainException( Throwable $e ) {
1114        ob_start();
1115
1116        try {
1117            $main = new self( RequestContext::getMain(), false );
1118            $main->handleException( $e );
1119            $main->logRequest( 0, $e );
1120        } catch ( Throwable ) {
1121            // Nope, even that didn't work. Punt.
1122            throw $e;
1123        }
1124
1125        // Reset cache headers
1126        $main->sendCacheHeaders( true );
1127
1128        ob_end_flush();
1129    }
1130
1131    /**
1132     * Check the &origin= and/or &crossorigin= query parameters and respond appropriately.
1133     *
1134     * If no origin or crossorigin parameter is present, nothing happens.
1135     * If both are present, a 403 status code is set and false is returned.
1136     *
1137     * If an origin parameter is present but doesn't match the Origin header, a 403 status code
1138     * is set and false is returned.
1139     * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
1140     * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
1141     * headers are set.
1142     * https://www.w3.org/TR/cors/#resource-requests
1143     * https://www.w3.org/TR/cors/#resource-preflight-requests
1144     *
1145     * If the crossorigin parameter is set, but the current session provider is not safe against CSRF,
1146     * a 403 status code is set and false is returned.
1147     * If it is set and the session is safe, then the appropriate CORS headers are set.
1148     *
1149     * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
1150     */
1151    protected function handleCORS() {
1152        $originParam = $this->getParameter( 'origin' ); // defaults to null
1153        $crossOriginParam = $this->getParameter( 'crossorigin' ); // defaults to false
1154        if ( $originParam === null && !$crossOriginParam ) {
1155            // No origin/crossorigin parameter, nothing to do
1156            return true;
1157        }
1158
1159        $request = $this->getRequest();
1160        $response = $request->response();
1161        $requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
1162        $preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
1163
1164        $allowTiming = false;
1165        $varyOrigin = true;
1166
1167        if ( $originParam !== null && $crossOriginParam ) {
1168            $response->statusHeader( 403 );
1169            $response->header( 'Cache-control: no-cache' );
1170            echo "'origin' and 'crossorigin' parameters cannot be used together\n";
1171
1172            return false;
1173        }
1174        if ( $crossOriginParam && !$request->getSession()->getProvider()->safeAgainstCsrf() && !$preflight ) {
1175            $response->statusHeader( 403 );
1176            $response->header( 'Cache-control: no-cache' );
1177            $language = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( 'en' );
1178            $described = $request->getSession()->getProvider()->describe( $language );
1179            echo "'crossorigin' cannot be used with $described\n";
1180
1181            return false;
1182        }
1183
1184        if ( $originParam === '*' || $crossOriginParam ) {
1185            // Request for CORS without browser-supplied credentials (e.g. cookies):
1186            // may be anonymous (origin=*) or authenticated with request-supplied
1187            // credentials (crossorigin=1 + Authorization header).
1188            // Technically we should check for the presence of an Origin header
1189            // and not process it as CORS if it's not set, but that would
1190            // require us to vary on Origin for all 'origin=*' requests which
1191            // we don't want to do.
1192            $matchedOrigin = true;
1193            $allowOrigin = '*';
1194            $allowCredentials = 'false';
1195            $varyOrigin = false; // No need to vary
1196        } else {
1197            // Non-anonymous CORS, check we allow the domain
1198
1199            // Origin: header is a space-separated list of origins, check all of them
1200            $originHeader = $request->getHeader( 'Origin' );
1201            if ( $originHeader === false ) {
1202                $origins = [];
1203            } else {
1204                $originHeader = trim( $originHeader );
1205                $origins = preg_split( '/\s+/', $originHeader );
1206            }
1207
1208            if ( !in_array( $originParam, $origins ) ) {
1209                // origin parameter set but incorrect
1210                // Send a 403 response
1211                $response->statusHeader( 403 );
1212                $response->header( 'Cache-Control: no-cache' );
1213                echo "'origin' parameter does not match Origin header\n";
1214
1215                return false;
1216            }
1217
1218            $config = $this->getConfig();
1219            $origin = Origin::parseHeaderList( $origins );
1220            $matchedOrigin = $origin->match(
1221                $config->get( MainConfigNames::CrossSiteAJAXdomains ),
1222                $config->get( MainConfigNames::CrossSiteAJAXdomainExceptions )
1223            );
1224
1225            $allowOrigin = $originHeader;
1226            $allowCredentials = 'true';
1227            $allowTiming = $originHeader;
1228        }
1229
1230        if ( $matchedOrigin ) {
1231            if ( $preflight ) {
1232                // We allow the actual request to send the following headers
1233                $requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
1234                $allowedHeaders = $this->getConfig()->get( MainConfigNames::AllowedCorsHeaders );
1235                if ( $requestedHeaders !== false ) {
1236                    if ( !self::matchRequestedHeaders( $requestedHeaders, $allowedHeaders ) ) {
1237                        $response->header( 'MediaWiki-CORS-Rejection: Unsupported header requested in preflight' );
1238                        return true;
1239                    }
1240                    $response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
1241                }
1242
1243                // We only allow the actual request to be GET, POST, or HEAD
1244                $response->header( 'Access-Control-Allow-Methods: POST, GET, HEAD' );
1245            }
1246
1247            $response->header( "Access-Control-Allow-Origin: $allowOrigin" );
1248            $response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
1249            // https://www.w3.org/TR/resource-timing/#timing-allow-origin
1250            if ( $allowTiming !== false ) {
1251                $response->header( "Timing-Allow-Origin: $allowTiming" );
1252            }
1253
1254            if ( !$preflight ) {
1255                $response->header(
1256                    'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag, '
1257                    . 'MediaWiki-Login-Suppressed'
1258                );
1259            }
1260        } else {
1261            $response->header( 'MediaWiki-CORS-Rejection: Origin mismatch' );
1262        }
1263
1264        if ( $varyOrigin ) {
1265            $this->getOutput()->addVaryHeader( 'Origin' );
1266        }
1267
1268        return true;
1269    }
1270
1271    /**
1272     * Attempt to validate the value of Access-Control-Request-Headers against a list
1273     * of headers that we allow the follow up request to send.
1274     *
1275     * @param string $requestedHeaders Comma separated list of HTTP headers
1276     * @param string[] $allowedHeaders List of allowed HTTP headers
1277     * @return bool True if all requested headers are in the list of allowed headers
1278     */
1279    protected static function matchRequestedHeaders( $requestedHeaders, $allowedHeaders ) {
1280        if ( trim( $requestedHeaders ) === '' ) {
1281            return true;
1282        }
1283        $requestedHeaders = explode( ',', $requestedHeaders );
1284        $allowedHeaders = array_change_key_case(
1285            array_fill_keys( $allowedHeaders, true ), CASE_LOWER );
1286        foreach ( $requestedHeaders as $rHeader ) {
1287            $rHeader = strtolower( trim( $rHeader ) );
1288            if ( !isset( $allowedHeaders[$rHeader] ) ) {
1289                LoggerFactory::getInstance( 'api-warning' )->warning(
1290                    'CORS preflight failed on requested header: {header}', [
1291                        'header' => $rHeader
1292                    ]
1293                );
1294                return false;
1295            }
1296        }
1297        return true;
1298    }
1299
1300    /**
1301     * Send caching headers
1302     * @param bool $isError Whether an error response is being output
1303     * @since 1.26 added $isError parameter
1304     */
1305    protected function sendCacheHeaders( $isError ) {
1306        $response = $this->getRequest()->response();
1307        $out = $this->getOutput();
1308
1309        $out->addVaryHeader( 'Treat-as-Untrusted' );
1310
1311        $config = $this->getConfig();
1312
1313        if ( $config->get( MainConfigNames::VaryOnXFP ) ) {
1314            $out->addVaryHeader( 'X-Forwarded-Proto' );
1315        }
1316
1317        if ( !$isError && $this->mModule &&
1318            ( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
1319        ) {
1320            $etag = $this->mModule->getConditionalRequestData( 'etag' );
1321            if ( $etag !== null ) {
1322                $response->header( "ETag: $etag" );
1323            }
1324            $lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
1325            if ( $lastMod !== null ) {
1326                $response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
1327            }
1328        }
1329
1330        // The logic should be:
1331        // $this->mCacheControl['max-age'] is set?
1332        //    Use it, the module knows better than our guess.
1333        // !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
1334        //    Use 0 because we can guess caching is probably the wrong thing to do.
1335        // Use $this->getParameter( 'maxage' ), which already defaults to 0.
1336        $maxage = 0;
1337        if ( isset( $this->mCacheControl['max-age'] ) ) {
1338            $maxage = $this->mCacheControl['max-age'];
1339        } elseif ( ( !$isError && $this->mModule && !$this->mModule->isWriteMode() ) ||
1340            $this->mCacheMode !== 'private'
1341        ) {
1342            $maxage = $this->getParameter( 'maxage' );
1343        }
1344        $privateCache = 'private, must-revalidate, max-age=' . $maxage;
1345
1346        if ( $this->mCacheMode == 'private' ) {
1347            $response->header( "Cache-Control: $privateCache" );
1348            return;
1349        }
1350
1351        if ( $this->mCacheMode == 'anon-public-user-private' ) {
1352            $out->addVaryHeader( 'Cookie' );
1353            $response->header( $out->getVaryHeader() );
1354            if ( $this->getRequest()->getSession()->isPersistent() ) {
1355                // Logged in or otherwise has session (e.g. anonymous users who have edited)
1356                // Mark request private
1357                $response->header( "Cache-Control: $privateCache" );
1358
1359                return;
1360            } // else anonymous, send public headers below
1361        }
1362
1363        // Send public headers
1364        $response->header( $out->getVaryHeader() );
1365
1366        // If nobody called setCacheMaxAge(), use the (s)maxage parameters
1367        if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
1368            $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
1369        }
1370        if ( !isset( $this->mCacheControl['max-age'] ) ) {
1371            $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
1372        }
1373
1374        if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
1375            // Public cache not requested
1376            // Sending a Vary header in this case is harmless, and protects us
1377            // against conditional calls of setCacheMaxAge().
1378            $response->header( "Cache-Control: $privateCache" );
1379
1380            return;
1381        }
1382
1383        $this->mCacheControl['public'] = true;
1384
1385        // Send an Expires header
1386        $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
1387        $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
1388        $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
1389
1390        // Construct the Cache-Control header
1391        $ccHeader = '';
1392        $separator = '';
1393        foreach ( $this->mCacheControl as $name => $value ) {
1394            if ( is_bool( $value ) ) {
1395                if ( $value ) {
1396                    $ccHeader .= $separator . $name;
1397                    $separator = ', ';
1398                }
1399            } else {
1400                $ccHeader .= $separator . "$name=$value";
1401                $separator = ', ';
1402            }
1403        }
1404
1405        $response->header( "Cache-Control: $ccHeader" );
1406    }
1407
1408    /**
1409     * Create the printer for error output
1410     */
1411    private function createErrorPrinter() {
1412        if ( !$this->mPrinter ) {
1413            $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
1414            if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
1415                $value = self::API_DEFAULT_FORMAT;
1416            }
1417            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable getVal does not return null here
1418            $this->mPrinter = $this->createPrinterByName( $value );
1419        }
1420
1421        // Printer may not be able to handle errors. This is particularly
1422        // likely if the module returns something for getCustomPrinter().
1423        if ( !$this->mPrinter->canPrintErrors() ) {
1424            $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
1425        }
1426    }
1427
1428    /**
1429     * Create an error message for the given throwable.
1430     *
1431     * If an ApiUsageException, errors/warnings will be extracted from the
1432     * embedded StatusValue.
1433     *
1434     * Any other throwable will be returned with a generic code and wrapper
1435     * text around the throwable's (presumably English) message as a single
1436     * error (no warnings).
1437     *
1438     * @param Throwable $e
1439     * @param string $type 'error' or 'warning'
1440     * @return ApiMessage[]
1441     * @since 1.27
1442     */
1443    protected function errorMessagesFromException( Throwable $e, $type = 'error' ) {
1444        $messages = [];
1445        if ( $e instanceof ApiUsageException ) {
1446            foreach ( $e->getStatusValue()->getMessages( $type ) as $msg ) {
1447                $messages[] = ApiMessage::create( $msg );
1448            }
1449        } elseif ( $type !== 'error' ) {
1450            // None of the rest have any messages for non-error types
1451        } else {
1452            // TODO: Avoid embedding arbitrary class names in the error code.
1453            $class = preg_replace( '#^Wikimedia\\\\Rdbms\\\\#', '', get_class( $e ) );
1454            $code = 'internal_api_error_' . $class;
1455            $data = [ 'errorclass' => get_class( $e ) ];
1456            if ( MWExceptionRenderer::shouldShowExceptionDetails() ) {
1457                if ( $e instanceof ILocalizedException ) {
1458                    $msg = $e->getMessageObject();
1459                } elseif ( $e instanceof MessageSpecifier ) {
1460                    $msg = Message::newFromSpecifier( $e );
1461                } else {
1462                    $msg = wfEscapeWikiText( $e->getMessage() );
1463                }
1464                $params = [ 'apierror-exceptioncaught', WebRequest::getRequestId(), $msg ];
1465            } else {
1466                $params = [ 'apierror-exceptioncaughttype', WebRequest::getRequestId(), get_class( $e ) ];
1467            }
1468
1469            $messages[] = ApiMessage::create( $params, $code, $data );
1470        }
1471        return $messages;
1472    }
1473
1474    /**
1475     * Replace the result data with the information about a throwable.
1476     * @param Throwable $e
1477     * @return string[] Error codes
1478     */
1479    protected function substituteResultWithError( Throwable $e ) {
1480        $result = $this->getResult();
1481        $formatter = $this->getErrorFormatter();
1482        $config = $this->getConfig();
1483        $errorCodes = [];
1484
1485        // Remember existing warnings and errors across the reset
1486        $errors = $result->getResultData( [ 'errors' ] );
1487        $warnings = $result->getResultData( [ 'warnings' ] );
1488        $result->reset();
1489        if ( $warnings !== null ) {
1490            $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
1491        }
1492        if ( $errors !== null ) {
1493            $result->addValue( null, 'errors', $errors, ApiResult::NO_SIZE_CHECK );
1494
1495            // Collect the copied error codes for the return value
1496            foreach ( $errors as $error ) {
1497                if ( isset( $error['code'] ) ) {
1498                    $errorCodes[$error['code']] = true;
1499                }
1500            }
1501        }
1502
1503        // Add errors from the exception
1504        $modulePath = $e instanceof ApiUsageException ? $e->getModulePath() : null;
1505        foreach ( $this->errorMessagesFromException( $e, 'error' ) as $msg ) {
1506            if ( ApiErrorFormatter::isValidApiCode( $msg->getApiCode() ) ) {
1507                $errorCodes[$msg->getApiCode()] = true;
1508            } else {
1509                LoggerFactory::getInstance( 'api-warning' )->error( 'Invalid API error code "{code}"', [
1510                    'code' => $msg->getApiCode(),
1511                    'exception' => $e,
1512                ] );
1513                $errorCodes['<invalid-code>'] = true;
1514            }
1515            $formatter->addError( $modulePath, $msg );
1516        }
1517        foreach ( $this->errorMessagesFromException( $e, 'warning' ) as $msg ) {
1518            $formatter->addWarning( $modulePath, $msg );
1519        }
1520
1521        // Add additional data. Path depends on whether we're in BC mode or not.
1522        // Data depends on the type of exception.
1523        if ( $formatter instanceof ApiErrorFormatter_BackCompat ) {
1524            $path = [ 'error' ];
1525        } else {
1526            $path = null;
1527        }
1528        if ( $e instanceof ApiUsageException ) {
1529            $link = (string)MediaWikiServices::getInstance()->getUrlUtils()->expand( wfScript( 'api' ) );
1530            $result->addContentValue(
1531                $path,
1532                'docref',
1533                trim(
1534                    $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text()
1535                    . ' '
1536                    . $this->msg( 'api-usage-mailinglist-ref' )->inLanguage( $formatter->getLanguage() )->text()
1537                )
1538            );
1539        } elseif ( $config->get( MainConfigNames::ShowExceptionDetails ) ) {
1540            $result->addContentValue(
1541                $path,
1542                'trace',
1543                $this->msg( 'api-exception-trace',
1544                    get_class( $e ),
1545                    $e->getFile(),
1546                    $e->getLine(),
1547                    MWExceptionHandler::getRedactedTraceAsString( $e )
1548                )->inLanguage( $formatter->getLanguage() )->text()
1549            );
1550        }
1551
1552        // Add the id and such
1553        $this->addRequestedFields( [ 'servedby' ] );
1554
1555        return array_keys( $errorCodes );
1556    }
1557
1558    /**
1559     * Add requested fields to the result
1560     * @param string[] $force Which fields to force even if not requested. Accepted values are:
1561     *  - servedby
1562     */
1563    protected function addRequestedFields( $force = [] ) {
1564        $result = $this->getResult();
1565
1566        $requestid = $this->getParameter( 'requestid' );
1567        if ( $requestid !== null ) {
1568            $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
1569        }
1570
1571        if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) && (
1572            in_array( 'servedby', $force, true ) || $this->getParameter( 'servedby' )
1573        ) ) {
1574            $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
1575        }
1576
1577        if ( $this->getParameter( 'curtimestamp' ) ) {
1578            $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601 ), ApiResult::NO_SIZE_CHECK );
1579        }
1580
1581        if ( $this->getParameter( 'responselanginfo' ) ) {
1582            $result->addValue(
1583                null,
1584                'uselang',
1585                $this->getLanguage()->getCode(),
1586                ApiResult::NO_SIZE_CHECK
1587            );
1588            $result->addValue(
1589                null,
1590                'errorlang',
1591                $this->getErrorFormatter()->getLanguage()->getCode(),
1592                ApiResult::NO_SIZE_CHECK
1593            );
1594        }
1595    }
1596
1597    /**
1598     * Set up for the execution.
1599     * @return array
1600     */
1601    protected function setupExecuteAction() {
1602        $this->addRequestedFields();
1603
1604        $params = $this->extractRequestParams();
1605        $this->mAction = $params['action'];
1606
1607        return $params;
1608    }
1609
1610    /**
1611     * Set up the module for response
1612     * @return ApiBase The module that will handle this action
1613     * @throws ApiUsageException
1614     */
1615    protected function setupModule() {
1616        // Instantiate the module requested by the user
1617        $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
1618        if ( $module === null ) {
1619            // Probably can't happen
1620            // @codeCoverageIgnoreStart
1621            $this->dieWithError(
1622                [ 'apierror-unknownaction', wfEscapeWikiText( $this->mAction ) ],
1623                'unknown_action'
1624            );
1625            // @codeCoverageIgnoreEnd
1626        }
1627        $moduleParams = $module->extractRequestParams();
1628
1629        // Check token, if necessary
1630        if ( $module->needsToken() === true ) {
1631            throw new LogicException(
1632                "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
1633                'See documentation for ApiBase::needsToken for details.'
1634            );
1635        }
1636        if ( $module->needsToken() ) {
1637            if ( !$module->mustBePosted() ) {
1638                throw new LogicException(
1639                    "Module '{$module->getModuleName()}' must require POST to use tokens."
1640                );
1641            }
1642
1643            if ( !isset( $moduleParams['token'] ) ) {
1644                // Probably can't happen
1645                // @codeCoverageIgnoreStart
1646                $module->dieWithError( [ 'apierror-missingparam', 'token' ] );
1647                // @codeCoverageIgnoreEnd
1648            }
1649
1650            $module->requirePostedParameters( [ 'token' ] );
1651
1652            if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
1653                $module->dieWithError( 'apierror-badtoken' );
1654            }
1655        }
1656
1657        // @phan-suppress-next-line PhanTypeMismatchReturnNullable T240141
1658        return $module;
1659    }
1660
1661    /**
1662     * @return array
1663     */
1664    private function getMaxLag() {
1665        $services = MediaWikiServices::getInstance();
1666        $dbLag = $services->getDBLoadBalancer()->getMaxLag();
1667        $lagInfo = [
1668            'host' => $dbLag[0],
1669            'lag' => $dbLag[1],
1670            'type' => 'db'
1671        ];
1672
1673        $jobQueueLagFactor =
1674            $this->getConfig()->get( MainConfigNames::JobQueueIncludeInMaxLagFactor );
1675        if ( $jobQueueLagFactor ) {
1676            // Turn total number of jobs into seconds by using the configured value
1677            $totalJobs = array_sum( $services->getJobQueueGroup()->getQueueSizes() );
1678            $jobQueueLag = $totalJobs / (float)$jobQueueLagFactor;
1679            if ( $jobQueueLag > $lagInfo['lag'] ) {
1680                $lagInfo = [
1681                    'host' => wfHostname(), // XXX: Is there a better value that could be used?
1682                    'lag' => $jobQueueLag,
1683                    'type' => 'jobqueue',
1684                    'jobs' => $totalJobs,
1685                ];
1686            }
1687        }
1688
1689        $this->getHookRunner()->onApiMaxLagInfo( $lagInfo );
1690
1691        return $lagInfo;
1692    }
1693
1694    /**
1695     * Check the max lag if necessary
1696     * @param ApiBase $module Api module being used
1697     * @param array $params Array an array containing the request parameters.
1698     * @return bool True on success, false should exit immediately
1699     */
1700    protected function checkMaxLag( $module, $params ) {
1701        if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
1702            $maxLag = $params['maxlag'];
1703            $lagInfo = $this->getMaxLag();
1704            if ( $lagInfo['lag'] > $maxLag ) {
1705                $response = $this->getRequest()->response();
1706
1707                $response->header( 'Retry-After: ' . max( (int)$maxLag, 5 ) );
1708                $response->header( 'X-Database-Lag: ' . (int)$lagInfo['lag'] );
1709
1710                if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) {
1711                    $this->dieWithError(
1712                        [ 'apierror-maxlag', $lagInfo['lag'], $lagInfo['host'] ],
1713                        'maxlag',
1714                        $lagInfo
1715                    );
1716                }
1717
1718                $this->dieWithError( [ 'apierror-maxlag-generic', $lagInfo['lag'] ], 'maxlag', $lagInfo );
1719            }
1720        }
1721
1722        return true;
1723    }
1724
1725    /**
1726     * Check selected RFC 7232 precondition headers
1727     *
1728     * RFC 7232 envisions a particular model where you send your request to "a
1729     * resource", and for write requests that you can read "the resource" by
1730     * changing the method to GET. When the API receives a GET request, it
1731     * works out even though "the resource" from RFC 7232's perspective might
1732     * be many resources from MediaWiki's perspective. But it totally fails for
1733     * a POST, since what HTTP sees as "the resource" is probably just
1734     * "/api.php" with all the interesting bits in the body.
1735     *
1736     * Therefore, we only support RFC 7232 precondition headers for GET (and
1737     * HEAD). That means we don't need to bother with If-Match and
1738     * If-Unmodified-Since since they only apply to modification requests.
1739     *
1740     * And since we don't support Range, If-Range is ignored too.
1741     *
1742     * @since 1.26
1743     * @param ApiBase $module Api module being used
1744     * @return bool True on success, false should exit immediately
1745     */
1746    protected function checkConditionalRequestHeaders( $module ) {
1747        if ( $this->mInternalMode ) {
1748            // No headers to check in internal mode
1749            return true;
1750        }
1751
1752        if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) {
1753            // Don't check POSTs
1754            return true;
1755        }
1756
1757        $return304 = false;
1758
1759        $ifNoneMatch = array_diff(
1760            $this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: [],
1761            [ '' ]
1762        );
1763        if ( $ifNoneMatch ) {
1764            // @phan-suppress-next-line PhanImpossibleTypeComparison
1765            if ( $ifNoneMatch === [ '*' ] ) {
1766                // API responses always "exist"
1767                $etag = '*';
1768            } else {
1769                $etag = $module->getConditionalRequestData( 'etag' );
1770            }
1771        }
1772        // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $etag is declared when $ifNoneMatch is true
1773        if ( $ifNoneMatch && $etag !== null ) {
1774            $test = str_starts_with( $etag, 'W/' ) ? substr( $etag, 2 ) : $etag;
1775            $match = array_map( static function ( $s ) {
1776                return str_starts_with( $s, 'W/' ) ? substr( $s, 2 ) : $s;
1777            }, $ifNoneMatch );
1778            $return304 = in_array( $test, $match, true );
1779        } else {
1780            $value = trim( $this->getRequest()->getHeader( 'If-Modified-Since' ) );
1781
1782            // Some old browsers sends sizes after the date, like this:
1783            //  Wed, 20 Aug 2003 06:51:19 GMT; length=5202
1784            // Ignore that.
1785            $i = strpos( $value, ';' );
1786            if ( $i !== false ) {
1787                $value = trim( substr( $value, 0, $i ) );
1788            }
1789
1790            if ( $value !== '' ) {
1791                try {
1792                    $ts = new ConvertibleTimestamp( $value );
1793                    if (
1794                        // RFC 7231 IMF-fixdate
1795                        $ts->getTimestamp( TS_RFC2822 ) === $value ||
1796                        // RFC 850
1797                        $ts->format( 'l, d-M-y H:i:s' ) . ' GMT' === $value ||
1798                        // asctime (with and without space-padded day)
1799                        $ts->format( 'D M j H:i:s Y' ) === $value ||
1800                        $ts->format( 'D M  j H:i:s Y' ) === $value
1801                    ) {
1802                        $config = $this->getConfig();
1803                        $lastMod = $module->getConditionalRequestData( 'last-modified' );
1804                        if ( $lastMod !== null ) {
1805                            // Mix in some MediaWiki modification times
1806                            $modifiedTimes = [
1807                                'page' => $lastMod,
1808                                'user' => $this->getUser()->getTouched(),
1809                                'epoch' => $config->get( MainConfigNames::CacheEpoch ),
1810                            ];
1811
1812                            if ( $config->get( MainConfigNames::UseCdn ) ) {
1813                                // T46570: the core page itself may not change, but resources might
1814                                $modifiedTimes['sepoch'] = wfTimestamp(
1815                                    TS_MW, time() - $config->get( MainConfigNames::CdnMaxAge )
1816                                );
1817                            }
1818                            $this->getHookRunner()->onOutputPageCheckLastModified( $modifiedTimes, $this->getOutput() );
1819                            $lastMod = max( $modifiedTimes );
1820                            $return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
1821                        }
1822                    }
1823                } catch ( TimestampException ) {
1824                    // Invalid timestamp, ignore it
1825                }
1826            }
1827        }
1828
1829        if ( $return304 ) {
1830            $this->getRequest()->response()->statusHeader( 304 );
1831
1832            // Avoid outputting the compressed representation of a zero-length body
1833            AtEase::suppressWarnings();
1834            ini_set( 'zlib.output_compression', 0 );
1835            AtEase::restoreWarnings();
1836            wfResetOutputBuffers( false );
1837
1838            return false;
1839        }
1840
1841        return true;
1842    }
1843
1844    /**
1845     * Check for sufficient permissions to execute
1846     * @param ApiBase $module An Api module
1847     */
1848    protected function checkExecutePermissions( $module ) {
1849        $user = $this->getUser();
1850        if ( $module->isReadMode() && !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) &&
1851            !$this->getAuthority()->isAllowed( 'read' )
1852        ) {
1853            $this->dieWithError( 'apierror-readapidenied' );
1854        }
1855
1856        if ( $module->isWriteMode() ) {
1857            if ( !$this->mEnableWrite ) {
1858                $this->dieWithError( 'apierror-noapiwrite' );
1859            } elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) {
1860                $this->dieWithError( 'apierror-promised-nonwrite-api' );
1861            }
1862
1863            $this->checkReadOnly( $module );
1864        }
1865
1866        // Allow extensions to stop execution for arbitrary reasons.
1867        // TODO: change hook to accept Authority
1868        $message = 'hookaborted';
1869        if ( !$this->getHookRunner()->onApiCheckCanExecute( $module, $user, $message ) ) {
1870            $this->dieWithError( $message );
1871        }
1872    }
1873
1874    /**
1875     * Check if the DB is read-only for this user
1876     * @param ApiBase $module An Api module
1877     */
1878    protected function checkReadOnly( $module ) {
1879        if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
1880            $this->dieReadOnly();
1881        }
1882
1883        if ( $module->isWriteMode()
1884            && $this->getUser()->isBot()
1885            && MediaWikiServices::getInstance()->getDBLoadBalancer()->hasReplicaServers()
1886        ) {
1887            $this->checkBotReadOnly();
1888        }
1889    }
1890
1891    /**
1892     * Check whether we are readonly for bots
1893     */
1894    private function checkBotReadOnly() {
1895        // Figure out how many servers have passed the lag threshold
1896        $numLagged = 0;
1897        $lagLimit = $this->getConfig()->get( MainConfigNames::APIMaxLagThreshold );
1898        $laggedServers = [];
1899        $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
1900        foreach ( $loadBalancer->getLagTimes() as $serverIndex => $lag ) {
1901            if ( $lag > $lagLimit ) {
1902                ++$numLagged;
1903                $laggedServers[] = $loadBalancer->getServerName( $serverIndex ) . " ({$lag}s)";
1904            }
1905        }
1906
1907        // If a majority of replica DBs are too lagged then disallow writes
1908        $replicaCount = $loadBalancer->getServerCount() - 1;
1909        if ( $numLagged >= ceil( $replicaCount / 2 ) ) {
1910            $laggedServers = implode( ', ', $laggedServers );
1911            wfDebugLog(
1912                'api-readonly', // Deprecate this channel in favor of api-warning?
1913                "Api request failed as read only because the following DBs are lagged: $laggedServers"
1914            );
1915            LoggerFactory::getInstance( 'api-warning' )->warning(
1916                "Api request failed as read only because the following DBs are lagged: {laggeddbs}", [
1917                    'laggeddbs' => $laggedServers,
1918                ]
1919            );
1920
1921            $this->dieWithError(
1922                'readonly_lag',
1923                'readonly',
1924                [ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ]
1925            );
1926        }
1927    }
1928
1929    /**
1930     * Check asserts of the user's rights
1931     * @param array $params
1932     */
1933    protected function checkAsserts( $params ) {
1934        if ( isset( $params['assert'] ) ) {
1935            $user = $this->getUser();
1936            switch ( $params['assert'] ) {
1937                case 'anon':
1938                    if ( $user->isRegistered() ) {
1939                        $this->dieWithError( 'apierror-assertanonfailed' );
1940                    }
1941                    break;
1942                case 'user':
1943                    if ( !$user->isRegistered() ) {
1944                        $this->dieWithError( 'apierror-assertuserfailed' );
1945                    }
1946                    break;
1947                case 'bot':
1948                    if ( !$this->getAuthority()->isAllowed( 'bot' ) ) {
1949                        $this->dieWithError( 'apierror-assertbotfailed' );
1950                    }
1951                    break;
1952            }
1953        }
1954        if ( isset( $params['assertuser'] ) ) {
1955            // TODO inject stuff, see T265644
1956            $assertUser = MediaWikiServices::getInstance()->getUserFactory()
1957                ->newFromName( $params['assertuser'], UserRigorOptions::RIGOR_NONE );
1958            if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) {
1959                $this->dieWithError(
1960                    [ 'apierror-assertnameduserfailed', wfEscapeWikiText( $params['assertuser'] ) ]
1961                );
1962            }
1963        }
1964    }
1965
1966    /**
1967     * Check POST for external response and setup result printer
1968     * @param ApiBase $module An Api module
1969     * @param array $params An array with the request parameters
1970     */
1971    protected function setupExternalResponse( $module, $params ) {
1972        $validMethods = [ 'GET', 'HEAD', 'POST', 'OPTIONS' ];
1973        $request = $this->getRequest();
1974
1975        if ( !in_array( $request->getMethod(), $validMethods ) ) {
1976            $this->dieWithError( 'apierror-invalidmethod', null, null, 405 );
1977        }
1978
1979        if ( !$request->wasPosted() && $module->mustBePosted() ) {
1980            // Module requires POST. GET request might still be allowed
1981            // if $wgDebugApi is true, otherwise fail.
1982            $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] );
1983        }
1984
1985        if ( $request->wasPosted() ) {
1986            if ( !$request->getHeader( 'Content-Type' ) ) {
1987                $this->addDeprecation(
1988                    'apiwarn-deprecation-post-without-content-type', 'post-without-content-type'
1989                );
1990            }
1991            $contentLength = $request->getHeader( 'Content-Length' );
1992            $maxPostSize = wfShorthandToInteger( ini_get( 'post_max_size' ), 0 );
1993            if ( $maxPostSize && $contentLength > $maxPostSize ) {
1994                $this->dieWithError(
1995                    [ 'apierror-http-contenttoolarge', Message::sizeParam( $maxPostSize ) ],
1996                    null, null, 413
1997                );
1998            }
1999        }
2000
2001        // See if custom printer is used
2002        $this->mPrinter = $module->getCustomPrinter() ??
2003            // Create an appropriate printer if not set
2004            $this->createPrinterByName( $params['format'] );
2005
2006        if ( $request->getProtocol() === 'http' &&
2007            (
2008                $this->getConfig()->get( MainConfigNames::ForceHTTPS ) ||
2009                $request->getSession()->shouldForceHTTPS() ||
2010                $this->getUser()->requiresHTTPS()
2011            )
2012        ) {
2013            $this->addDeprecation( 'apiwarn-deprecation-httpsexpected', 'https-expected' );
2014        }
2015    }
2016
2017    /**
2018     * Execute the actual module, without any error handling
2019     */
2020    protected function executeAction() {
2021        $params = $this->setupExecuteAction();
2022
2023        // Check asserts early so e.g. errors in parsing a module's parameters due to being
2024        // logged out don't override the client's intended "am I logged in?" check.
2025        $this->checkAsserts( $params );
2026
2027        $module = $this->setupModule();
2028        $this->mModule = $module;
2029
2030        if ( !$this->mInternalMode ) {
2031            ProfilingContext::singleton()->init( MW_ENTRY_POINT, $module->getModuleName() );
2032            $this->setRequestExpectations( $module );
2033        }
2034
2035        $this->checkExecutePermissions( $module );
2036
2037        if ( !$this->checkMaxLag( $module, $params ) ) {
2038            return;
2039        }
2040
2041        if ( !$this->checkConditionalRequestHeaders( $module ) ) {
2042            return;
2043        }
2044
2045        if ( !$this->mInternalMode ) {
2046            $this->setupExternalResponse( $module, $params );
2047        }
2048
2049        $scope = LoggerFactory::getContext()->addScoped( [
2050            'context.api_module_name' => $module->getModuleName(),
2051            'context.api_client_useragent' => $this->getUserAgent(),
2052        ] );
2053        $module->execute();
2054        ScopedCallback::consume( $scope );
2055        $this->getHookRunner()->onAPIAfterExecute( $module );
2056
2057        $this->reportUnusedParams();
2058
2059        if ( !$this->mInternalMode ) {
2060            MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
2061
2062            $this->printResult();
2063        }
2064    }
2065
2066    /**
2067     * Set database connection, query, and write expectations given this module request
2068     */
2069    protected function setRequestExpectations( ApiBase $module ) {
2070        $request = $this->getRequest();
2071
2072        $trxLimits = $this->getConfig()->get( MainConfigNames::TrxProfilerLimits );
2073        $trxProfiler = Profiler::instance()->getTransactionProfiler();
2074        $trxProfiler->setLogger( LoggerFactory::getInstance( 'rdbms' ) );
2075        $trxProfiler->setStatsFactory( MediaWikiServices::getInstance()->getStatsFactory() );
2076        $trxProfiler->setRequestMethod( $request->getMethod() );
2077        if ( $request->hasSafeMethod() ) {
2078            $trxProfiler->setExpectations( $trxLimits['GET'], __METHOD__ );
2079        } elseif ( $request->wasPosted() && !$module->isWriteMode() ) {
2080            $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ );
2081        } else {
2082            $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ );
2083        }
2084    }
2085
2086    /**
2087     * Log the preceding request
2088     * @param float $time Time in seconds
2089     * @param Throwable|null $e Throwable caught while processing the request
2090     */
2091    protected function logRequest( $time, ?Throwable $e = null ) {
2092        $request = $this->getRequest();
2093
2094        $user = $this->getUser();
2095        $performer = [
2096            'user_text' => $user->getName(),
2097        ];
2098        if ( $user->isRegistered() ) {
2099            $performer['user_id'] = $user->getId();
2100        }
2101        $logCtx = [
2102            // https://gerrit.wikimedia.org/g/mediawiki/event-schemas/+/master/jsonschema/mediawiki/api/request
2103            '$schema' => '/mediawiki/api/request/1.0.0',
2104            'meta' => [
2105                'request_id' => WebRequest::getRequestId(),
2106                'id' => MediaWikiServices::getInstance()
2107                    ->getGlobalIdGenerator()->newUUIDv4(),
2108                'domain' => $this->getConfig()->get( MainConfigNames::ServerName ),
2109                // If using the EventBus extension (as intended) with this log channel,
2110                // this stream name will map to a Kafka topic.
2111                'stream' => 'mediawiki.api-request'
2112            ],
2113            'http' => [
2114                'method' => $request->getMethod(),
2115                'client_ip' => $request->getIP()
2116            ],
2117            'performer' => $performer,
2118            'database' => WikiMap::getCurrentWikiDbDomain()->getId(),
2119            'backend_time_ms' => (int)round( $time * 1000 ),
2120        ];
2121
2122        // If set, these headers will be logged in http.request_headers.
2123        $httpRequestHeadersToLog = [ 'accept-language', 'referer', 'user-agent' ];
2124        foreach ( $httpRequestHeadersToLog as $header ) {
2125            if ( $request->getHeader( $header ) ) {
2126                // Set the header in http.request_headers
2127                $logCtx['http']['request_headers'][$header] = $request->getHeader( $header );
2128            }
2129        }
2130
2131        if ( $e ) {
2132            $logCtx['api_error_codes'] = [];
2133            foreach ( $this->errorMessagesFromException( $e ) as $msg ) {
2134                $logCtx['api_error_codes'][] = $msg->getApiCode();
2135            }
2136        }
2137
2138        // Construct space separated message for 'api' log channel
2139        $msg = "API {$request->getMethod()} " .
2140            wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
2141            " {$logCtx['http']['client_ip']} " .
2142            "T={$logCtx['backend_time_ms']}ms";
2143
2144        $sensitive = array_fill_keys( $this->getSensitiveParams(), true );
2145        foreach ( $this->getParamsUsed() as $name ) {
2146            $value = $request->getVal( $name );
2147            if ( $value === null ) {
2148                continue;
2149            }
2150
2151            if ( isset( $sensitive[$name] ) ) {
2152                $value = '[redacted]';
2153                $encValue = '[redacted]';
2154            } elseif ( strlen( $value ) > 256 ) {
2155                $value = substr( $value, 0, 256 );
2156                $encValue = $this->encodeRequestLogValue( $value ) . '[...]';
2157            } else {
2158                $encValue = $this->encodeRequestLogValue( $value );
2159            }
2160
2161            $logCtx['params'][$name] = $value;
2162            $msg .= " {$name}={$encValue}";
2163        }
2164
2165        // Log an unstructured message to the api channel.
2166        wfDebugLog( 'api', $msg, 'private' );
2167
2168        // The api-request channel a structured data log channel.
2169        wfDebugLog( 'api-request', '', 'private', $logCtx );
2170    }
2171
2172    /**
2173     * Encode a value in a format suitable for a space-separated log line.
2174     * @param string $s
2175     * @return string
2176     */
2177    protected function encodeRequestLogValue( $s ) {
2178        static $table = [];
2179        if ( !$table ) {
2180            $chars = ';@$!*(),/:';
2181            $numChars = strlen( $chars );
2182            for ( $i = 0; $i < $numChars; $i++ ) {
2183                $table[rawurlencode( $chars[$i] )] = $chars[$i];
2184            }
2185        }
2186
2187        return strtr( rawurlencode( $s ), $table );
2188    }
2189
2190    /**
2191     * Get the request parameters used in the course of the preceding execute() request
2192     * @return array
2193     */
2194    protected function getParamsUsed() {
2195        return array_keys( $this->mParamsUsed );
2196    }
2197
2198    /**
2199     * Mark parameters as used
2200     * @param string|string[] $params
2201     */
2202    public function markParamsUsed( $params ) {
2203        $this->mParamsUsed += array_fill_keys( (array)$params, true );
2204    }
2205
2206    /**
2207     * Get the request parameters that should be considered sensitive
2208     * @since 1.29
2209     * @return array
2210     */
2211    protected function getSensitiveParams() {
2212        return array_keys( $this->mParamsSensitive );
2213    }
2214
2215    /**
2216     * Mark parameters as sensitive
2217     *
2218     * This is called automatically for you when declaring a parameter
2219     * with ApiBase::PARAM_SENSITIVE.
2220     *
2221     * @since 1.29
2222     * @param string|string[] $params
2223     */
2224    public function markParamsSensitive( $params ) {
2225        $this->mParamsSensitive += array_fill_keys( (array)$params, true );
2226    }
2227
2228    /**
2229     * Get a request value, and register the fact that it was used, for logging.
2230     * @param string $name
2231     * @param string|null $default
2232     * @return string|null
2233     */
2234    public function getVal( $name, $default = null ) {
2235        $this->mParamsUsed[$name] = true;
2236
2237        $ret = $this->getRequest()->getVal( $name );
2238        if ( $ret === null ) {
2239            if ( $this->getRequest()->getArray( $name ) !== null ) {
2240                // See T12262 for why we don't just implode( '|', ... ) the
2241                // array.
2242                $this->addWarning( [ 'apiwarn-unsupportedarray', $name ] );
2243            }
2244            $ret = $default;
2245        }
2246        return $ret;
2247    }
2248
2249    /**
2250     * Get a boolean request value, and register the fact that the parameter
2251     * was used, for logging.
2252     * @param string $name
2253     * @return bool
2254     */
2255    public function getCheck( $name ) {
2256        $this->mParamsUsed[$name] = true;
2257        return $this->getRequest()->getCheck( $name );
2258    }
2259
2260    /**
2261     * Get a request upload, and register the fact that it was used, for logging.
2262     *
2263     * @since 1.21
2264     * @param string $name Parameter name
2265     * @return WebRequestUpload
2266     */
2267    public function getUpload( $name ) {
2268        $this->mParamsUsed[$name] = true;
2269
2270        return $this->getRequest()->getUpload( $name );
2271    }
2272
2273    /**
2274     * Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,
2275     * for example in case of spelling mistakes or a missing 'g' prefix for generators.
2276     */
2277    protected function reportUnusedParams() {
2278        $paramsUsed = $this->getParamsUsed();
2279        $allParams = $this->getRequest()->getValueNames();
2280
2281        if ( !$this->mInternalMode ) {
2282            // Printer has not yet executed; don't warn that its parameters are unused
2283            $printerParams = $this->mPrinter->encodeParamName(
2284                array_keys( $this->mPrinter->getFinalParams() ?: [] )
2285            );
2286            $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
2287        } else {
2288            $unusedParams = array_diff( $allParams, $paramsUsed );
2289        }
2290
2291        if ( count( $unusedParams ) ) {
2292            $this->addWarning( [
2293                'apierror-unrecognizedparams',
2294                // Do not use `wfEscapeWikiText( ... )` here for compatibility with PHP <8.1.4
2295                // https://gerrit.wikimedia.org/r/c/mediawiki/core/+/1160800/comment/92e67687_ab221188/
2296                Message::listParam( array_map( 'wfEscapeWikiText', $unusedParams ), ListType::COMMA ),
2297                count( $unusedParams )
2298            ] );
2299        }
2300    }
2301
2302    /**
2303     * Print results using the current printer
2304     *
2305     * @param int $httpCode HTTP status code, or 0 to not change
2306     */
2307    protected function printResult( $httpCode = 0 ) {
2308        if ( $this->getConfig()->get( MainConfigNames::DebugAPI ) !== false ) {
2309            $this->addWarning( 'apiwarn-wgdebugapi' );
2310        }
2311
2312        $printer = $this->mPrinter;
2313        $printer->initPrinter( false );
2314        if ( $httpCode ) {
2315            $printer->setHttpStatus( $httpCode );
2316        }
2317        $printer->execute();
2318        $printer->closePrinter();
2319    }
2320
2321    /**
2322     * @return bool
2323     */
2324    public function isReadMode() {
2325        return false;
2326    }
2327
2328    /**
2329     * See ApiBase for description.
2330     *
2331     * @return array
2332     */
2333    public function getAllowedParams() {
2334        return [
2335            'action' => [
2336                ParamValidator::PARAM_DEFAULT => 'help',
2337                ParamValidator::PARAM_TYPE => 'submodule',
2338            ],
2339            'format' => [
2340                ParamValidator::PARAM_DEFAULT => self::API_DEFAULT_FORMAT,
2341                ParamValidator::PARAM_TYPE => 'submodule',
2342            ],
2343            'maxlag' => [
2344                ParamValidator::PARAM_TYPE => 'integer'
2345            ],
2346            'smaxage' => [
2347                ParamValidator::PARAM_TYPE => 'integer',
2348                ParamValidator::PARAM_DEFAULT => 0,
2349                IntegerDef::PARAM_MIN => 0,
2350            ],
2351            'maxage' => [
2352                ParamValidator::PARAM_TYPE => 'integer',
2353                ParamValidator::PARAM_DEFAULT => 0,
2354                IntegerDef::PARAM_MIN => 0,
2355            ],
2356            'assert' => [
2357                ParamValidator::PARAM_TYPE => [ 'anon', 'user', 'bot' ]
2358            ],
2359            'assertuser' => [
2360                ParamValidator::PARAM_TYPE => 'user',
2361                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'temp' ],
2362            ],
2363            'requestid' => null,
2364            'servedby' => false,
2365            'curtimestamp' => false,
2366            'responselanginfo' => false,
2367            'origin' => null,
2368            'crossorigin' => false,
2369            'uselang' => [
2370                ParamValidator::PARAM_DEFAULT => self::API_DEFAULT_USELANG,
2371            ],
2372            'variant' => null,
2373            'errorformat' => [
2374                ParamValidator::PARAM_TYPE => [ 'plaintext', 'wikitext', 'html', 'raw', 'none', 'bc' ],
2375                ParamValidator::PARAM_DEFAULT => 'bc',
2376                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
2377            ],
2378            'errorlang' => [
2379                ParamValidator::PARAM_DEFAULT => 'uselang',
2380            ],
2381            'errorsuselocal' => [
2382                ParamValidator::PARAM_DEFAULT => false,
2383            ],
2384        ];
2385    }
2386
2387    /** @inheritDoc */
2388    protected function getExamplesMessages() {
2389        return [
2390            'action=help'
2391                => 'apihelp-help-example-main',
2392            'action=help&recursivesubmodules=1'
2393                => 'apihelp-help-example-recursive',
2394        ];
2395    }
2396
2397    /**
2398     * @inheritDoc
2399     * @phan-param array{nolead?:bool,headerlevel?:int,tocnumber?:int[]} $options
2400     */
2401    public function modifyHelp( array &$help, array $options, array &$tocData ) {
2402        // Wish PHP had an "array_insert_before". Instead, we have to manually
2403        // reindex the array to get 'permissions' in the right place.
2404        $oldHelp = $help;
2405        $help = [];
2406        foreach ( $oldHelp as $k => $v ) {
2407            if ( $k === 'submodules' ) {
2408                $help['permissions'] = '';
2409            }
2410            $help[$k] = $v;
2411        }
2412        $help['datatypes'] = '';
2413        $help['templatedparams'] = '';
2414        $help['credits'] = '';
2415
2416        // Fill 'permissions'
2417        $help['permissions'] .= Html::openElement( 'div',
2418            [ 'class' => [ 'apihelp-block', 'apihelp-permissions' ] ] );
2419        $m = $this->msg( 'api-help-permissions' );
2420        if ( !$m->isDisabled() ) {
2421            $help['permissions'] .= Html::rawElement( 'div', [ 'class' => 'apihelp-block-head' ],
2422                $m->numParams( count( self::RIGHTS_MAP ) )->parse()
2423            );
2424        }
2425        $help['permissions'] .= Html::openElement( 'dl' );
2426        // TODO inject stuff, see T265644
2427        $groupPermissionsLookup = MediaWikiServices::getInstance()->getGroupPermissionsLookup();
2428        foreach ( self::RIGHTS_MAP as $right => $rightMsg ) {
2429            $help['permissions'] .= Html::element( 'dt', [], $right );
2430
2431            $rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
2432            $help['permissions'] .= Html::rawElement( 'dd', [], $rightMsg );
2433
2434            $groups = array_map( static function ( $group ) {
2435                return $group == '*' ? 'all' : $group;
2436            }, $groupPermissionsLookup->getGroupsWithPermission( $right ) );
2437
2438            $help['permissions'] .= Html::rawElement( 'dd', [],
2439                $this->msg( 'api-help-permissions-granted-to' )
2440                    ->numParams( count( $groups ) )
2441                    ->params( Message::listParam( $groups ) )
2442                    ->parse()
2443            );
2444        }
2445        $help['permissions'] .= Html::closeElement( 'dl' );
2446        $help['permissions'] .= Html::closeElement( 'div' );
2447
2448        // Fill 'datatypes', 'templatedparams', and 'credits', if applicable
2449        if ( empty( $options['nolead'] ) ) {
2450            // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Must set when nolead is not set
2451            $level = $options['headerlevel'];
2452            // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Must set when nolead is not set
2453            $tocnumber = &$options['tocnumber'];
2454
2455            $header = $this->msg( 'api-help-datatypes-header' )->parse();
2456            $headline = Html::rawElement(
2457                'h' . min( 6, $level ),
2458                [ 'class' => 'apihelp-header', 'id' => 'main/datatypes' ],
2459                $header
2460            );
2461            $help['datatypes'] .= $headline;
2462            $help['datatypes'] .= $this->msg( 'api-help-datatypes-top' )->parseAsBlock();
2463            $help['datatypes'] .= '<dl>';
2464            foreach ( $this->getParamValidator()->knownTypes() as $type ) {
2465                $m = $this->msg( "api-help-datatype-$type" );
2466                if ( !$m->isDisabled() ) {
2467                    $help['datatypes'] .= Html::element( 'dt', [ 'id' => "main/datatype/$type" ], $type );
2468                    $help['datatypes'] .= Html::rawElement( 'dd', [], $m->parseAsBlock() );
2469                }
2470            }
2471            $help['datatypes'] .= '</dl>';
2472            if ( !isset( $tocData['main/datatypes'] ) ) {
2473                $tocnumber[$level]++;
2474                $tocData['main/datatypes'] = [
2475                    'toclevel' => count( $tocnumber ),
2476                    'level' => $level,
2477                    'anchor' => 'main/datatypes',
2478                    'line' => $header,
2479                    'number' => implode( '.', $tocnumber ),
2480                    'index' => '',
2481                ];
2482            }
2483
2484            $header = $this->msg( 'api-help-templatedparams-header' )->parse();
2485            $headline = Html::rawElement(
2486                'h' . min( 6, $level ),
2487                [ 'class' => 'apihelp-header', 'id' => 'main/templatedparams' ],
2488                $header
2489            );
2490            $help['templatedparams'] .= $headline;
2491            $help['templatedparams'] .= $this->msg( 'api-help-templatedparams' )->parseAsBlock();
2492            if ( !isset( $tocData['main/templatedparams'] ) ) {
2493                $tocnumber[$level]++;
2494                $tocData['main/templatedparams'] = [
2495                    'toclevel' => count( $tocnumber ),
2496                    'level' => $level,
2497                    'anchor' => 'main/templatedparams',
2498                    'line' => $header,
2499                    'number' => implode( '.', $tocnumber ),
2500                    'index' => '',
2501                ];
2502            }
2503
2504            $header = $this->msg( 'api-credits-header' )->parse();
2505            $headline = Html::rawElement(
2506                'h' . min( 6, $level ),
2507                [ 'class' => 'apihelp-header', 'id' => 'main/credits' ],
2508                $header
2509            );
2510            $help['credits'] .= $headline;
2511            $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
2512            if ( !isset( $tocData['main/credits'] ) ) {
2513                $tocnumber[$level]++;
2514                $tocData['main/credits'] = [
2515                    'toclevel' => count( $tocnumber ),
2516                    'level' => $level,
2517                    'anchor' => 'main/credits',
2518                    'line' => $header,
2519                    'number' => implode( '.', $tocnumber ),
2520                    'index' => '',
2521                ];
2522            }
2523        }
2524    }
2525
2526    /** @var bool|null */
2527    private $mCanApiHighLimits = null;
2528
2529    /**
2530     * Check whether the current user is allowed to use high limits
2531     * @return bool
2532     */
2533    public function canApiHighLimits() {
2534        if ( $this->mCanApiHighLimits === null ) {
2535            $this->mCanApiHighLimits = $this->getAuthority()->isAllowed( 'apihighlimits' );
2536        }
2537
2538        return $this->mCanApiHighLimits;
2539    }
2540
2541    /**
2542     * Overrides to return this instance's module manager.
2543     * @return ApiModuleManager
2544     */
2545    public function getModuleManager() {
2546        return $this->mModuleMgr;
2547    }
2548
2549    /**
2550     * Fetches the user agent used for this request
2551     *
2552     * This returns the value of the 'Api-User-Agent' header, if any,
2553     * or the standard User-Agent header, otherwise.
2554     *
2555     * @return string
2556     */
2557    public function getUserAgent() {
2558        $agent = (string)$this->getRequest()->getHeader( 'Api-user-agent' );
2559        if ( $agent == '' ) {
2560            $agent = $this->getRequest()->getHeader( 'User-agent' );
2561        }
2562
2563        return $agent;
2564    }
2565}
2566
2567/**
2568 * For really cool vim folding this needs to be at the end:
2569 * vim: foldmarker=@{,@} foldmethod=marker
2570 */
2571
2572/** @deprecated class alias since 1.43 */
2573class_alias( ApiMain::class, 'ApiMain' );