Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.23% covered (warning)
84.23%
753 / 894
55.36% covered (warning)
55.36%
31 / 56
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiMain
84.23% covered (warning)
84.23%
753 / 894
55.36% covered (warning)
55.36%
31 / 56
537.43
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
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.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
46.43% covered (danger)
46.43%
26 / 56
0.00% covered (danger)
0.00%
0 / 1
38.98
 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%
15 / 15
100.00% covered (success)
100.00%
1 / 1
9
 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
64.71% covered (warning)
64.71%
11 / 17
0.00% covered (danger)
0.00%
0 / 1
14.40
 executeAction
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
6
 setRequestExpectations
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
4.07
 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%
50 / 50
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
2
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
24use MediaWiki\Api\Validator\ApiParamValidator;
25use MediaWiki\Context\DerivativeContext;
26use MediaWiki\Context\IContextSource;
27use MediaWiki\Context\RequestContext;
28use MediaWiki\Debug\MWDebug;
29use MediaWiki\Html\Html;
30use MediaWiki\Logger\LoggerFactory;
31use MediaWiki\MainConfigNames;
32use MediaWiki\MediaWikiServices;
33use MediaWiki\Message\Message;
34use MediaWiki\ParamValidator\TypeDef\UserDef;
35use MediaWiki\Profiler\ProfilingContext;
36use MediaWiki\Request\FauxRequest;
37use MediaWiki\Request\WebRequest;
38use MediaWiki\Request\WebRequestUpload;
39use MediaWiki\Rest\HeaderParser\Origin;
40use MediaWiki\Session\SessionManager;
41use MediaWiki\StubObject\StubGlobalUser;
42use MediaWiki\User\UserRigorOptions;
43use MediaWiki\Utils\MWTimestamp;
44use MediaWiki\WikiMap\WikiMap;
45use Wikimedia\AtEase\AtEase;
46use Wikimedia\ParamValidator\ParamValidator;
47use Wikimedia\ParamValidator\TypeDef\IntegerDef;
48use Wikimedia\Stats\StatsFactory;
49use Wikimedia\Timestamp\TimestampException;
50
51/**
52 * This is the main API class, used for both external and internal processing.
53 * When executed, it will create the requested formatter object,
54 * instantiate and execute an object associated with the needed action,
55 * and use formatter to print results.
56 * In case of an exception, an error message will be printed using the same formatter.
57 *
58 * To use API from another application, run it using MediaWiki\Request\FauxRequest object, in which
59 * case any internal exceptions will not be handled but passed up to the caller.
60 * After successful execution, use getResult() for the resulting data.
61 *
62 * @newable
63 * @note marked as newable in 1.35 for lack of a better alternative,
64 *       but should use a factory in the future.
65 * @ingroup API
66 */
67class ApiMain extends ApiBase {
68    /**
69     * When no format parameter is given, this format will be used
70     */
71    private const API_DEFAULT_FORMAT = 'jsonfm';
72
73    /**
74     * When no uselang parameter is given, this language will be used
75     */
76    private const API_DEFAULT_USELANG = 'user';
77
78    /**
79     * List of available modules: action name => module class
80     */
81    private const MODULES = [
82        'login' => [
83            'class' => ApiLogin::class,
84            'services' => [
85                'AuthManager',
86            ],
87        ],
88        'clientlogin' => [
89            'class' => ApiClientLogin::class,
90            'services' => [
91                'AuthManager',
92            ],
93        ],
94        'logout' => [
95            'class' => ApiLogout::class,
96        ],
97        'createaccount' => [
98            'class' => ApiAMCreateAccount::class,
99            'services' => [
100                'AuthManager',
101            ],
102        ],
103        'linkaccount' => [
104            'class' => ApiLinkAccount::class,
105            'services' => [
106                'AuthManager',
107            ],
108        ],
109        'unlinkaccount' => [
110            'class' => ApiRemoveAuthenticationData::class,
111            'services' => [
112                'AuthManager',
113            ],
114        ],
115        'changeauthenticationdata' => [
116            'class' => ApiChangeAuthenticationData::class,
117            'services' => [
118                'AuthManager',
119            ],
120        ],
121        'removeauthenticationdata' => [
122            'class' => ApiRemoveAuthenticationData::class,
123            'services' => [
124                'AuthManager',
125            ],
126        ],
127        'resetpassword' => [
128            'class' => ApiResetPassword::class,
129            'services' => [
130                'PasswordReset',
131            ]
132        ],
133        'query' => [
134            'class' => ApiQuery::class,
135            'services' => [
136                'ObjectFactory',
137                'WikiExporterFactory',
138                'TitleFormatter',
139                'TitleFactory',
140            ]
141        ],
142        'expandtemplates' => [
143            'class' => ApiExpandTemplates::class,
144            'services' => [
145                'RevisionStore',
146                'ParserFactory',
147            ]
148        ],
149        'parse' => [
150            'class' => ApiParse::class,
151            'services' => [
152                'RevisionLookup',
153                'SkinFactory',
154                'LanguageNameUtils',
155                'LinkBatchFactory',
156                'LinkCache',
157                'ContentHandlerFactory',
158                'ParserFactory',
159                'WikiPageFactory',
160                'ContentRenderer',
161                'ContentTransformer',
162                'CommentFormatter',
163                'TempUserCreator',
164                'UserFactory',
165                'UrlUtils',
166                'TitleFormatter',
167            ]
168        ],
169        'stashedit' => [
170            'class' => ApiStashEdit::class,
171            'services' => [
172                'ContentHandlerFactory',
173                'PageEditStash',
174                'RevisionLookup',
175                'StatsdDataFactory',
176                'WikiPageFactory',
177                'TempUserCreator',
178                'UserFactory',
179            ]
180        ],
181        'opensearch' => [
182            'class' => ApiOpenSearch::class,
183            'services' => [
184                'LinkBatchFactory',
185                'SearchEngineConfig',
186                'SearchEngineFactory',
187                'UrlUtils',
188            ]
189        ],
190        'feedcontributions' => [
191            'class' => ApiFeedContributions::class,
192            'services' => [
193                'RevisionStore',
194                'LinkRenderer',
195                'LinkBatchFactory',
196                'HookContainer',
197                'DBLoadBalancerFactory',
198                'NamespaceInfo',
199                'UserFactory',
200                'CommentFormatter',
201            ]
202        ],
203        'feedrecentchanges' => [
204            'class' => ApiFeedRecentChanges::class,
205            'services' => [
206                'SpecialPageFactory',
207                'TempUserConfig',
208            ]
209        ],
210        'feedwatchlist' => [
211            'class' => ApiFeedWatchlist::class,
212            'services' => [
213                'ParserFactory',
214            ]
215        ],
216        'help' => [
217            'class' => ApiHelp::class,
218            'services' => [
219                'SkinFactory',
220            ]
221        ],
222        'paraminfo' => [
223            'class' => ApiParamInfo::class,
224            'services' => [
225                'UserFactory',
226            ],
227        ],
228        'rsd' => [
229            'class' => ApiRsd::class,
230        ],
231        'compare' => [
232            'class' => ApiComparePages::class,
233            'services' => [
234                'RevisionStore',
235                'ArchivedRevisionLookup',
236                'SlotRoleRegistry',
237                'ContentHandlerFactory',
238                'ContentTransformer',
239                'CommentFormatter',
240                'TempUserCreator',
241                'UserFactory',
242            ]
243        ],
244        'checktoken' => [
245            'class' => ApiCheckToken::class,
246        ],
247        'cspreport' => [
248            'class' => ApiCSPReport::class,
249        ],
250        'validatepassword' => [
251            'class' => ApiValidatePassword::class,
252            'services' => [
253                'AuthManager',
254                'UserFactory',
255            ]
256        ],
257
258        // Write modules
259        'purge' => [
260            'class' => ApiPurge::class,
261            'services' => [
262                'WikiPageFactory',
263                'TitleFormatter',
264            ],
265        ],
266        'setnotificationtimestamp' => [
267            'class' => ApiSetNotificationTimestamp::class,
268            'services' => [
269                'DBLoadBalancerFactory',
270                'RevisionStore',
271                'WatchedItemStore',
272                'TitleFormatter',
273                'TitleFactory',
274            ]
275        ],
276        'rollback' => [
277            'class' => ApiRollback::class,
278            'services' => [
279                'RollbackPageFactory',
280                'WatchlistManager',
281                'UserOptionsLookup',
282            ]
283        ],
284        'delete' => [
285            'class' => ApiDelete::class,
286            'services' => [
287                'RepoGroup',
288                'WatchlistManager',
289                'UserOptionsLookup',
290                'DeletePageFactory',
291            ]
292        ],
293        'undelete' => [
294            'class' => ApiUndelete::class,
295            'services' => [
296                'WatchlistManager',
297                'UserOptionsLookup',
298                'UndeletePageFactory',
299                'WikiPageFactory',
300            ]
301        ],
302        'protect' => [
303            'class' => ApiProtect::class,
304            'services' => [
305                'WatchlistManager',
306                'UserOptionsLookup',
307                'RestrictionStore',
308            ]
309        ],
310        'block' => [
311            'class' => ApiBlock::class,
312            'services' => [
313                'BlockPermissionCheckerFactory',
314                'BlockUserFactory',
315                'TitleFactory',
316                'UserIdentityLookup',
317                'WatchedItemStore',
318                'BlockUtils',
319                'BlockActionInfo',
320                'WatchlistManager',
321                'UserOptionsLookup',
322            ]
323        ],
324        'unblock' => [
325            'class' => ApiUnblock::class,
326            'services' => [
327                'BlockPermissionCheckerFactory',
328                'UnblockUserFactory',
329                'UserIdentityLookup',
330                'WatchedItemStore',
331                'WatchlistManager',
332                'UserOptionsLookup',
333            ]
334        ],
335        'move' => [
336            'class' => ApiMove::class,
337            'services' => [
338                'MovePageFactory',
339                'RepoGroup',
340                'WatchlistManager',
341                'UserOptionsLookup',
342            ]
343        ],
344        'edit' => [
345            'class' => ApiEditPage::class,
346            'services' => [
347                'ContentHandlerFactory',
348                'RevisionLookup',
349                'WatchedItemStore',
350                'WikiPageFactory',
351                'WatchlistManager',
352                'UserOptionsLookup',
353                'RedirectLookup',
354                'TempUserCreator',
355                'UserFactory',
356            ]
357        ],
358        'upload' => [
359            'class' => ApiUpload::class,
360            'services' => [
361                'JobQueueGroup',
362                'WatchlistManager',
363                'UserOptionsLookup',
364            ]
365        ],
366        'filerevert' => [
367            'class' => ApiFileRevert::class,
368            'services' => [
369                'RepoGroup',
370            ]
371        ],
372        'emailuser' => [
373            'class' => ApiEmailUser::class,
374            'services' => [
375                'EmailUserFactory',
376                'UserFactory',
377            ]
378        ],
379        'watch' => [
380            'class' => ApiWatch::class,
381            'services' => [
382                'WatchlistManager',
383                'TitleFormatter',
384            ]
385        ],
386        'patrol' => [
387            'class' => ApiPatrol::class,
388            'services' => [
389                'RevisionStore',
390            ]
391        ],
392        'import' => [
393            'class' => ApiImport::class,
394            'services' => [
395                'WikiImporterFactory',
396            ]
397        ],
398        'clearhasmsg' => [
399            'class' => ApiClearHasMsg::class,
400            'services' => [
401                'TalkPageNotificationManager',
402            ]
403        ],
404        'userrights' => [
405            'class' => ApiUserrights::class,
406            'services' => [
407                'UserGroupManager',
408                'WatchedItemStore',
409                'WatchlistManager',
410                'UserOptionsLookup',
411            ]
412        ],
413        'options' => [
414            'class' => ApiOptions::class,
415            'services' => [
416                'UserOptionsManager',
417                'PreferencesFactory',
418            ],
419        ],
420        'imagerotate' => [
421            'class' => ApiImageRotate::class,
422            'services' => [
423                'RepoGroup',
424                'TempFSFileFactory',
425                'TitleFactory',
426            ]
427        ],
428        'revisiondelete' => [
429            'class' => ApiRevisionDelete::class,
430        ],
431        'managetags' => [
432            'class' => ApiManageTags::class,
433        ],
434        'tag' => [
435            'class' => ApiTag::class,
436            'services' => [
437                'DBLoadBalancerFactory',
438                'RevisionStore',
439                'ChangeTagsStore',
440            ]
441        ],
442        'mergehistory' => [
443            'class' => ApiMergeHistory::class,
444            'services' => [
445                'MergeHistoryFactory',
446            ],
447        ],
448        'setpagelanguage' => [
449            'class' => ApiSetPageLanguage::class,
450            'services' => [
451                'DBLoadBalancerFactory',
452                'LanguageNameUtils',
453            ]
454        ],
455        'changecontentmodel' => [
456            'class' => ApiChangeContentModel::class,
457            'services' => [
458                'ContentHandlerFactory',
459                'ContentModelChangeFactory',
460            ]
461        ],
462        'acquiretempusername' => [
463            'class' => ApiAcquireTempUserName::class,
464            'services' => [
465                'TempUserCreator',
466            ]
467        ],
468    ];
469
470    /**
471     * List of available formats: format name => format class
472     */
473    private const FORMATS = [
474        'json' => [
475            'class' => ApiFormatJson::class,
476        ],
477        'jsonfm' => [
478            'class' => ApiFormatJson::class,
479        ],
480        'php' => [
481            'class' => ApiFormatPhp::class,
482        ],
483        'phpfm' => [
484            'class' => ApiFormatPhp::class,
485        ],
486        'xml' => [
487            'class' => ApiFormatXml::class,
488        ],
489        'xmlfm' => [
490            'class' => ApiFormatXml::class,
491        ],
492        'rawfm' => [
493            'class' => ApiFormatJson::class,
494        ],
495        'none' => [
496            'class' => ApiFormatNone::class,
497        ],
498    ];
499
500    /**
501     * List of user roles that are specifically relevant to the API.
502     * [ 'right' => [ 'msg'    => 'Some message with a $1',
503     *                'params' => [ $someVarToSubst ] ],
504     * ];
505     */
506    private const RIGHTS_MAP = [
507        'writeapi' => [
508            'msg' => 'right-writeapi',
509            'params' => []
510        ],
511        'apihighlimits' => [
512            'msg' => 'api-help-right-apihighlimits',
513            'params' => [ ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ]
514        ]
515    ];
516
517    /** @var ApiFormatBase|null */
518    private $mPrinter;
519
520    /** @var ApiModuleManager */
521    private $mModuleMgr;
522
523    /** @var ApiResult */
524    private $mResult;
525
526    /** @var ApiErrorFormatter */
527    private $mErrorFormatter;
528
529    /** @var ApiParamValidator */
530    private $mParamValidator;
531
532    /** @var ApiContinuationManager|null */
533    private $mContinuationManager;
534
535    /** @var string|null */
536    private $mAction;
537
538    /** @var bool */
539    private $mEnableWrite;
540
541    /** @var bool */
542    private $mInternalMode;
543
544    /** @var ApiBase */
545    private $mModule;
546
547    /** @var string */
548    private $mCacheMode = 'private';
549
550    /** @var array */
551    private $mCacheControl = [];
552
553    /** @var array */
554    private $mParamsUsed = [];
555
556    /** @var array */
557    private $mParamsSensitive = [];
558
559    /** @var bool|null Cached return value from self::lacksSameOriginSecurity() */
560    private $lacksSameOriginSecurity = null;
561
562    /** @var StatsFactory */
563    private $statsFactory;
564
565    /**
566     * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
567     *
568     * @stable to call
569     * @param IContextSource|WebRequest|null $context If this is an instance of
570     *    MediaWiki\Request\FauxRequest, errors are thrown and no printing occurs
571     * @param bool $enableWrite Should be set to true if the api may modify data
572     * @param bool|null $internal Whether the API request is an internal faux
573     *        request. If null or not given, the request is assumed to be internal
574     *        if $context contains a FauxRequest.
575     */
576    public function __construct( $context = null, $enableWrite = false, $internal = null ) {
577        if ( $context === null ) {
578            $context = RequestContext::getMain();
579        } elseif ( $context instanceof WebRequest ) {
580            // BC for pre-1.19
581            $request = $context;
582            $context = RequestContext::getMain();
583        }
584        // We set a derivative context so we can change stuff later
585        $derivativeContext = new DerivativeContext( $context );
586        $this->setContext( $derivativeContext );
587
588        if ( isset( $request ) ) {
589            $derivativeContext->setRequest( $request );
590        } else {
591            $request = $this->getRequest();
592        }
593
594        $this->mInternalMode = $internal ?? ( $request instanceof FauxRequest );
595
596        // Special handling for the main module: $parent === $this
597        parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
598
599        $config = $this->getConfig();
600        // TODO inject stuff, see T265644
601        $services = MediaWikiServices::getInstance();
602
603        if ( !$this->mInternalMode ) {
604            // If we're in a mode that breaks the same-origin policy, strip
605            // user credentials for security.
606            if ( $this->lacksSameOriginSecurity() ) {
607                wfDebug( "API: stripping user credentials when the same-origin policy is not applied" );
608                $user = $services->getUserFactory()->newAnonymous();
609                StubGlobalUser::setUser( $user );
610                $derivativeContext->setUser( $user );
611                $request->response()->header( 'MediaWiki-Login-Suppressed: true' );
612            }
613        }
614
615        $this->mParamValidator = new ApiParamValidator(
616            $this,
617            $services->getObjectFactory()
618        );
619
620        $this->statsFactory = $services->getStatsFactory();
621
622        $this->mResult =
623            new ApiResult( $this->getConfig()->get( MainConfigNames::APIMaxResultSize ) );
624
625        // Setup uselang. This doesn't use $this->getParameter()
626        // because we're not ready to handle errors yet.
627        // Optimisation: Avoid slow getVal(), this isn't user-generated content.
628        $uselang = $request->getRawVal( 'uselang', self::API_DEFAULT_USELANG );
629        if ( $uselang === 'user' ) {
630            // Assume the parent context is going to return the user language
631            // for uselang=user (see T85635).
632        } else {
633            if ( $uselang === 'content' ) {
634                $uselang = $services->getContentLanguage()->getCode();
635            }
636            $code = RequestContext::sanitizeLangCode( $uselang );
637            $derivativeContext->setLanguage( $code );
638            if ( !$this->mInternalMode ) {
639                // phpcs:disable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
640                global $wgLang;
641                $wgLang = $derivativeContext->getLanguage();
642                RequestContext::getMain()->setLanguage( $wgLang );
643                // phpcs:enable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
644            }
645        }
646
647        // Set up the error formatter. This doesn't use $this->getParameter()
648        // because we're not ready to handle errors yet.
649        // Optimisation: Avoid slow getVal(), this isn't user-generated content.
650        $errorFormat = $request->getRawVal( 'errorformat', 'bc' );
651        $errorLangCode = $request->getRawVal( 'errorlang', 'uselang' );
652        $errorsUseDB = $request->getCheck( 'errorsuselocal' );
653        if ( in_array( $errorFormat, [ 'plaintext', 'wikitext', 'html', 'raw', 'none' ], true ) ) {
654            if ( $errorLangCode === 'uselang' ) {
655                $errorLang = $this->getLanguage();
656            } elseif ( $errorLangCode === 'content' ) {
657                $errorLang = $services->getContentLanguage();
658            } else {
659                $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode );
660                $errorLang = $services->getLanguageFactory()->getLanguage( $errorLangCode );
661            }
662            $this->mErrorFormatter = new ApiErrorFormatter(
663                $this->mResult,
664                $errorLang,
665                $errorFormat,
666                $errorsUseDB
667            );
668        } else {
669            $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
670        }
671        $this->mResult->setErrorFormatter( $this->getErrorFormatter() );
672
673        $this->mModuleMgr = new ApiModuleManager(
674            $this,
675            $services->getObjectFactory()
676        );
677        $this->mModuleMgr->addModules( self::MODULES, 'action' );
678        $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIModules ), 'action' );
679        $this->mModuleMgr->addModules( self::FORMATS, 'format' );
680        $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIFormatModules ), 'format' );
681
682        $this->getHookRunner()->onApiMain__moduleManager( $this->mModuleMgr );
683
684        $this->mContinuationManager = null;
685        $this->mEnableWrite = $enableWrite;
686    }
687
688    /**
689     * Return true if the API was started by other PHP code using MediaWiki\Request\FauxRequest
690     * @return bool
691     */
692    public function isInternalMode() {
693        return $this->mInternalMode;
694    }
695
696    /**
697     * Get the ApiResult object associated with current request
698     *
699     * @return ApiResult
700     */
701    public function getResult() {
702        return $this->mResult;
703    }
704
705    /**
706     * Get the security flag for the current request
707     * @return bool
708     */
709    public function lacksSameOriginSecurity() {
710        if ( $this->lacksSameOriginSecurity !== null ) {
711            return $this->lacksSameOriginSecurity;
712        }
713
714        $request = $this->getRequest();
715
716        // JSONP mode
717        if ( $request->getCheck( 'callback' ) ||
718            // Anonymous CORS
719            $request->getVal( 'origin' ) === '*' ||
720            // Header to be used from XMLHTTPRequest when the request might
721            // otherwise be used for XSS.
722            $request->getHeader( 'Treat-as-Untrusted' ) !== false
723        ) {
724            $this->lacksSameOriginSecurity = true;
725            return true;
726        }
727
728        // Allow extensions to override.
729        $this->lacksSameOriginSecurity = !$this->getHookRunner()
730            ->onRequestHasSameOriginSecurity( $request );
731        return $this->lacksSameOriginSecurity;
732    }
733
734    /**
735     * Get the ApiErrorFormatter object associated with current request
736     * @return ApiErrorFormatter
737     */
738    public function getErrorFormatter() {
739        return $this->mErrorFormatter;
740    }
741
742    /**
743     * @return ApiContinuationManager|null
744     */
745    public function getContinuationManager() {
746        return $this->mContinuationManager;
747    }
748
749    /**
750     * @param ApiContinuationManager|null $manager
751     */
752    public function setContinuationManager( ApiContinuationManager $manager = null ) {
753        if ( $manager !== null && $this->mContinuationManager !== null ) {
754            throw new UnexpectedValueException(
755                __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
756                ' when a manager is already set from ' . $this->mContinuationManager->getSource()
757            );
758        }
759        $this->mContinuationManager = $manager;
760    }
761
762    /**
763     * Get the parameter validator
764     * @return ApiParamValidator
765     */
766    public function getParamValidator(): ApiParamValidator {
767        return $this->mParamValidator;
768    }
769
770    /**
771     * Get the API module object. Only works after executeAction()
772     *
773     * @return ApiBase
774     */
775    public function getModule() {
776        return $this->mModule;
777    }
778
779    /**
780     * Get the result formatter object. Only works after setupExecuteAction()
781     *
782     * @return ApiFormatBase
783     */
784    public function getPrinter() {
785        return $this->mPrinter;
786    }
787
788    /**
789     * Set how long the response should be cached.
790     *
791     * @param int $maxage
792     */
793    public function setCacheMaxAge( $maxage ) {
794        $this->setCacheControl( [
795            'max-age' => $maxage,
796            's-maxage' => $maxage
797        ] );
798    }
799
800    /**
801     * Set the type of caching headers which will be sent.
802     *
803     * @param string $mode One of:
804     *    - 'public':     Cache this object in public caches, if the maxage or smaxage
805     *         parameter is set, or if setCacheMaxAge() was called. If a maximum age is
806     *         not provided by any of these means, the object will be private.
807     *    - 'private':    Cache this object only in private client-side caches.
808     *    - 'anon-public-user-private': Make this object cacheable for logged-out
809     *         users, but private for logged-in users. IMPORTANT: If this is set, it must be
810     *         set consistently for a given URL, it cannot be set differently depending on
811     *         things like the contents of the database, or whether the user is logged in.
812     *
813     *  If the wiki does not allow anonymous users to read it, the mode set here
814     *  will be ignored, and private caching headers will always be sent. In other words,
815     *  the "public" mode is equivalent to saying that the data sent is as public as a page
816     *  view.
817     *
818     *  For user-dependent data, the private mode should generally be used. The
819     *  anon-public-user-private mode should only be used where there is a particularly
820     *  good performance reason for caching the anonymous response, but where the
821     *  response to logged-in users may differ, or may contain private data.
822     *
823     *  If this function is never called, then the default will be the private mode.
824     */
825    public function setCacheMode( $mode ) {
826        if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
827            wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"" );
828
829            // Ignore for forwards-compatibility
830            return;
831        }
832
833        if ( !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) ) {
834            // Private wiki, only private headers
835            if ( $mode !== 'private' ) {
836                wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki" );
837
838                return;
839            }
840        }
841
842        if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
843            // User language is used for i18n, so we don't want to publicly
844            // cache. Anons are ok, because if they have non-default language
845            // then there's an appropriate Vary header set by whatever set
846            // their non-default language.
847            wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
848                "'anon-public-user-private' due to uselang=user" );
849            $mode = 'anon-public-user-private';
850        }
851
852        wfDebug( __METHOD__ . ": setting cache mode $mode" );
853        $this->mCacheMode = $mode;
854    }
855
856    public function getCacheMode() {
857        return $this->mCacheMode;
858    }
859
860    /**
861     * Set directives (key/value pairs) for the Cache-Control header.
862     * Boolean values will be formatted as such, by including or omitting
863     * without an equals sign.
864     *
865     * Cache control values set here will only be used if the cache mode is not
866     * private, see setCacheMode().
867     *
868     * @param array $directives
869     */
870    public function setCacheControl( $directives ) {
871        $this->mCacheControl = $directives + $this->mCacheControl;
872    }
873
874    /**
875     * Create an instance of an output formatter by its name
876     *
877     * @param string $format
878     *
879     * @return ApiFormatBase
880     */
881    public function createPrinterByName( $format ) {
882        $printer = $this->mModuleMgr->getModule( $format, 'format', /* $ignoreCache */ true );
883        if ( $printer === null ) {
884            $this->dieWithError(
885                [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
886            );
887        }
888
889        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
890        return $printer;
891    }
892
893    /**
894     * Execute api request. Any errors will be handled if the API was called by the remote client.
895     */
896    public function execute() {
897        if ( $this->mInternalMode ) {
898            $this->executeAction();
899        } else {
900            $this->executeActionWithErrorHandling();
901        }
902    }
903
904    /**
905     * Execute an action, and in case of an error, erase whatever partial results
906     * have been accumulated, and replace it with an error message and a help screen.
907     */
908    protected function executeActionWithErrorHandling() {
909        // Verify the CORS header before executing the action
910        if ( !$this->handleCORS() ) {
911            // handleCORS() has sent a 403, abort
912            return;
913        }
914
915        // Exit here if the request method was OPTIONS
916        // (assume there will be a followup GET or POST)
917        if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
918            return;
919        }
920
921        // In case an error occurs during data output,
922        // clear the output buffer and print just the error information
923        $obLevel = ob_get_level();
924        ob_start();
925
926        $t = microtime( true );
927        $isError = false;
928        try {
929            $this->executeAction();
930            $runTime = microtime( true ) - $t;
931            $this->logRequest( $runTime );
932
933            $this->statsFactory->getTiming( 'api_executeTiming_seconds' )
934                ->setLabel( 'module', $this->mModule->getModuleName() )
935                ->copyToStatsdAt( 'api.' . $this->mModule->getModuleName() . '.executeTiming' )
936                ->observe( 1000 * $runTime );
937        } catch ( Throwable $e ) {
938            $this->handleException(