Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.19% covered (warning)
82.19%
757 / 921
55.36% covered (warning)
55.36%
31 / 56
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiMain
82.28% covered (warning)
82.28%
757 / 920
55.36% covered (warning)
55.36%
31 / 56
703.74
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
 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
92.86% covered (success)
92.86%
26 / 28
0.00% covered (danger)
0.00%
0 / 1
8.02
 handleException
76.67% covered (warning)
76.67%
23 / 30
0.00% covered (danger)
0.00%
0 / 1
8.81
 handleApiBeforeMainException
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 handleCORS
40.54% covered (danger)
40.54%
30 / 74
0.00% covered (danger)
0.00%
0 / 1
123.74
 matchRequestedHeaders
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 sendCacheHeaders
65.38% covered (warning)
65.38%
34 / 52
0.00% covered (danger)
0.00%
0 / 1
47.89
 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%
20 / 20
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.68% covered (warning)
88.68%
47 / 53
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
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Copyright Â© 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @defgroup API API
22 */
23
24namespace MediaWiki\Api;
25
26use ILocalizedException;
27use LogicException;
28use MediaWiki;
29use MediaWiki\Api\Validator\ApiParamValidator;
30use MediaWiki\Context\DerivativeContext;
31use MediaWiki\Context\IContextSource;
32use MediaWiki\Context\RequestContext;
33use MediaWiki\Debug\MWDebug;
34use MediaWiki\Html\Html;
35use MediaWiki\Logger\LoggerFactory;
36use MediaWiki\MainConfigNames;
37use MediaWiki\MediaWikiServices;
38use MediaWiki\Message\Message;
39use MediaWiki\ParamValidator\TypeDef\UserDef;
40use MediaWiki\Profiler\ProfilingContext;
41use MediaWiki\Request\FauxRequest;
42use MediaWiki\Request\WebRequest;
43use MediaWiki\Request\WebRequestUpload;
44use MediaWiki\Rest\HeaderParser\Origin;
45use MediaWiki\Session\SessionManager;
46use MediaWiki\StubObject\StubGlobalUser;
47use MediaWiki\User\UserRigorOptions;
48use MediaWiki\Utils\MWTimestamp;
49use MediaWiki\WikiMap\WikiMap;
50use MWExceptionHandler;
51use MWExceptionRenderer;
52use Profiler;
53use Throwable;
54use UnexpectedValueException;
55use Wikimedia\AtEase\AtEase;
56use Wikimedia\Message\MessageSpecifier;
57use Wikimedia\ParamValidator\ParamValidator;
58use Wikimedia\ParamValidator\TypeDef\IntegerDef;
59use Wikimedia\Stats\StatsFactory;
60use Wikimedia\Timestamp\TimestampException;
61
62/**
63 * This is the main API class, used for both external and internal processing.
64 * When executed, it will create the requested formatter object,
65 * instantiate and execute an object associated with the needed action,
66 * and use formatter to print results.
67 * In case of an exception, an error message will be printed using the same formatter.
68 *
69 * To use API from another application, run it using MediaWiki\Request\FauxRequest object, in which
70 * case any internal exceptions will not be handled but passed up to the caller.
71 * After successful execution, use getResult() for the resulting data.
72 *
73 * @newable
74 * @note marked as newable in 1.35 for lack of a better alternative,
75 *       but should use a factory in the future.
76 * @ingroup API
77 */
78class ApiMain extends ApiBase {
79    /**
80     * When no format parameter is given, this format will be used
81     */
82    private const API_DEFAULT_FORMAT = 'jsonfm';
83
84    /**
85     * When no uselang parameter is given, this language will be used
86     */
87    private const API_DEFAULT_USELANG = 'user';
88
89    /**
90     * List of available modules: action name => module class
91     */
92    private const MODULES = [
93        'login' => [
94            'class' => ApiLogin::class,
95            'services' => [
96                'AuthManager',
97                'UserIdentityUtils'
98            ],
99        ],
100        'clientlogin' => [
101            'class' => ApiClientLogin::class,
102            'services' => [
103                'AuthManager',
104                'UrlUtils',
105            ],
106        ],
107        'logout' => [
108            'class' => ApiLogout::class,
109        ],
110        'createaccount' => [
111            'class' => ApiAMCreateAccount::class,
112            'services' => [
113                'AuthManager',
114                'UrlUtils',
115            ],
116        ],
117        'linkaccount' => [
118            'class' => ApiLinkAccount::class,
119            'services' => [
120                'AuthManager',
121                'UrlUtils',
122            ],
123        ],
124        'unlinkaccount' => [
125            'class' => ApiRemoveAuthenticationData::class,
126            'services' => [
127                'AuthManager',
128            ],
129        ],
130        'changeauthenticationdata' => [
131            'class' => ApiChangeAuthenticationData::class,
132            'services' => [
133                'AuthManager',
134            ],
135        ],
136        'removeauthenticationdata' => [
137            'class' => ApiRemoveAuthenticationData::class,
138            'services' => [
139                'AuthManager',
140            ],
141        ],
142        'resetpassword' => [
143            'class' => ApiResetPassword::class,
144            'services' => [
145                'PasswordReset',
146            ]
147        ],
148        'query' => [
149            'class' => ApiQuery::class,
150            'services' => [
151                'ObjectFactory',
152                'WikiExporterFactory',
153                'TitleFormatter',
154                'TitleFactory',
155            ]
156        ],
157        'expandtemplates' => [
158            'class' => ApiExpandTemplates::class,
159            'services' => [
160                'RevisionStore',
161                'ParserFactory',
162            ]
163        ],
164        'parse' => [
165            'class' => ApiParse::class,
166            'services' => [
167                'RevisionLookup',
168                'SkinFactory',
169                'LanguageNameUtils',
170                'LinkBatchFactory',
171                'LinkCache',
172                'ContentHandlerFactory',
173                'ParserFactory',
174                'WikiPageFactory',
175                'ContentRenderer',
176                'ContentTransformer',
177                'CommentFormatter',
178                'TempUserCreator',
179                'UserFactory',
180                'UrlUtils',
181                'TitleFormatter',
182            ]
183        ],
184        'stashedit' => [
185            'class' => ApiStashEdit::class,
186            'services' => [
187                'ContentHandlerFactory',
188                'PageEditStash',
189                'RevisionLookup',
190                'StatsFactory',
191                'WikiPageFactory',
192                'TempUserCreator',
193                'UserFactory',
194            ]
195        ],
196        'opensearch' => [
197            'class' => ApiOpenSearch::class,
198            'services' => [
199                'LinkBatchFactory',
200                'SearchEngineConfig',
201                'SearchEngineFactory',
202                'UrlUtils',
203            ]
204        ],
205        'feedcontributions' => [
206            'class' => ApiFeedContributions::class,
207            'services' => [
208                'RevisionStore',
209                'LinkRenderer',
210                'LinkBatchFactory',
211                'HookContainer',
212                'DBLoadBalancerFactory',
213                'NamespaceInfo',
214                'UserFactory',
215                'CommentFormatter',
216            ]
217        ],
218        'feedrecentchanges' => [
219            'class' => ApiFeedRecentChanges::class,
220            'services' => [
221                'SpecialPageFactory',
222                'TempUserConfig',
223            ]
224        ],
225        'feedwatchlist' => [
226            'class' => ApiFeedWatchlist::class,
227            'services' => [
228                'ParserFactory',
229            ]
230        ],
231        'help' => [
232            'class' => ApiHelp::class,
233            'services' => [
234                'SkinFactory',
235            ]
236        ],
237        'paraminfo' => [
238            'class' => ApiParamInfo::class,
239            'services' => [
240                'UserFactory',
241            ],
242        ],
243        'rsd' => [
244            'class' => ApiRsd::class,
245        ],
246        'compare' => [
247            'class' => ApiComparePages::class,
248            'services' => [
249                'RevisionStore',
250                'ArchivedRevisionLookup',
251                'SlotRoleRegistry',
252                'ContentHandlerFactory',
253                'ContentTransformer',
254                'CommentFormatter',
255                'TempUserCreator',
256                'UserFactory',
257            ]
258        ],
259        'checktoken' => [
260            'class' => ApiCheckToken::class,
261        ],
262        'cspreport' => [
263            'class' => ApiCSPReport::class,
264            'services' => [
265                'UrlUtils',
266            ]
267        ],
268        'validatepassword' => [
269            'class' => ApiValidatePassword::class,
270            'services' => [
271                'AuthManager',
272                'UserFactory',
273            ]
274        ],
275
276        // Write modules
277        'purge' => [
278            'class' => ApiPurge::class,
279            'services' => [
280                'WikiPageFactory',
281                'TitleFormatter',
282            ],
283        ],
284        'setnotificationtimestamp' => [
285            'class' => ApiSetNotificationTimestamp::class,
286            'services' => [
287                'DBLoadBalancerFactory',
288                'RevisionStore',
289                'WatchedItemStore',
290                'TitleFormatter',
291                'TitleFactory',
292            ]
293        ],
294        'rollback' => [
295            'class' => ApiRollback::class,
296            'services' => [
297                'RollbackPageFactory',
298                'WatchlistManager',
299                'UserOptionsLookup',
300            ]
301        ],
302        'delete' => [
303            'class' => ApiDelete::class,
304            'services' => [
305                'RepoGroup',
306                'WatchlistManager',
307                'UserOptionsLookup',
308                'DeletePageFactory',
309            ]
310        ],
311        'undelete' => [
312            'class' => ApiUndelete::class,
313            'services' => [
314                'WatchlistManager',
315                'UserOptionsLookup',
316                'UndeletePageFactory',
317                'WikiPageFactory',
318            ]
319        ],
320        'protect' => [
321            'class' => ApiProtect::class,
322            'services' => [
323                'WatchlistManager',
324                'UserOptionsLookup',
325                'RestrictionStore',
326            ]
327        ],
328        'block' => [
329            'class' => ApiBlock::class,
330            'services' => [
331                'BlockPermissionCheckerFactory',
332                'BlockUserFactory',
333                'TitleFactory',
334                'UserIdentityLookup',
335                'WatchedItemStore',
336                'BlockUtils',
337                'BlockActionInfo',
338                'DatabaseBlockStore',
339                'WatchlistManager',
340                'UserOptionsLookup',
341            ]
342        ],
343        'unblock' => [
344            'class' => ApiUnblock::class,
345            'services' => [
346                'BlockPermissionCheckerFactory',
347                'UnblockUserFactory',
348                'UserIdentityLookup',
349                'WatchedItemStore',
350                'WatchlistManager',
351                'UserOptionsLookup',
352                'DatabaseBlockStore',
353            ]
354        ],
355        'move' => [
356            'class' => ApiMove::class,
357            'services' => [
358                'MovePageFactory',
359                'RepoGroup',
360                'WatchlistManager',
361                'UserOptionsLookup',
362            ]
363        ],
364        'edit' => [
365            'class' => ApiEditPage::class,
366            'services' => [
367                'ContentHandlerFactory',
368                'RevisionLookup',
369                'WatchedItemStore',
370                'WikiPageFactory',
371                'WatchlistManager',
372                'UserOptionsLookup',
373                'RedirectLookup',
374                'TempUserCreator',
375                'UserFactory',
376            ]
377        ],
378        'upload' => [
379            'class' => ApiUpload::class,
380            'services' => [
381                'JobQueueGroup',
382                'WatchlistManager',
383                'UserOptionsLookup',
384            ]
385        ],
386        'filerevert' => [
387            'class' => ApiFileRevert::class,
388            'services' => [
389                'RepoGroup',
390            ]
391        ],
392        'emailuser' => [
393            'class' => ApiEmailUser::class,
394            'services' => [
395                'EmailUserFactory',
396                'UserFactory',
397            ]
398        ],
399        'watch' => [
400            'class' => ApiWatch::class,
401            'services' => [
402                'WatchlistManager',
403                'TitleFormatter',
404            ]
405        ],
406        'patrol' => [
407            'class' => ApiPatrol::class,
408            'services' => [
409                'RevisionStore',
410            ]
411        ],
412        'import' => [
413            'class' => ApiImport::class,
414            'services' => [
415                'WikiImporterFactory',
416            ]
417        ],
418        'clearhasmsg' => [
419            'class' => ApiClearHasMsg::class,
420            'services' => [
421                'TalkPageNotificationManager',
422            ]
423        ],
424        'userrights' => [
425            'class' => ApiUserrights::class,
426            'services' => [
427                'UserGroupManager',
428                'WatchedItemStore',
429                'WatchlistManager',
430                'UserOptionsLookup',
431            ]
432        ],
433        'options' => [
434            'class' => ApiOptions::class,
435            'services' => [
436                'UserOptionsManager',
437                'PreferencesFactory',
438            ],
439        ],
440        'imagerotate' => [
441            'class' => ApiImageRotate::class,
442            'services' => [
443                'RepoGroup',
444                'TempFSFileFactory',
445                'TitleFactory',
446            ]
447        ],
448        'revisiondelete' => [
449            'class' => ApiRevisionDelete::class,
450        ],
451        'managetags' => [
452            'class' => ApiManageTags::class,
453        ],
454        'tag' => [
455            'class' => ApiTag::class,
456            'services' => [
457                'DBLoadBalancerFactory',
458                'RevisionStore',
459                'ChangeTagsStore',
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 MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
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 result formatter object. Only works after setupExecuteAction()
798     *
799     * @return ApiFormatBase
800     */
801    public function getPrinter() {
802        return $this->mPrinter;
803    }
804
805    /**
806     * Set how long the response should be cached.
807     *
808     * @param int $maxage
809     */
810    public function setCacheMaxAge( $maxage ) {
811        $this->setCacheControl( [
812            'max-age' => $maxage,
813            's-maxage' => $maxage
814        ] );
815    }
816
817    /**
818     * Set the type of caching headers which will be sent.
819     *
820     * @param string $mode One of:
821     *    - 'public':     Cache this object in public caches, if the maxage or smaxage
822     *         parameter is set, or if setCacheMaxAge() was called. If a maximum age is
823     *         not provided by any of these means, the object will be private.
824     *    - 'private':    Cache this object only in private client-side caches.
825     *    - 'anon-public-user-private': Make this object cacheable for logged-out
826     *         users, but private for logged-in users. IMPORTANT: If this is set, it must be
827     *         set consistently for a given URL, it cannot be set differently depending on
828     *         things like the contents of the database, or whether the user is logged in.
829     *
830     *  If the wiki does not allow anonymous users to read it, the mode set here
831     *  will be ignored, and private caching headers will always be sent. In other words,
832     *  the "public" mode is equivalent to saying that the data sent is as public as a page
833     *  view.
834     *
835     *  For user-dependent data, the private mode should generally be used. The
836     *  anon-public-user-private mode should only be used where there is a particularly
837     *  good performance reason for caching the anonymous response, but where the
838     *  response to logged-in users may differ, or may contain private data.
839     *
840     *  If this function is never called, then the default will be the private mode.
841     */
842    public function setCacheMode( $mode ) {
843        if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
844            wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"" );
845
846            // Ignore for forwards-compatibility
847            return;
848        }
849
850        if ( !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) ) {
851            // Private wiki, only private headers
852            if ( $mode !== 'private' ) {
853                wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki" );
854
855                return;
856            }
857        }
858
859        if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
860            // User language is used for i18n, so we don't want to publicly
861            // cache. Anons are ok, because if they have non-default language
862            // then there's an appropriate Vary header set by whatever set
863            // their non-default language.
864            wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
865                "'anon-public-user-private' due to uselang=user" );
866            $mode = 'anon-public-user-private';
867        }
868
869        wfDebug( __METHOD__ . ": setting cache mode $mode" );
870        $this->mCacheMode = $mode;
871    }
872
873    public function getCacheMode() {
874        return $this->mCacheMode;
875    }
876
877    /**
878     * Set directives (key/value pairs) for the Cache-Control header.
879     * Boolean values will be formatted as such, by including or omitting
880     * without an equals sign.
881     *
882     * Cache control values set here will only be used if the cache mode is not
883     * private, see setCacheMode().
884     *
885     * @param array $directives
886     */
887    public function setCacheControl( $directives ) {
888        $this->mCacheControl = $directives + $this->mCacheControl;
889    }
890
891    /**
892     * Create an instance of an output formatter by its name
893     *
894     * @param string $format
895     *
896     * @return ApiFormatBase
897     */
898    public function createPrinterByName( $format ) {
899        $printer = $this->mModuleMgr->getModule( $format, 'format', /* $ignoreCache */ true );
900        if ( $printer === null ) {
901            $this->dieWithError(
902                [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
903            );
904        }
905
906        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
907        return $printer;
908    }
909
910    /**
911     * Execute api request. Any errors will be handled if the API was called by the remote client.
912     */
913    public function execute() {
914        if ( $this->mInternalMode ) {
915            $this->executeAction();
916        } else {
917            $this->executeActionWithErrorHandling();
918        }
919    }
920
921    /**
922     * Execute an action, and in case of an error, erase whatever partial results
923     * have been accumulated, and replace it with an error message and a help screen.
924     */
925    protected function executeActionWithErrorHandling() {
926        // Verify the CORS header before executing the action
927        if ( !$this->handleCORS() ) {
928            // handleCORS() has sent a 403, abort
929            return;
930        }
931
932        // Exit here if the request method was OPTIONS
933        // (assume there will be a followup GET or POST)
934        if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
935            return;
936        }
937
938        // In case an error occurs during data output,
939        // clear the output buffer and print just the error information
940        $obLevel = ob_get_level();
941        ob_start();
942
943        $t = microtime( true );
944        $isError = false;
945        try {
946            $this->executeAction();
947            $runTime = microtime( true ) - $t;
948            $this->logRequest( $runTime );
949
950            $this->statsFactory->getTiming( 'api_executeTiming_seconds' )
951                ->setLabel( 'module', $this->mModule->getModuleName() )
952                ->copyToStatsdAt( 'api.' . $this->mModule->getModuleName() . '.executeTiming' )
953                ->observe( 1000 * $runTime );
954        } catch