Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
82.19% |
757 / 921 |
|
55.36% |
31 / 56 |
CRAP | |
0.00% |
0 / 1 |
ApiMain | |
82.28% |
757 / 920 |
|
55.36% |
31 / 56 |
703.74 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
66 / 66 |
|
100.00% |
1 / 1 |
13 | |||
isInternalMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getResult | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
lacksSameOriginSecurity | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
7.02 | |||
getErrorFormatter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContinuationManager | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setContinuationManager | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getParamValidator | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getModule | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPrinter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setCacheMaxAge | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
setCacheMode | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
6 | |||
getCacheMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCacheControl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
createPrinterByName | |
50.00% |
3 / 6 |
|
0.00% |
0 / 1 |
2.50 | |||
execute | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
executeActionWithErrorHandling | |
92.86% |
26 / 28 |
|
0.00% |
0 / 1 |
8.02 | |||
handleException | |
76.67% |
23 / 30 |
|
0.00% |
0 / 1 |
8.81 | |||
handleApiBeforeMainException | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
handleCORS | |
40.54% |
30 / 74 |
|
0.00% |
0 / 1 |
123.74 | |||
matchRequestedHeaders | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
4.00 | |||
sendCacheHeaders | |
65.38% |
34 / 52 |
|
0.00% |
0 / 1 |
47.89 | |||
createErrorPrinter | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
4.37 | |||
errorMessagesFromException | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
7.07 | |||
substituteResultWithError | |
100.00% |
53 / 53 |
|
100.00% |
1 / 1 |
12 | |||
addRequestedFields | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
7 | |||
setupExecuteAction | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
setupModule | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
7 | |||
getMaxLag | |
57.14% |
12 / 21 |
|
0.00% |
0 / 1 |
3.71 | |||
checkMaxLag | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
5 | |||
checkConditionalRequestHeaders | |
100.00% |
53 / 53 |
|
100.00% |
1 / 1 |
21 | |||
checkExecutePermissions | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
8 | |||
checkReadOnly | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
5.93 | |||
checkBotReadOnly | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
20 | |||
checkAsserts | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
11 | |||
setupExternalResponse | |
44.00% |
11 / 25 |
|
0.00% |
0 / 1 |
37.29 | |||
executeAction | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
6 | |||
setRequestExpectations | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
4.10 | |||
logRequest | |
88.68% |
47 / 53 |
|
0.00% |
0 / 1 |
10.15 | |||
encodeRequestLogValue | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getParamsUsed | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
markParamsUsed | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSensitiveParams | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
markParamsSensitive | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getVal | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getCheck | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getUpload | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
reportUnusedParams | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
printResult | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
3.14 | |||
isReadMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAllowedParams | |
100.00% |
51 / 51 |
|
100.00% |
1 / 1 |
1 | |||
getExamplesMessages | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
modifyHelp | |
100.00% |
97 / 97 |
|
100.00% |
1 / 1 |
12 | |||
canApiHighLimits | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getModuleManager | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUserAgent | |
0.00% |
0 / 4 |
|
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 | |
24 | namespace MediaWiki\Api; |
25 | |
26 | use ILocalizedException; |
27 | use LogicException; |
28 | use MediaWiki; |
29 | use MediaWiki\Api\Validator\ApiParamValidator; |
30 | use MediaWiki\Context\DerivativeContext; |
31 | use MediaWiki\Context\IContextSource; |
32 | use MediaWiki\Context\RequestContext; |
33 | use MediaWiki\Debug\MWDebug; |
34 | use MediaWiki\Html\Html; |
35 | use MediaWiki\Logger\LoggerFactory; |
36 | use MediaWiki\MainConfigNames; |
37 | use MediaWiki\MediaWikiServices; |
38 | use MediaWiki\Message\Message; |
39 | use MediaWiki\ParamValidator\TypeDef\UserDef; |
40 | use MediaWiki\Profiler\ProfilingContext; |
41 | use MediaWiki\Request\FauxRequest; |
42 | use MediaWiki\Request\WebRequest; |
43 | use MediaWiki\Request\WebRequestUpload; |
44 | use MediaWiki\Rest\HeaderParser\Origin; |
45 | use MediaWiki\Session\SessionManager; |
46 | use MediaWiki\StubObject\StubGlobalUser; |
47 | use MediaWiki\User\UserRigorOptions; |
48 | use MediaWiki\Utils\MWTimestamp; |
49 | use MediaWiki\WikiMap\WikiMap; |
50 | use MWExceptionHandler; |
51 | use MWExceptionRenderer; |
52 | use Profiler; |
53 | use Throwable; |
54 | use UnexpectedValueException; |
55 | use Wikimedia\AtEase\AtEase; |
56 | use Wikimedia\Message\MessageSpecifier; |
57 | use Wikimedia\ParamValidator\ParamValidator; |
58 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
59 | use Wikimedia\Stats\StatsFactory; |
60 | use 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 | */ |
78 | class 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 |