Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / api / ApiMain.php
blob2476043b25280d1bb6c0bdf891ddfe8b707d9dda
1 <?php
2 /**
3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
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.
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.
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
20 * @file
21 * @defgroup API API
24 namespace MediaWiki\Api;
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;
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.
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.
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
78 class ApiMain extends ApiBase {
79 /**
80 * When no format parameter is given, this format will be used
82 private const API_DEFAULT_FORMAT = 'jsonfm';
84 /**
85 * When no uselang parameter is given, this language will be used
87 private const API_DEFAULT_USELANG = 'user';
89 /**
90 * List of available modules: action name => module class
92 private const MODULES = [
93 'login' => [
94 'class' => ApiLogin::class,
95 'services' => [
96 'AuthManager',
97 'UserIdentityUtils'
100 'clientlogin' => [
101 'class' => ApiClientLogin::class,
102 'services' => [
103 'AuthManager',
104 'UrlUtils',
107 'logout' => [
108 'class' => ApiLogout::class,
110 'createaccount' => [
111 'class' => ApiAMCreateAccount::class,
112 'services' => [
113 'AuthManager',
114 'UrlUtils',
117 'linkaccount' => [
118 'class' => ApiLinkAccount::class,
119 'services' => [
120 'AuthManager',
121 'UrlUtils',
124 'unlinkaccount' => [
125 'class' => ApiRemoveAuthenticationData::class,
126 'services' => [
127 'AuthManager',
130 'changeauthenticationdata' => [
131 'class' => ApiChangeAuthenticationData::class,
132 'services' => [
133 'AuthManager',
136 'removeauthenticationdata' => [
137 'class' => ApiRemoveAuthenticationData::class,
138 'services' => [
139 'AuthManager',
142 'resetpassword' => [
143 'class' => ApiResetPassword::class,
144 'services' => [
145 'PasswordReset',
148 'query' => [
149 'class' => ApiQuery::class,
150 'services' => [
151 'ObjectFactory',
152 'WikiExporterFactory',
153 'TitleFormatter',
154 'TitleFactory',
157 'expandtemplates' => [
158 'class' => ApiExpandTemplates::class,
159 'services' => [
160 'RevisionStore',
161 'ParserFactory',
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',
184 'stashedit' => [
185 'class' => ApiStashEdit::class,
186 'services' => [
187 'ContentHandlerFactory',
188 'PageEditStash',
189 'RevisionLookup',
190 'StatsFactory',
191 'WikiPageFactory',
192 'TempUserCreator',
193 'UserFactory',
196 'opensearch' => [
197 'class' => ApiOpenSearch::class,
198 'services' => [
199 'LinkBatchFactory',
200 'SearchEngineConfig',
201 'SearchEngineFactory',
202 'UrlUtils',
205 'feedcontributions' => [
206 'class' => ApiFeedContributions::class,
207 'services' => [
208 'RevisionStore',
209 'LinkRenderer',
210 'LinkBatchFactory',
211 'HookContainer',
212 'DBLoadBalancerFactory',
213 'NamespaceInfo',
214 'UserFactory',
215 'CommentFormatter',
218 'feedrecentchanges' => [
219 'class' => ApiFeedRecentChanges::class,
220 'services' => [
221 'SpecialPageFactory',
222 'TempUserConfig',
225 'feedwatchlist' => [
226 'class' => ApiFeedWatchlist::class,
227 'services' => [
228 'ParserFactory',
231 'help' => [
232 'class' => ApiHelp::class,
233 'services' => [
234 'SkinFactory',
237 'paraminfo' => [
238 'class' => ApiParamInfo::class,
239 'services' => [
240 'UserFactory',
243 'rsd' => [
244 'class' => ApiRsd::class,
246 'compare' => [
247 'class' => ApiComparePages::class,
248 'services' => [
249 'RevisionStore',
250 'ArchivedRevisionLookup',
251 'SlotRoleRegistry',
252 'ContentHandlerFactory',
253 'ContentTransformer',
254 'CommentFormatter',
255 'TempUserCreator',
256 'UserFactory',
259 'checktoken' => [
260 'class' => ApiCheckToken::class,
262 'cspreport' => [
263 'class' => ApiCSPReport::class,
264 'services' => [
265 'UrlUtils',
268 'validatepassword' => [
269 'class' => ApiValidatePassword::class,
270 'services' => [
271 'AuthManager',
272 'UserFactory',
276 // Write modules
277 'purge' => [
278 'class' => ApiPurge::class,
279 'services' => [
280 'WikiPageFactory',
281 'TitleFormatter',
284 'setnotificationtimestamp' => [
285 'class' => ApiSetNotificationTimestamp::class,
286 'services' => [
287 'DBLoadBalancerFactory',
288 'RevisionStore',
289 'WatchedItemStore',
290 'TitleFormatter',
291 'TitleFactory',
294 'rollback' => [
295 'class' => ApiRollback::class,
296 'services' => [
297 'RollbackPageFactory',
298 'WatchlistManager',
299 'UserOptionsLookup',
302 'delete' => [
303 'class' => ApiDelete::class,
304 'services' => [
305 'RepoGroup',
306 'WatchlistManager',
307 'UserOptionsLookup',
308 'DeletePageFactory',
311 'undelete' => [
312 'class' => ApiUndelete::class,
313 'services' => [
314 'WatchlistManager',
315 'UserOptionsLookup',
316 'UndeletePageFactory',
317 'WikiPageFactory',
320 'protect' => [
321 'class' => ApiProtect::class,
322 'services' => [
323 'WatchlistManager',
324 'UserOptionsLookup',
325 'RestrictionStore',
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',
343 'unblock' => [
344 'class' => ApiUnblock::class,
345 'services' => [
346 'BlockPermissionCheckerFactory',
347 'UnblockUserFactory',
348 'UserIdentityLookup',
349 'WatchedItemStore',
350 'WatchlistManager',
351 'UserOptionsLookup',
354 'move' => [
355 'class' => ApiMove::class,
356 'services' => [
357 'MovePageFactory',
358 'RepoGroup',
359 'WatchlistManager',
360 'UserOptionsLookup',
363 'edit' => [
364 'class' => ApiEditPage::class,
365 'services' => [
366 'ContentHandlerFactory',
367 'RevisionLookup',
368 'WatchedItemStore',
369 'WikiPageFactory',
370 'WatchlistManager',
371 'UserOptionsLookup',
372 'RedirectLookup',
373 'TempUserCreator',
374 'UserFactory',
377 'upload' => [
378 'class' => ApiUpload::class,
379 'services' => [
380 'JobQueueGroup',
381 'WatchlistManager',
382 'UserOptionsLookup',
385 'filerevert' => [
386 'class' => ApiFileRevert::class,
387 'services' => [
388 'RepoGroup',
391 'emailuser' => [
392 'class' => ApiEmailUser::class,
393 'services' => [
394 'EmailUserFactory',
395 'UserFactory',
398 'watch' => [
399 'class' => ApiWatch::class,
400 'services' => [
401 'WatchlistManager',
402 'TitleFormatter',
405 'patrol' => [
406 'class' => ApiPatrol::class,
407 'services' => [
408 'RevisionStore',
411 'import' => [
412 'class' => ApiImport::class,
413 'services' => [
414 'WikiImporterFactory',
417 'clearhasmsg' => [
418 'class' => ApiClearHasMsg::class,
419 'services' => [
420 'TalkPageNotificationManager',
423 'userrights' => [
424 'class' => ApiUserrights::class,
425 'services' => [
426 'UserGroupManager',
427 'WatchedItemStore',
428 'WatchlistManager',
429 'UserOptionsLookup',
432 'options' => [
433 'class' => ApiOptions::class,
434 'services' => [
435 'UserOptionsManager',
436 'PreferencesFactory',
439 'imagerotate' => [
440 'class' => ApiImageRotate::class,
441 'services' => [
442 'RepoGroup',
443 'TempFSFileFactory',
444 'TitleFactory',
447 'revisiondelete' => [
448 'class' => ApiRevisionDelete::class,
450 'managetags' => [
451 'class' => ApiManageTags::class,
453 'tag' => [
454 'class' => ApiTag::class,
455 'services' => [
456 'DBLoadBalancerFactory',
457 'RevisionStore',
458 'ChangeTagsStore',
461 'mergehistory' => [
462 'class' => ApiMergeHistory::class,
463 'services' => [
464 'MergeHistoryFactory',
467 'setpagelanguage' => [
468 'class' => ApiSetPageLanguage::class,
469 'services' => [
470 'DBLoadBalancerFactory',
471 'LanguageNameUtils',
474 'changecontentmodel' => [
475 'class' => ApiChangeContentModel::class,
476 'services' => [
477 'ContentHandlerFactory',
478 'ContentModelChangeFactory',
481 'acquiretempusername' => [
482 'class' => ApiAcquireTempUserName::class,
483 'services' => [
484 'TempUserCreator',
490 * List of available formats: format name => format class
492 private const FORMATS = [
493 'json' => [
494 'class' => ApiFormatJson::class,
496 'jsonfm' => [
497 'class' => ApiFormatJson::class,
499 'php' => [
500 'class' => ApiFormatPhp::class,
502 'phpfm' => [
503 'class' => ApiFormatPhp::class,
505 'xml' => [
506 'class' => ApiFormatXml::class,
508 'xmlfm' => [
509 'class' => ApiFormatXml::class,
511 'rawfm' => [
512 'class' => ApiFormatJson::class,
514 'none' => [
515 'class' => ApiFormatNone::class,
520 * List of user roles that are specifically relevant to the API.
521 * [ 'right' => [ 'msg' => 'Some message with a $1',
522 * 'params' => [ $someVarToSubst ] ],
523 * ];
525 private const RIGHTS_MAP = [
526 'apihighlimits' => [
527 'msg' => 'api-help-right-apihighlimits',
528 'params' => [ ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ]
532 /** @var ApiFormatBase|null */
533 private $mPrinter;
535 /** @var ApiModuleManager */
536 private $mModuleMgr;
538 /** @var ApiResult */
539 private $mResult;
541 /** @var ApiErrorFormatter */
542 private $mErrorFormatter;
544 /** @var ApiParamValidator */
545 private $mParamValidator;
547 /** @var ApiContinuationManager|null */
548 private $mContinuationManager;
550 /** @var string|null */
551 private $mAction;
553 /** @var bool */
554 private $mEnableWrite;
556 /** @var bool */
557 private $mInternalMode;
559 /** @var ApiBase */
560 private $mModule;
562 /** @var string */
563 private $mCacheMode = 'private';
565 /** @var array */
566 private $mCacheControl = [];
568 /** @var array */
569 private $mParamsUsed = [];
571 /** @var array */
572 private $mParamsSensitive = [];
574 /** @var bool|null Cached return value from self::lacksSameOriginSecurity() */
575 private $lacksSameOriginSecurity = null;
577 /** @var StatsFactory */
578 private $statsFactory;
581 * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
583 * @stable to call
584 * @param IContextSource|WebRequest|null $context If this is an instance of
585 * MediaWiki\Request\FauxRequest, errors are thrown and no printing occurs
586 * @param bool $enableWrite Should be set to true if the api may modify data
587 * @param bool|null $internal Whether the API request is an internal faux
588 * request. If null or not given, the request is assumed to be internal
589 * if $context contains a FauxRequest.
591 public function __construct( $context = null, $enableWrite = false, $internal = null ) {
592 if ( $context === null ) {
593 $context = RequestContext::getMain();
594 } elseif ( $context instanceof WebRequest ) {
595 // BC for pre-1.19
596 $request = $context;
597 $context = RequestContext::getMain();
599 // We set a derivative context so we can change stuff later
600 $derivativeContext = new DerivativeContext( $context );
601 $this->setContext( $derivativeContext );
603 if ( isset( $request ) ) {
604 $derivativeContext->setRequest( $request );
605 } else {
606 $request = $this->getRequest();
609 $this->mInternalMode = $internal ?? ( $request instanceof FauxRequest );
611 // Special handling for the main module: $parent === $this
612 parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
614 $config = $this->getConfig();
615 // TODO inject stuff, see T265644
616 $services = MediaWikiServices::getInstance();
618 if ( !$this->mInternalMode ) {
619 // If we're in a mode that breaks the same-origin policy, strip
620 // user credentials for security.
621 if ( $this->lacksSameOriginSecurity() ) {
622 wfDebug( "API: stripping user credentials when the same-origin policy is not applied" );
623 $user = $services->getUserFactory()->newAnonymous();
624 StubGlobalUser::setUser( $user );
625 $derivativeContext->setUser( $user );
626 $request->response()->header( 'MediaWiki-Login-Suppressed: true' );
630 $this->mParamValidator = new ApiParamValidator(
631 $this,
632 $services->getObjectFactory()
635 $this->statsFactory = $services->getStatsFactory();
637 $this->mResult =
638 new ApiResult( $this->getConfig()->get( MainConfigNames::APIMaxResultSize ) );
640 // Setup uselang. This doesn't use $this->getParameter()
641 // because we're not ready to handle errors yet.
642 // Optimisation: Avoid slow getVal(), this isn't user-generated content.
643 $uselang = $request->getRawVal( 'uselang' ) ?? self::API_DEFAULT_USELANG;
644 if ( $uselang === 'user' ) {
645 // Assume the parent context is going to return the user language
646 // for uselang=user (see T85635).
647 } else {
648 if ( $uselang === 'content' ) {
649 $uselang = $services->getContentLanguageCode()->toString();
651 $code = RequestContext::sanitizeLangCode( $uselang );
652 $derivativeContext->setLanguage( $code );
653 if ( !$this->mInternalMode ) {
654 // phpcs:disable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
655 global $wgLang;
656 $wgLang = $derivativeContext->getLanguage();
657 RequestContext::getMain()->setLanguage( $wgLang );
658 // phpcs:enable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
662 // Set up the error formatter. This doesn't use $this->getParameter()
663 // because we're not ready to handle errors yet.
664 // Optimisation: Avoid slow getVal(), this isn't user-generated content.
665 $errorFormat = $request->getRawVal( 'errorformat' ) ?? 'bc';
666 $errorLangCode = $request->getRawVal( 'errorlang' ) ?? 'uselang';
667 $errorsUseDB = $request->getCheck( 'errorsuselocal' );
668 if ( in_array( $errorFormat, [ 'plaintext', 'wikitext', 'html', 'raw', 'none' ], true ) ) {
669 if ( $errorLangCode === 'uselang' ) {
670 $errorLang = $this->getLanguage();
671 } elseif ( $errorLangCode === 'content' ) {
672 $errorLang = $services->getContentLanguage();
673 } else {
674 $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode );
675 $errorLang = $services->getLanguageFactory()->getLanguage( $errorLangCode );
677 $this->mErrorFormatter = new ApiErrorFormatter(
678 $this->mResult,
679 $errorLang,
680 $errorFormat,
681 $errorsUseDB
683 } else {
684 $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
686 $this->mResult->setErrorFormatter( $this->getErrorFormatter() );
688 $this->mModuleMgr = new ApiModuleManager(
689 $this,
690 $services->getObjectFactory()
692 $this->mModuleMgr->addModules( self::MODULES, 'action' );
693 $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIModules ), 'action' );
694 $this->mModuleMgr->addModules( self::FORMATS, 'format' );
695 $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIFormatModules ), 'format' );
697 $this->getHookRunner()->onApiMain__moduleManager( $this->mModuleMgr );
699 $this->mContinuationManager = null;
700 $this->mEnableWrite = $enableWrite;
704 * Return true if the API was started by other PHP code using MediaWiki\Request\FauxRequest
705 * @return bool
707 public function isInternalMode() {
708 return $this->mInternalMode;
712 * Get the ApiResult object associated with current request
714 * @return ApiResult
716 public function getResult() {
717 return $this->mResult;
721 * Get the security flag for the current request
722 * @return bool
724 public function lacksSameOriginSecurity() {
725 if ( $this->lacksSameOriginSecurity !== null ) {
726 return $this->lacksSameOriginSecurity;
729 $request = $this->getRequest();
731 // JSONP mode
732 if ( $request->getCheck( 'callback' ) ||
733 // Anonymous CORS
734 $request->getVal( 'origin' ) === '*' ||
735 // Header to be used from XMLHTTPRequest when the request might
736 // otherwise be used for XSS.
737 $request->getHeader( 'Treat-as-Untrusted' ) !== false
739 $this->lacksSameOriginSecurity = true;
740 return true;
743 // Allow extensions to override.
744 $this->lacksSameOriginSecurity = !$this->getHookRunner()
745 ->onRequestHasSameOriginSecurity( $request );
746 return $this->lacksSameOriginSecurity;
750 * Get the ApiErrorFormatter object associated with current request
751 * @return ApiErrorFormatter
753 public function getErrorFormatter() {
754 return $this->mErrorFormatter;
758 * @return ApiContinuationManager|null
760 public function getContinuationManager() {
761 return $this->mContinuationManager;
765 * @param ApiContinuationManager|null $manager
767 public function setContinuationManager( ?ApiContinuationManager $manager = null ) {
768 if ( $manager !== null && $this->mContinuationManager !== null ) {
769 throw new UnexpectedValueException(
770 __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
771 ' when a manager is already set from ' . $this->mContinuationManager->getSource()
774 $this->mContinuationManager = $manager;
778 * Get the parameter validator
779 * @return ApiParamValidator
781 public function getParamValidator(): ApiParamValidator {
782 return $this->mParamValidator;
786 * Get the API module object. Only works after executeAction()
788 * @return ApiBase
790 public function getModule() {
791 return $this->mModule;
795 * Get the result formatter object. Only works after setupExecuteAction()
797 * @return ApiFormatBase
799 public function getPrinter() {
800 return $this->mPrinter;
804 * Set how long the response should be cached.
806 * @param int $maxage
808 public function setCacheMaxAge( $maxage ) {
809 $this->setCacheControl( [
810 'max-age' => $maxage,
811 's-maxage' => $maxage
812 ] );
816 * Set the type of caching headers which will be sent.
818 * @param string $mode One of:
819 * - 'public': Cache this object in public caches, if the maxage or smaxage
820 * parameter is set, or if setCacheMaxAge() was called. If a maximum age is
821 * not provided by any of these means, the object will be private.
822 * - 'private': Cache this object only in private client-side caches.
823 * - 'anon-public-user-private': Make this object cacheable for logged-out
824 * users, but private for logged-in users. IMPORTANT: If this is set, it must be
825 * set consistently for a given URL, it cannot be set differently depending on
826 * things like the contents of the database, or whether the user is logged in.
828 * If the wiki does not allow anonymous users to read it, the mode set here
829 * will be ignored, and private caching headers will always be sent. In other words,
830 * the "public" mode is equivalent to saying that the data sent is as public as a page
831 * view.
833 * For user-dependent data, the private mode should generally be used. The
834 * anon-public-user-private mode should only be used where there is a particularly
835 * good performance reason for caching the anonymous response, but where the
836 * response to logged-in users may differ, or may contain private data.
838 * If this function is never called, then the default will be the private mode.
840 public function setCacheMode( $mode ) {
841 if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
842 wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"" );
844 // Ignore for forwards-compatibility
845 return;
848 if ( !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) ) {
849 // Private wiki, only private headers
850 if ( $mode !== 'private' ) {
851 wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki" );
853 return;
857 if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
858 // User language is used for i18n, so we don't want to publicly
859 // cache. Anons are ok, because if they have non-default language
860 // then there's an appropriate Vary header set by whatever set
861 // their non-default language.
862 wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
863 "'anon-public-user-private' due to uselang=user" );
864 $mode = 'anon-public-user-private';
867 wfDebug( __METHOD__ . ": setting cache mode $mode" );
868 $this->mCacheMode = $mode;
871 public function getCacheMode() {
872 return $this->mCacheMode;
876 * Set directives (key/value pairs) for the Cache-Control header.
877 * Boolean values will be formatted as such, by including or omitting
878 * without an equals sign.
880 * Cache control values set here will only be used if the cache mode is not
881 * private, see setCacheMode().
883 * @param array $directives
885 public function setCacheControl( $directives ) {
886 $this->mCacheControl = $directives + $this->mCacheControl;
890 * Create an instance of an output formatter by its name
892 * @param string $format
894 * @return ApiFormatBase
896 public function createPrinterByName( $format ) {
897 $printer = $this->mModuleMgr->getModule( $format, 'format', /* $ignoreCache */ true );
898 if ( $printer === null ) {
899 $this->dieWithError(
900 [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
904 // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
905 return $printer;
909 * Execute api request. Any errors will be handled if the API was called by the remote client.
911 public function execute() {
912 if ( $this->mInternalMode ) {
913 $this->executeAction();
914 } else {
915 $this->executeActionWithErrorHandling();
920 * Execute an action, and in case of an error, erase whatever partial results
921 * have been accumulated, and replace it with an error message and a help screen.
923 protected function executeActionWithErrorHandling() {
924 // Verify the CORS header before executing the action
925 if ( !$this->handleCORS() ) {
926 // handleCORS() has sent a 403, abort
927 return;
930 // Exit here if the request method was OPTIONS
931 // (assume there will be a followup GET or POST)
932 if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
933 return;
936 // In case an error occurs during data output,
937 // clear the output buffer and print just the error information
938 $obLevel = ob_get_level();
939 ob_start();
941 $t = microtime( true );
942 $isError = false;
943 try {
944 $this->executeAction();
945 $runTime = microtime( true ) - $t;
946 $this->logRequest( $runTime );
948 $this->statsFactory->getTiming( 'api_executeTiming_seconds' )
949 ->setLabel( 'module', $this->mModule->getModuleName() )
950 ->copyToStatsdAt( 'api.' . $this->mModule->getModuleName() . '.executeTiming' )
951 ->observe( 1000 * $runTime );
952 } catch ( Throwable $e ) {
953 $this->handleException( $e );
954 $this->logRequest( microtime( true ) - $t, $e );
955 $isError = true;
958 // Disable the client cache on the output so that BlockManager::trackBlockWithCookie is executed
959 // as part of MediaWiki::preOutputCommit().
960 if (
961 $this->mCacheMode === 'private'
962 || (
963 $this->mCacheMode === 'anon-public-user-private'
964 && SessionManager::getGlobalSession()->isPersistent()
967 $this->getContext()->getOutput()->disableClientCache();
968 $this->getContext()->getOutput()->considerCacheSettingsFinal();
971 // Commit DBs and send any related cookies and headers
972 MediaWiki::preOutputCommit( $this->getContext() );
974 // Send cache headers after any code which might generate an error, to
975 // avoid sending public cache headers for errors.
976 $this->sendCacheHeaders( $isError );
978 // Executing the action might have already messed with the output
979 // buffers.
980 while ( ob_get_level() > $obLevel ) {
981 ob_end_flush();
986 * Handle a throwable as an API response
988 * @since 1.23
989 * @param Throwable $e
991 protected function handleException( Throwable $e ) {
992 // T65145: Rollback any open database transactions
993 if ( !$e instanceof ApiUsageException ) {
994 // ApiUsageExceptions are intentional, so don't rollback if that's the case
995 MWExceptionHandler::rollbackPrimaryChangesAndLog(
997 MWExceptionHandler::CAUGHT_BY_ENTRYPOINT
1001 // Allow extra cleanup and logging
1002 $this->getHookRunner()->onApiMain__onException( $this, $e );
1004 // Handle any kind of exception by outputting properly formatted error message.
1005 // If this fails, an unhandled exception should be thrown so that global error
1006 // handler will process and log it.
1008 $errCodes = $this->substituteResultWithError( $e );
1010 // Error results should not be cached
1011 $this->setCacheMode( 'private' );
1013 $response = $this->getRequest()->response();
1014 $headerStr = 'MediaWiki-API-Error: ' . implode( ', ', $errCodes );
1015 $response->header( $headerStr );
1017 // Reset and print just the error message
1018 ob_clean();
1020 // Printer may not be initialized if the extractRequestParams() fails for the main module
1021 $this->createErrorPrinter();
1023 // Get desired HTTP code from an ApiUsageException. Don't use codes from other
1024 // exception types, as they are unlikely to be intended as an HTTP code.
1025 $httpCode = $e instanceof ApiUsageException ? $e->getCode() : 0;
1027 $failed = false;
1028 try {
1029 $this->printResult( $httpCode );
1030 } catch ( ApiUsageException $ex ) {
1031 // The error printer itself is failing. Try suppressing its request
1032 // parameters and redo.
1033 $failed = true;
1034 $this->addWarning( 'apiwarn-errorprinterfailed' );
1035 foreach ( $ex->getStatusValue()->getMessages() as $error ) {
1036 try {
1037 $this->mPrinter->addWarning( $error );
1038 } catch ( Throwable $ex2 ) {
1039 // WTF?
1040 $this->addWarning( $error );
1044 if ( $failed ) {
1045 $this->mPrinter = null;
1046 $this->createErrorPrinter();
1047 // @phan-suppress-next-line PhanNonClassMethodCall False positive
1048 $this->mPrinter->forceDefaultParams();
1049 if ( $httpCode ) {
1050 $response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200
1052 $this->printResult( $httpCode );
1057 * Handle a throwable from the ApiBeforeMain hook.
1059 * This tries to print the throwable as an API response, to be more
1060 * friendly to clients. If it fails, it will rethrow the throwable.
1062 * @since 1.23
1063 * @param Throwable $e
1064 * @throws Throwable
1066 public static function handleApiBeforeMainException( Throwable $e ) {
1067 ob_start();
1069 try {
1070 $main = new self( RequestContext::getMain(), false );
1071 $main->handleException( $e );
1072 $main->logRequest( 0, $e );
1073 } catch ( Throwable $e2 ) {
1074 // Nope, even that didn't work. Punt.
1075 throw $e;
1078 // Reset cache headers
1079 $main->sendCacheHeaders( true );
1081 ob_end_flush();
1085 * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
1087 * If no origin parameter is present, nothing happens.
1088 * If an origin parameter is present but doesn't match the Origin header, a 403 status code
1089 * is set and false is returned.
1090 * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
1091 * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
1092 * headers are set.
1093 * https://www.w3.org/TR/cors/#resource-requests
1094 * https://www.w3.org/TR/cors/#resource-preflight-requests
1096 * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
1098 protected function handleCORS() {
1099 $originParam = $this->getParameter( 'origin' ); // defaults to null
1100 if ( $originParam === null ) {
1101 // No origin parameter, nothing to do
1102 return true;
1105 $request = $this->getRequest();
1106 $response = $request->response();
1108 $allowTiming = false;
1109 $varyOrigin = true;
1111 if ( $originParam === '*' ) {
1112 // Request for anonymous CORS
1113 // Technically we should check for the presence of an Origin header
1114 // and not process it as CORS if it's not set, but that would
1115 // require us to vary on Origin for all 'origin=*' requests which
1116 // we don't want to do.
1117 $matchedOrigin = true;
1118 $allowOrigin = '*';
1119 $allowCredentials = 'false';
1120 $varyOrigin = false; // No need to vary
1121 } else {
1122 // Non-anonymous CORS, check we allow the domain
1124 // Origin: header is a space-separated list of origins, check all of them
1125 $originHeader = $request->getHeader( 'Origin' );
1126 if ( $originHeader === false ) {
1127 $origins = [];
1128 } else {
1129 $originHeader = trim( $originHeader );
1130 $origins = preg_split( '/\s+/', $originHeader );
1133 if ( !in_array( $originParam, $origins ) ) {
1134 // origin parameter set but incorrect
1135 // Send a 403 response
1136 $response->statusHeader( 403 );
1137 $response->header( 'Cache-Control: no-cache' );
1138 echo "'origin' parameter does not match Origin header\n";
1140 return false;
1143 $config = $this->getConfig();
1144 $origin = Origin::parseHeaderList( $origins );
1145 $matchedOrigin = $origin->match(
1146 $config->get( MainConfigNames::CrossSiteAJAXdomains ),
1147 $config->get( MainConfigNames::CrossSiteAJAXdomainExceptions )
1150 $allowOrigin = $originHeader;
1151 $allowCredentials = 'true';
1152 $allowTiming = $originHeader;
1155 if ( $matchedOrigin ) {
1156 $requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
1157 $preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
1158 if ( $preflight ) {
1159 // We allow the actual request to send the following headers
1160 $requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
1161 $allowedHeaders = $this->getConfig()->get( MainConfigNames::AllowedCorsHeaders );
1162 if ( $requestedHeaders !== false ) {
1163 if ( !self::matchRequestedHeaders( $requestedHeaders, $allowedHeaders ) ) {
1164 $response->header( 'MediaWiki-CORS-Rejection: Unsupported header requested in preflight' );
1165 return true;
1167 $response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
1170 // We only allow the actual request to be GET, POST, or HEAD
1171 $response->header( 'Access-Control-Allow-Methods: POST, GET, HEAD' );
1174 $response->header( "Access-Control-Allow-Origin: $allowOrigin" );
1175 $response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
1176 // https://www.w3.org/TR/resource-timing/#timing-allow-origin
1177 if ( $allowTiming !== false ) {
1178 $response->header( "Timing-Allow-Origin: $allowTiming" );
1181 if ( !$preflight ) {
1182 $response->header(
1183 'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag, '
1184 . 'MediaWiki-Login-Suppressed'
1187 } else {
1188 $response->header( 'MediaWiki-CORS-Rejection: Origin mismatch' );
1191 if ( $varyOrigin ) {
1192 $this->getOutput()->addVaryHeader( 'Origin' );
1195 return true;
1199 * Attempt to validate the value of Access-Control-Request-Headers against a list
1200 * of headers that we allow the follow up request to send.
1202 * @param string $requestedHeaders Comma separated list of HTTP headers
1203 * @param string[] $allowedHeaders List of allowed HTTP headers
1204 * @return bool True if all requested headers are in the list of allowed headers
1206 protected static function matchRequestedHeaders( $requestedHeaders, $allowedHeaders ) {
1207 if ( trim( $requestedHeaders ) === '' ) {
1208 return true;
1210 $requestedHeaders = explode( ',', $requestedHeaders );
1211 $allowedHeaders = array_change_key_case(
1212 array_fill_keys( $allowedHeaders, true ), CASE_LOWER );
1213 foreach ( $requestedHeaders as $rHeader ) {
1214 $rHeader = strtolower( trim( $rHeader ) );
1215 if ( !isset( $allowedHeaders[$rHeader] ) ) {
1216 LoggerFactory::getInstance( 'api-warning' )->warning(
1217 'CORS preflight failed on requested header: {header}', [
1218 'header' => $rHeader
1221 return false;
1224 return true;
1228 * Send caching headers
1229 * @param bool $isError Whether an error response is being output
1230 * @since 1.26 added $isError parameter
1232 protected function sendCacheHeaders( $isError ) {
1233 $response = $this->getRequest()->response();
1234 $out = $this->getOutput();
1236 $out->addVaryHeader( 'Treat-as-Untrusted' );
1238 $config = $this->getConfig();
1240 if ( $config->get( MainConfigNames::VaryOnXFP ) ) {
1241 $out->addVaryHeader( 'X-Forwarded-Proto' );
1244 if ( !$isError && $this->mModule &&
1245 ( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
1247 $etag = $this->mModule->getConditionalRequestData( 'etag' );
1248 if ( $etag !== null ) {
1249 $response->header( "ETag: $etag" );
1251 $lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
1252 if ( $lastMod !== null ) {
1253 $response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
1257 // The logic should be:
1258 // $this->mCacheControl['max-age'] is set?
1259 // Use it, the module knows better than our guess.
1260 // !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
1261 // Use 0 because we can guess caching is probably the wrong thing to do.
1262 // Use $this->getParameter( 'maxage' ), which already defaults to 0.
1263 $maxage = 0;
1264 if ( isset( $this->mCacheControl['max-age'] ) ) {
1265 $maxage = $this->mCacheControl['max-age'];
1266 } elseif ( ( !$isError && $this->mModule && !$this->mModule->isWriteMode() ) ||
1267 $this->mCacheMode !== 'private'
1269 $maxage = $this->getParameter( 'maxage' );
1271 $privateCache = 'private, must-revalidate, max-age=' . $maxage;
1273 if ( $this->mCacheMode == 'private' ) {
1274 $response->header( "Cache-Control: $privateCache" );
1275 return;
1278 if ( $this->mCacheMode == 'anon-public-user-private' ) {
1279 $out->addVaryHeader( 'Cookie' );
1280 $response->header( $out->getVaryHeader() );
1281 if ( SessionManager::getGlobalSession()->isPersistent() ) {
1282 // Logged in or otherwise has session (e.g. anonymous users who have edited)
1283 // Mark request private
1284 $response->header( "Cache-Control: $privateCache" );
1286 return;
1287 } // else anonymous, send public headers below
1290 // Send public headers
1291 $response->header( $out->getVaryHeader() );
1293 // If nobody called setCacheMaxAge(), use the (s)maxage parameters
1294 if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
1295 $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
1297 if ( !isset( $this->mCacheControl['max-age'] ) ) {
1298 $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
1301 if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
1302 // Public cache not requested
1303 // Sending a Vary header in this case is harmless, and protects us
1304 // against conditional calls of setCacheMaxAge().
1305 $response->header( "Cache-Control: $privateCache" );
1307 return;
1310 $this->mCacheControl['public'] = true;
1312 // Send an Expires header
1313 $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
1314 $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
1315 $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
1317 // Construct the Cache-Control header
1318 $ccHeader = '';
1319 $separator = '';
1320 foreach ( $this->mCacheControl as $name => $value ) {
1321 if ( is_bool( $value ) ) {
1322 if ( $value ) {
1323 $ccHeader .= $separator . $name;
1324 $separator = ', ';
1326 } else {
1327 $ccHeader .= $separator . "$name=$value";
1328 $separator = ', ';
1332 $response->header( "Cache-Control: $ccHeader" );
1336 * Create the printer for error output
1338 private function createErrorPrinter() {
1339 if ( !$this->mPrinter ) {
1340 $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
1341 if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
1342 $value = self::API_DEFAULT_FORMAT;
1344 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable getVal does not return null here
1345 $this->mPrinter = $this->createPrinterByName( $value );
1348 // Printer may not be able to handle errors. This is particularly
1349 // likely if the module returns something for getCustomPrinter().
1350 if ( !$this->mPrinter->canPrintErrors() ) {
1351 $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
1356 * Create an error message for the given throwable.
1358 * If an ApiUsageException, errors/warnings will be extracted from the
1359 * embedded StatusValue.
1361 * Any other throwable will be returned with a generic code and wrapper
1362 * text around the throwable's (presumably English) message as a single
1363 * error (no warnings).
1365 * @param Throwable $e
1366 * @param string $type 'error' or 'warning'
1367 * @return ApiMessage[]
1368 * @since 1.27
1370 protected function errorMessagesFromException( Throwable $e, $type = 'error' ) {
1371 $messages = [];
1372 if ( $e instanceof ApiUsageException ) {
1373 foreach ( $e->getStatusValue()->getMessages( $type ) as $msg ) {
1374 $messages[] = ApiMessage::create( $msg );
1376 } elseif ( $type !== 'error' ) {
1377 // None of the rest have any messages for non-error types
1378 } else {
1379 // TODO: Avoid embedding arbitrary class names in the error code.
1380 $class = preg_replace( '#^Wikimedia\\\\Rdbms\\\\#', '', get_class( $e ) );
1381 $code = 'internal_api_error_' . $class;
1382 $data = [ 'errorclass' => get_class( $e ) ];
1383 if ( MWExceptionRenderer::shouldShowExceptionDetails() ) {
1384 if ( $e instanceof ILocalizedException ) {
1385 $msg = $e->getMessageObject();
1386 } elseif ( $e instanceof MessageSpecifier ) {
1387 $msg = Message::newFromSpecifier( $e );
1388 } else {
1389 $msg = wfEscapeWikiText( $e->getMessage() );
1391 $params = [ 'apierror-exceptioncaught', WebRequest::getRequestId(), $msg ];
1392 } else {
1393 $params = [ 'apierror-exceptioncaughttype', WebRequest::getRequestId(), get_class( $e ) ];
1396 $messages[] = ApiMessage::create( $params, $code, $data );
1398 return $messages;
1402 * Replace the result data with the information about a throwable.
1403 * @param Throwable $e
1404 * @return string[] Error codes
1406 protected function substituteResultWithError( Throwable $e ) {
1407 $result = $this->getResult();
1408 $formatter = $this->getErrorFormatter();
1409 $config = $this->getConfig();
1410 $errorCodes = [];
1412 // Remember existing warnings and errors across the reset
1413 $errors = $result->getResultData( [ 'errors' ] );
1414 $warnings = $result->getResultData( [ 'warnings' ] );
1415 $result->reset();
1416 if ( $warnings !== null ) {
1417 $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
1419 if ( $errors !== null ) {
1420 $result->addValue( null, 'errors', $errors, ApiResult::NO_SIZE_CHECK );
1422 // Collect the copied error codes for the return value
1423 foreach ( $errors as $error ) {
1424 if ( isset( $error['code'] ) ) {
1425 $errorCodes[$error['code']] = true;
1430 // Add errors from the exception
1431 $modulePath = $e instanceof ApiUsageException ? $e->getModulePath() : null;
1432 foreach ( $this->errorMessagesFromException( $e, 'error' ) as $msg ) {
1433 if ( ApiErrorFormatter::isValidApiCode( $msg->getApiCode() ) ) {
1434 $errorCodes[$msg->getApiCode()] = true;
1435 } else {
1436 LoggerFactory::getInstance( 'api-warning' )->error( 'Invalid API error code "{code}"', [
1437 'code' => $msg->getApiCode(),
1438 'exception' => $e,
1439 ] );
1440 $errorCodes['<invalid-code>'] = true;
1442 $formatter->addError( $modulePath, $msg );
1444 foreach ( $this->errorMessagesFromException( $e, 'warning' ) as $msg ) {
1445 $formatter->addWarning( $modulePath, $msg );
1448 // Add additional data. Path depends on whether we're in BC mode or not.
1449 // Data depends on the type of exception.
1450 if ( $formatter instanceof ApiErrorFormatter_BackCompat ) {
1451 $path = [ 'error' ];
1452 } else {
1453 $path = null;
1455 if ( $e instanceof ApiUsageException ) {
1456 $link = (string)MediaWikiServices::getInstance()->getUrlUtils()->expand( wfScript( 'api' ) );
1457 $result->addContentValue(
1458 $path,
1459 'docref',
1460 trim(
1461 $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text()
1462 . ' '
1463 . $this->msg( 'api-usage-mailinglist-ref' )->inLanguage( $formatter->getLanguage() )->text()
1466 } elseif ( $config->get( MainConfigNames::ShowExceptionDetails ) ) {
1467 $result->addContentValue(
1468 $path,
1469 'trace',
1470 $this->msg( 'api-exception-trace',
1471 get_class( $e ),
1472 $e->getFile(),
1473 $e->getLine(),
1474 MWExceptionHandler::getRedactedTraceAsString( $e )
1475 )->inLanguage( $formatter->getLanguage() )->text()
1479 // Add the id and such
1480 $this->addRequestedFields( [ 'servedby' ] );
1482 return array_keys( $errorCodes );
1486 * Add requested fields to the result
1487 * @param string[] $force Which fields to force even if not requested. Accepted values are:
1488 * - servedby
1490 protected function addRequestedFields( $force = [] ) {
1491 $result = $this->getResult();
1493 $requestid = $this->getParameter( 'requestid' );
1494 if ( $requestid !== null ) {
1495 $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
1498 if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) && (
1499 in_array( 'servedby', $force, true ) || $this->getParameter( 'servedby' )
1500 ) ) {
1501 $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
1504 if ( $this->getParameter( 'curtimestamp' ) ) {
1505 $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601 ), ApiResult::NO_SIZE_CHECK );
1508 if ( $this->getParameter( 'responselanginfo' ) ) {
1509 $result->addValue(
1510 null,
1511 'uselang',
1512 $this->getLanguage()->getCode(),
1513 ApiResult::NO_SIZE_CHECK
1515 $result->addValue(
1516 null,
1517 'errorlang',
1518 $this->getErrorFormatter()->getLanguage()->getCode(),
1519 ApiResult::NO_SIZE_CHECK
1525 * Set up for the execution.
1526 * @return array
1528 protected function setupExecuteAction() {
1529 $this->addRequestedFields();
1531 $params = $this->extractRequestParams();
1532 $this->mAction = $params['action'];
1534 return $params;
1538 * Set up the module for response
1539 * @return ApiBase The module that will handle this action
1540 * @throws ApiUsageException
1542 protected function setupModule() {
1543 // Instantiate the module requested by the user
1544 $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
1545 if ( $module === null ) {
1546 // Probably can't happen
1547 // @codeCoverageIgnoreStart
1548 $this->dieWithError(
1549 [ 'apierror-unknownaction', wfEscapeWikiText( $this->mAction ) ],
1550 'unknown_action'
1552 // @codeCoverageIgnoreEnd
1554 $moduleParams = $module->extractRequestParams();
1556 // Check token, if necessary
1557 if ( $module->needsToken() === true ) {
1558 throw new LogicException(
1559 "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
1560 'See documentation for ApiBase::needsToken for details.'
1563 if ( $module->needsToken() ) {
1564 if ( !$module->mustBePosted() ) {
1565 throw new LogicException(
1566 "Module '{$module->getModuleName()}' must require POST to use tokens."
1570 if ( !isset( $moduleParams['token'] ) ) {
1571 // Probably can't happen
1572 // @codeCoverageIgnoreStart
1573 $module->dieWithError( [ 'apierror-missingparam', 'token' ] );
1574 // @codeCoverageIgnoreEnd
1577 $module->requirePostedParameters( [ 'token' ] );
1579 if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
1580 $module->dieWithError( 'apierror-badtoken' );
1584 // @phan-suppress-next-line PhanTypeMismatchReturnNullable T240141
1585 return $module;
1589 * @return array
1591 private function getMaxLag() {
1592 $services = MediaWikiServices::getInstance();
1593 $dbLag = $services->getDBLoadBalancer()->getMaxLag();
1594 $lagInfo = [
1595 'host' => $dbLag[0],
1596 'lag' => $dbLag[1],
1597 'type' => 'db'
1600 $jobQueueLagFactor =
1601 $this->getConfig()->get( MainConfigNames::JobQueueIncludeInMaxLagFactor );
1602 if ( $jobQueueLagFactor ) {
1603 // Turn total number of jobs into seconds by using the configured value
1604 $totalJobs = array_sum( $services->getJobQueueGroup()->getQueueSizes() );
1605 $jobQueueLag = $totalJobs / (float)$jobQueueLagFactor;
1606 if ( $jobQueueLag > $lagInfo['lag'] ) {
1607 $lagInfo = [
1608 'host' => wfHostname(), // XXX: Is there a better value that could be used?
1609 'lag' => $jobQueueLag,
1610 'type' => 'jobqueue',
1611 'jobs' => $totalJobs,
1616 $this->getHookRunner()->onApiMaxLagInfo( $lagInfo );
1618 return $lagInfo;
1622 * Check the max lag if necessary
1623 * @param ApiBase $module Api module being used
1624 * @param array $params Array an array containing the request parameters.
1625 * @return bool True on success, false should exit immediately
1627 protected function checkMaxLag( $module, $params ) {
1628 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
1629 $maxLag = $params['maxlag'];
1630 $lagInfo = $this->getMaxLag();
1631 if ( $lagInfo['lag'] > $maxLag ) {
1632 $response = $this->getRequest()->response();
1634 $response->header( 'Retry-After: ' . max( (int)$maxLag, 5 ) );
1635 $response->header( 'X-Database-Lag: ' . (int)$lagInfo['lag'] );
1637 if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) {
1638 $this->dieWithError(
1639 [ 'apierror-maxlag', $lagInfo['lag'], $lagInfo['host'] ],
1640 'maxlag',
1641 $lagInfo
1645 $this->dieWithError( [ 'apierror-maxlag-generic', $lagInfo['lag'] ], 'maxlag', $lagInfo );
1649 return true;
1653 * Check selected RFC 7232 precondition headers
1655 * RFC 7232 envisions a particular model where you send your request to "a
1656 * resource", and for write requests that you can read "the resource" by
1657 * changing the method to GET. When the API receives a GET request, it
1658 * works out even though "the resource" from RFC 7232's perspective might
1659 * be many resources from MediaWiki's perspective. But it totally fails for
1660 * a POST, since what HTTP sees as "the resource" is probably just
1661 * "/api.php" with all the interesting bits in the body.
1663 * Therefore, we only support RFC 7232 precondition headers for GET (and
1664 * HEAD). That means we don't need to bother with If-Match and
1665 * If-Unmodified-Since since they only apply to modification requests.
1667 * And since we don't support Range, If-Range is ignored too.
1669 * @since 1.26
1670 * @param ApiBase $module Api module being used
1671 * @return bool True on success, false should exit immediately
1673 protected function checkConditionalRequestHeaders( $module ) {
1674 if ( $this->mInternalMode ) {
1675 // No headers to check in internal mode
1676 return true;
1679 if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) {
1680 // Don't check POSTs
1681 return true;
1684 $return304 = false;
1686 $ifNoneMatch = array_diff(
1687 $this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: [],
1688 [ '' ]
1690 if ( $ifNoneMatch ) {
1691 // @phan-suppress-next-line PhanImpossibleTypeComparison
1692 if ( $ifNoneMatch === [ '*' ] ) {
1693 // API responses always "exist"
1694 $etag = '*';
1695 } else {
1696 $etag = $module->getConditionalRequestData( 'etag' );
1699 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $etag is declared when $ifNoneMatch is true
1700 if ( $ifNoneMatch && $etag !== null ) {
1701 $test = str_starts_with( $etag, 'W/' ) ? substr( $etag, 2 ) : $etag;
1702 $match = array_map( static function ( $s ) {
1703 return str_starts_with( $s, 'W/' ) ? substr( $s, 2 ) : $s;
1704 }, $ifNoneMatch );
1705 $return304 = in_array( $test, $match, true );
1706 } else {
1707 $value = trim( $this->getRequest()->getHeader( 'If-Modified-Since' ) );
1709 // Some old browsers sends sizes after the date, like this:
1710 // Wed, 20 Aug 2003 06:51:19 GMT; length=5202
1711 // Ignore that.
1712 $i = strpos( $value, ';' );
1713 if ( $i !== false ) {
1714 $value = trim( substr( $value, 0, $i ) );
1717 if ( $value !== '' ) {
1718 try {
1719 $ts = new MWTimestamp( $value );
1720 if (
1721 // RFC 7231 IMF-fixdate
1722 $ts->getTimestamp( TS_RFC2822 ) === $value ||
1723 // RFC 850
1724 $ts->format( 'l, d-M-y H:i:s' ) . ' GMT' === $value ||
1725 // asctime (with and without space-padded day)
1726 $ts->format( 'D M j H:i:s Y' ) === $value ||
1727 $ts->format( 'D M j H:i:s Y' ) === $value
1729 $config = $this->getConfig();
1730 $lastMod = $module->getConditionalRequestData( 'last-modified' );
1731 if ( $lastMod !== null ) {
1732 // Mix in some MediaWiki modification times
1733 $modifiedTimes = [
1734 'page' => $lastMod,
1735 'user' => $this->getUser()->getTouched(),
1736 'epoch' => $config->get( MainConfigNames::CacheEpoch ),
1739 if ( $config->get( MainConfigNames::UseCdn ) ) {
1740 // T46570: the core page itself may not change, but resources might
1741 $modifiedTimes['sepoch'] = wfTimestamp(
1742 TS_MW, time() - $config->get( MainConfigNames::CdnMaxAge )
1745 $this->getHookRunner()->onOutputPageCheckLastModified( $modifiedTimes, $this->getOutput() );
1746 $lastMod = max( $modifiedTimes );
1747 $return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
1750 } catch ( TimestampException $e ) {
1751 // Invalid timestamp, ignore it
1756 if ( $return304 ) {
1757 $this->getRequest()->response()->statusHeader( 304 );
1759 // Avoid outputting the compressed representation of a zero-length body
1760 AtEase::suppressWarnings();
1761 ini_set( 'zlib.output_compression', 0 );
1762 AtEase::restoreWarnings();
1763 wfResetOutputBuffers( false );
1765 return false;
1768 return true;
1772 * Check for sufficient permissions to execute
1773 * @param ApiBase $module An Api module
1775 protected function checkExecutePermissions( $module ) {
1776 $user = $this->getUser();
1777 if ( $module->isReadMode() && !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) &&
1778 !$this->getAuthority()->isAllowed( 'read' )
1780 $this->dieWithError( 'apierror-readapidenied' );
1783 if ( $module->isWriteMode() ) {
1784 if ( !$this->mEnableWrite ) {
1785 $this->dieWithError( 'apierror-noapiwrite' );
1786 } elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) {
1787 $this->dieWithError( 'apierror-promised-nonwrite-api' );
1790 $this->checkReadOnly( $module );
1793 // Allow extensions to stop execution for arbitrary reasons.
1794 // TODO: change hook to accept Authority
1795 $message = 'hookaborted';
1796 if ( !$this->getHookRunner()->onApiCheckCanExecute( $module, $user, $message ) ) {
1797 $this->dieWithError( $message );
1802 * Check if the DB is read-only for this user
1803 * @param ApiBase $module An Api module
1805 protected function checkReadOnly( $module ) {
1806 if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
1807 $this->dieReadOnly();
1810 if ( $module->isWriteMode()
1811 && $this->getUser()->isBot()
1812 && MediaWikiServices::getInstance()->getDBLoadBalancer()->hasReplicaServers()
1814 $this->checkBotReadOnly();
1819 * Check whether we are readonly for bots
1821 private function checkBotReadOnly() {
1822 // Figure out how many servers have passed the lag threshold
1823 $numLagged = 0;
1824 $lagLimit = $this->getConfig()->get( MainConfigNames::APIMaxLagThreshold );
1825 $laggedServers = [];
1826 $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
1827 foreach ( $loadBalancer->getLagTimes() as $serverIndex => $lag ) {
1828 if ( $lag > $lagLimit ) {
1829 ++$numLagged;
1830 $laggedServers[] = $loadBalancer->getServerName( $serverIndex ) . " ({$lag}s)";
1834 // If a majority of replica DBs are too lagged then disallow writes
1835 $replicaCount = $loadBalancer->getServerCount() - 1;
1836 if ( $numLagged >= ceil( $replicaCount / 2 ) ) {
1837 $laggedServers = implode( ', ', $laggedServers );
1838 wfDebugLog(
1839 'api-readonly', // Deprecate this channel in favor of api-warning?
1840 "Api request failed as read only because the following DBs are lagged: $laggedServers"
1842 LoggerFactory::getInstance( 'api-warning' )->warning(
1843 "Api request failed as read only because the following DBs are lagged: {laggeddbs}", [
1844 'laggeddbs' => $laggedServers,
1848 $this->dieWithError(
1849 'readonly_lag',
1850 'readonly',
1851 [ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ]
1857 * Check asserts of the user's rights
1858 * @param array $params
1860 protected function checkAsserts( $params ) {
1861 if ( isset( $params['assert'] ) ) {
1862 $user = $this->getUser();
1863 switch ( $params['assert'] ) {
1864 case 'anon':
1865 if ( $user->isRegistered() ) {
1866 $this->dieWithError( 'apierror-assertanonfailed' );
1868 break;
1869 case 'user':
1870 if ( !$user->isRegistered() ) {
1871 $this->dieWithError( 'apierror-assertuserfailed' );
1873 break;
1874 case 'bot':
1875 if ( !$this->getAuthority()->isAllowed( 'bot' ) ) {
1876 $this->dieWithError( 'apierror-assertbotfailed' );
1878 break;
1881 if ( isset( $params['assertuser'] ) ) {
1882 // TODO inject stuff, see T265644
1883 $assertUser = MediaWikiServices::getInstance()->getUserFactory()
1884 ->newFromName( $params['assertuser'], UserRigorOptions::RIGOR_NONE );
1885 if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) {
1886 $this->dieWithError(
1887 [ 'apierror-assertnameduserfailed', wfEscapeWikiText( $params['assertuser'] ) ]
1894 * Check POST for external response and setup result printer
1895 * @param ApiBase $module An Api module
1896 * @param array $params An array with the request parameters
1898 protected function setupExternalResponse( $module, $params ) {
1899 $validMethods = [ 'GET', 'HEAD', 'POST', 'OPTIONS' ];
1900 $request = $this->getRequest();
1902 if ( !in_array( $request->getMethod(), $validMethods ) ) {
1903 $this->dieWithError( 'apierror-invalidmethod', null, null, 405 );
1906 if ( !$request->wasPosted() && $module->mustBePosted() ) {
1907 // Module requires POST. GET request might still be allowed
1908 // if $wgDebugApi is true, otherwise fail.
1909 $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] );
1912 if ( $request->wasPosted() ) {
1913 if ( !$request->getHeader( 'Content-Type' ) ) {
1914 $this->addDeprecation(
1915 'apiwarn-deprecation-post-without-content-type', 'post-without-content-type'
1918 $contentLength = $request->getHeader( 'Content-Length' );
1919 $maxPostSize = wfShorthandToInteger( ini_get( 'post_max_size' ), 0 );
1920 if ( $maxPostSize && $contentLength > $maxPostSize ) {
1921 $this->dieWithError(
1922 [ 'apierror-http-contenttoolarge', Message::sizeParam( $maxPostSize ) ],
1923 null, null, 413
1928 // See if custom printer is used
1929 $this->mPrinter = $module->getCustomPrinter() ??
1930 // Create an appropriate printer if not set
1931 $this->createPrinterByName( $params['format'] );
1933 if ( $request->getProtocol() === 'http' &&
1935 $this->getConfig()->get( MainConfigNames::ForceHTTPS ) ||
1936 $request->getSession()->shouldForceHTTPS() ||
1937 $this->getUser()->requiresHTTPS()
1940 $this->addDeprecation( 'apiwarn-deprecation-httpsexpected', 'https-expected' );
1945 * Execute the actual module, without any error handling
1947 protected function executeAction() {
1948 $params = $this->setupExecuteAction();
1950 // Check asserts early so e.g. errors in parsing a module's parameters due to being
1951 // logged out don't override the client's intended "am I logged in?" check.
1952 $this->checkAsserts( $params );
1954 $module = $this->setupModule();
1955 $this->mModule = $module;
1957 if ( !$this->mInternalMode ) {
1958 ProfilingContext::singleton()->init( MW_ENTRY_POINT, $module->getModuleName() );
1959 $this->setRequestExpectations( $module );
1962 $this->checkExecutePermissions( $module );
1964 if ( !$this->checkMaxLag( $module, $params ) ) {
1965 return;
1968 if ( !$this->checkConditionalRequestHeaders( $module ) ) {
1969 return;
1972 if ( !$this->mInternalMode ) {
1973 $this->setupExternalResponse( $module, $params );
1976 $module->execute();
1977 $this->getHookRunner()->onAPIAfterExecute( $module );
1979 $this->reportUnusedParams();
1981 if ( !$this->mInternalMode ) {
1982 MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
1984 $this->printResult();
1989 * Set database connection, query, and write expectations given this module request
1990 * @param ApiBase $module
1992 protected function setRequestExpectations( ApiBase $module ) {
1993 $request = $this->getRequest();
1995 $trxLimits = $this->getConfig()->get( MainConfigNames::TrxProfilerLimits );
1996 $trxProfiler = Profiler::instance()->getTransactionProfiler();
1997 $trxProfiler->setLogger( LoggerFactory::getInstance( 'rdbms' ) );
1998 $trxProfiler->setStatsFactory( MediaWikiServices::getInstance()->getStatsFactory() );
1999 $trxProfiler->setRequestMethod( $request->getMethod() );
2000 if ( $request->hasSafeMethod() ) {
2001 $trxProfiler->setExpectations( $trxLimits['GET'], __METHOD__ );
2002 } elseif ( $request->wasPosted() && !$module->isWriteMode() ) {
2003 $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ );
2004 } else {
2005 $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ );
2010 * Log the preceding request
2011 * @param float $time Time in seconds
2012 * @param Throwable|null $e Throwable caught while processing the request
2014 protected function logRequest( $time, ?Throwable $e = null ) {
2015 $request = $this->getRequest();
2017 $user = $this->getUser();
2018 $performer = [
2019 'user_text' => $user->getName(),
2021 if ( $user->isRegistered() ) {
2022 $performer['user_id'] = $user->getId();
2024 $logCtx = [
2025 // https://gerrit.wikimedia.org/g/mediawiki/event-schemas/+/master/jsonschema/mediawiki/api/request
2026 '$schema' => '/mediawiki/api/request/1.0.0',
2027 'meta' => [
2028 'request_id' => WebRequest::getRequestId(),
2029 'id' => MediaWikiServices::getInstance()
2030 ->getGlobalIdGenerator()->newUUIDv4(),
2031 'dt' => wfTimestamp( TS_ISO_8601 ),
2032 'domain' => $this->getConfig()->get( MainConfigNames::ServerName ),
2033 // If using the EventBus extension (as intended) with this log channel,
2034 // this stream name will map to a Kafka topic.
2035 'stream' => 'mediawiki.api-request'
2037 'http' => [
2038 'method' => $request->getMethod(),
2039 'client_ip' => $request->getIP()
2041 'performer' => $performer,
2042 'database' => WikiMap::getCurrentWikiDbDomain()->getId(),
2043 'backend_time_ms' => (int)round( $time * 1000 ),
2046 // If set, these headers will be logged in http.request_headers.
2047 $httpRequestHeadersToLog = [ 'accept-language', 'referer', 'user-agent' ];
2048 foreach ( $httpRequestHeadersToLog as $header ) {
2049 if ( $request->getHeader( $header ) ) {
2050 // Set the header in http.request_headers
2051 $logCtx['http']['request_headers'][$header] = $request->getHeader( $header );
2055 if ( $e ) {
2056 $logCtx['api_error_codes'] = [];
2057 foreach ( $this->errorMessagesFromException( $e ) as $msg ) {
2058 $logCtx['api_error_codes'][] = $msg->getApiCode();
2062 // Construct space separated message for 'api' log channel
2063 $msg = "API {$request->getMethod()} " .
2064 wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
2065 " {$logCtx['http']['client_ip']} " .
2066 "T={$logCtx['backend_time_ms']}ms";
2068 $sensitive = array_fill_keys( $this->getSensitiveParams(), true );
2069 foreach ( $this->getParamsUsed() as $name ) {
2070 $value = $request->getVal( $name );
2071 if ( $value === null ) {
2072 continue;
2075 if ( isset( $sensitive[$name] ) ) {
2076 $value = '[redacted]';
2077 $encValue = '[redacted]';
2078 } elseif ( strlen( $value ) > 256 ) {
2079 $value = substr( $value, 0, 256 );
2080 $encValue = $this->encodeRequestLogValue( $value ) . '[...]';
2081 } else {
2082 $encValue = $this->encodeRequestLogValue( $value );
2085 $logCtx['params'][$name] = $value;
2086 $msg .= " {$name}={$encValue}";
2089 // Log an unstructured message to the api channel.
2090 wfDebugLog( 'api', $msg, 'private' );
2092 // The api-request channel a structured data log channel.
2093 wfDebugLog( 'api-request', '', 'private', $logCtx );
2097 * Encode a value in a format suitable for a space-separated log line.
2098 * @param string $s
2099 * @return string
2101 protected function encodeRequestLogValue( $s ) {
2102 static $table = [];
2103 if ( !$table ) {
2104 $chars = ';@$!*(),/:';
2105 $numChars = strlen( $chars );
2106 for ( $i = 0; $i < $numChars; $i++ ) {
2107 $table[rawurlencode( $chars[$i] )] = $chars[$i];
2111 return strtr( rawurlencode( $s ), $table );
2115 * Get the request parameters used in the course of the preceding execute() request
2116 * @return array
2118 protected function getParamsUsed() {
2119 return array_keys( $this->mParamsUsed );
2123 * Mark parameters as used
2124 * @param string|string[] $params
2126 public function markParamsUsed( $params ) {
2127 $this->mParamsUsed += array_fill_keys( (array)$params, true );
2131 * Get the request parameters that should be considered sensitive
2132 * @since 1.29
2133 * @return array
2135 protected function getSensitiveParams() {
2136 return array_keys( $this->mParamsSensitive );
2140 * Mark parameters as sensitive
2142 * This is called automatically for you when declaring a parameter
2143 * with ApiBase::PARAM_SENSITIVE.
2145 * @since 1.29
2146 * @param string|string[] $params
2148 public function markParamsSensitive( $params ) {
2149 $this->mParamsSensitive += array_fill_keys( (array)$params, true );
2153 * Get a request value, and register the fact that it was used, for logging.
2154 * @param string $name
2155 * @param string|null $default
2156 * @return string|null
2158 public function getVal( $name, $default = null ) {
2159 $this->mParamsUsed[$name] = true;
2161 $ret = $this->getRequest()->getVal( $name );
2162 if ( $ret === null ) {
2163 if ( $this->getRequest()->getArray( $name ) !== null ) {
2164 // See T12262 for why we don't just implode( '|', ... ) the
2165 // array.
2166 $this->addWarning( [ 'apiwarn-unsupportedarray', $name ] );
2168 $ret = $default;
2170 return $ret;
2174 * Get a boolean request value, and register the fact that the parameter
2175 * was used, for logging.
2176 * @param string $name
2177 * @return bool
2179 public function getCheck( $name ) {
2180 $this->mParamsUsed[$name] = true;
2181 return $this->getRequest()->getCheck( $name );
2185 * Get a request upload, and register the fact that it was used, for logging.
2187 * @since 1.21
2188 * @param string $name Parameter name
2189 * @return WebRequestUpload
2191 public function getUpload( $name ) {
2192 $this->mParamsUsed[$name] = true;
2194 return $this->getRequest()->getUpload( $name );
2198 * Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,
2199 * for example in case of spelling mistakes or a missing 'g' prefix for generators.
2201 protected function reportUnusedParams() {
2202 $paramsUsed = $this->getParamsUsed();
2203 $allParams = $this->getRequest()->getValueNames();
2205 if ( !$this->mInternalMode ) {
2206 // Printer has not yet executed; don't warn that its parameters are unused
2207 $printerParams = $this->mPrinter->encodeParamName(
2208 array_keys( $this->mPrinter->getFinalParams() ?: [] )
2210 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
2211 } else {
2212 $unusedParams = array_diff( $allParams, $paramsUsed );
2215 if ( count( $unusedParams ) ) {
2216 $this->addWarning( [
2217 'apierror-unrecognizedparams',
2218 Message::listParam( array_map( 'wfEscapeWikiText', $unusedParams ), 'comma' ),
2219 count( $unusedParams )
2220 ] );
2225 * Print results using the current printer
2227 * @param int $httpCode HTTP status code, or 0 to not change
2229 protected function printResult( $httpCode = 0 ) {
2230 if ( $this->getConfig()->get( MainConfigNames::DebugAPI ) !== false ) {
2231 $this->addWarning( 'apiwarn-wgdebugapi' );
2234 $printer = $this->mPrinter;
2235 $printer->initPrinter( false );
2236 if ( $httpCode ) {
2237 $printer->setHttpStatus( $httpCode );
2239 $printer->execute();
2240 $printer->closePrinter();
2244 * @return bool
2246 public function isReadMode() {
2247 return false;
2251 * See ApiBase for description.
2253 * @return array
2255 public function getAllowedParams() {
2256 return [
2257 'action' => [
2258 ParamValidator::PARAM_DEFAULT => 'help',
2259 ParamValidator::PARAM_TYPE => 'submodule',
2261 'format' => [
2262 ParamValidator::PARAM_DEFAULT => self::API_DEFAULT_FORMAT,
2263 ParamValidator::PARAM_TYPE => 'submodule',
2265 'maxlag' => [
2266 ParamValidator::PARAM_TYPE => 'integer'
2268 'smaxage' => [
2269 ParamValidator::PARAM_TYPE => 'integer',
2270 ParamValidator::PARAM_DEFAULT => 0,
2271 IntegerDef::PARAM_MIN => 0,
2273 'maxage' => [
2274 ParamValidator::PARAM_TYPE => 'integer',
2275 ParamValidator::PARAM_DEFAULT => 0,
2276 IntegerDef::PARAM_MIN => 0,
2278 'assert' => [
2279 ParamValidator::PARAM_TYPE => [ 'anon', 'user', 'bot' ]
2281 'assertuser' => [
2282 ParamValidator::PARAM_TYPE => 'user',
2283 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'temp' ],
2285 'requestid' => null,
2286 'servedby' => false,
2287 'curtimestamp' => false,
2288 'responselanginfo' => false,
2289 'origin' => null,
2290 'uselang' => [
2291 ParamValidator::PARAM_DEFAULT => self::API_DEFAULT_USELANG,
2293 'variant' => null,
2294 'errorformat' => [
2295 ParamValidator::PARAM_TYPE => [ 'plaintext', 'wikitext', 'html', 'raw', 'none', 'bc' ],
2296 ParamValidator::PARAM_DEFAULT => 'bc',
2297 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
2299 'errorlang' => [
2300 ParamValidator::PARAM_DEFAULT => 'uselang',
2302 'errorsuselocal' => [
2303 ParamValidator::PARAM_DEFAULT => false,
2308 /** @inheritDoc */
2309 protected function getExamplesMessages() {
2310 return [
2311 'action=help'
2312 => 'apihelp-help-example-main',
2313 'action=help&recursivesubmodules=1'
2314 => 'apihelp-help-example-recursive',
2319 * @inheritDoc
2320 * @phan-param array{nolead?:bool,headerlevel?:int,tocnumber?:int[]} $options
2322 public function modifyHelp( array &$help, array $options, array &$tocData ) {
2323 // Wish PHP had an "array_insert_before". Instead, we have to manually
2324 // reindex the array to get 'permissions' in the right place.
2325 $oldHelp = $help;
2326 $help = [];
2327 foreach ( $oldHelp as $k => $v ) {
2328 if ( $k === 'submodules' ) {
2329 $help['permissions'] = '';
2331 $help[$k] = $v;
2333 $help['datatypes'] = '';
2334 $help['templatedparams'] = '';
2335 $help['credits'] = '';
2337 // Fill 'permissions'
2338 $help['permissions'] .= Html::openElement( 'div',
2339 [ 'class' => [ 'apihelp-block', 'apihelp-permissions' ] ] );
2340 $m = $this->msg( 'api-help-permissions' );
2341 if ( !$m->isDisabled() ) {
2342 $help['permissions'] .= Html::rawElement( 'div', [ 'class' => 'apihelp-block-head' ],
2343 $m->numParams( count( self::RIGHTS_MAP ) )->parse()
2346 $help['permissions'] .= Html::openElement( 'dl' );
2347 // TODO inject stuff, see T265644
2348 $groupPermissionsLookup = MediaWikiServices::getInstance()->getGroupPermissionsLookup();
2349 foreach ( self::RIGHTS_MAP as $right => $rightMsg ) {
2350 $help['permissions'] .= Html::element( 'dt', [], $right );
2352 $rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
2353 $help['permissions'] .= Html::rawElement( 'dd', [], $rightMsg );
2355 $groups = array_map( static function ( $group ) {
2356 return $group == '*' ? 'all' : $group;
2357 }, $groupPermissionsLookup->getGroupsWithPermission( $right ) );
2359 $help['permissions'] .= Html::rawElement( 'dd', [],
2360 $this->msg( 'api-help-permissions-granted-to' )
2361 ->numParams( count( $groups ) )
2362 ->params( Message::listParam( $groups ) )
2363 ->parse()
2366 $help['permissions'] .= Html::closeElement( 'dl' );
2367 $help['permissions'] .= Html::closeElement( 'div' );
2369 // Fill 'datatypes', 'templatedparams', and 'credits', if applicable
2370 if ( empty( $options['nolead'] ) ) {
2371 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Must set when nolead is not set
2372 $level = $options['headerlevel'];
2373 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Must set when nolead is not set
2374 $tocnumber = &$options['tocnumber'];
2376 $header = $this->msg( 'api-help-datatypes-header' )->parse();
2377 $headline = Html::rawElement(
2378 'h' . min( 6, $level ),
2379 [ 'class' => 'apihelp-header', 'id' => 'main/datatypes' ],
2380 $header
2382 $help['datatypes'] .= $headline;
2383 $help['datatypes'] .= $this->msg( 'api-help-datatypes-top' )->parseAsBlock();
2384 $help['datatypes'] .= '<dl>';
2385 foreach ( $this->getParamValidator()->knownTypes() as $type ) {
2386 $m = $this->msg( "api-help-datatype-$type" );
2387 if ( !$m->isDisabled() ) {
2388 $help['datatypes'] .= Html::element( 'dt', [ 'id' => "main/datatype/$type" ], $type );
2389 $help['datatypes'] .= Html::rawElement( 'dd', [], $m->parseAsBlock() );
2392 $help['datatypes'] .= '</dl>';
2393 if ( !isset( $tocData['main/datatypes'] ) ) {
2394 $tocnumber[$level]++;
2395 $tocData['main/datatypes'] = [
2396 'toclevel' => count( $tocnumber ),
2397 'level' => $level,
2398 'anchor' => 'main/datatypes',
2399 'line' => $header,
2400 'number' => implode( '.', $tocnumber ),
2401 'index' => '',
2405 $header = $this->msg( 'api-help-templatedparams-header' )->parse();
2406 $headline = Html::rawElement(
2407 'h' . min( 6, $level ),
2408 [ 'class' => 'apihelp-header', 'id' => 'main/templatedparams' ],
2409 $header
2411 $help['templatedparams'] .= $headline;
2412 $help['templatedparams'] .= $this->msg( 'api-help-templatedparams' )->parseAsBlock();
2413 if ( !isset( $tocData['main/templatedparams'] ) ) {
2414 $tocnumber[$level]++;
2415 $tocData['main/templatedparams'] = [
2416 'toclevel' => count( $tocnumber ),
2417 'level' => $level,
2418 'anchor' => 'main/templatedparams',
2419 'line' => $header,
2420 'number' => implode( '.', $tocnumber ),
2421 'index' => '',
2425 $header = $this->msg( 'api-credits-header' )->parse();
2426 $headline = Html::rawElement(
2427 'h' . min( 6, $level ),
2428 [ 'class' => 'apihelp-header', 'id' => 'main/credits' ],
2429 $header
2431 $help['credits'] .= $headline;
2432 $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
2433 if ( !isset( $tocData['main/credits'] ) ) {
2434 $tocnumber[$level]++;
2435 $tocData['main/credits'] = [
2436 'toclevel' => count( $tocnumber ),
2437 'level' => $level,
2438 'anchor' => 'main/credits',
2439 'line' => $header,
2440 'number' => implode( '.', $tocnumber ),
2441 'index' => '',
2447 /** @var bool|null */
2448 private $mCanApiHighLimits = null;
2451 * Check whether the current user is allowed to use high limits
2452 * @return bool
2454 public function canApiHighLimits() {
2455 if ( $this->mCanApiHighLimits === null ) {
2456 $this->mCanApiHighLimits = $this->getAuthority()->isAllowed( 'apihighlimits' );
2459 return $this->mCanApiHighLimits;
2463 * Overrides to return this instance's module manager.
2464 * @return ApiModuleManager
2466 public function getModuleManager() {
2467 return $this->mModuleMgr;
2471 * Fetches the user agent used for this request
2473 * This returns the value of the 'Api-User-Agent' header, if any,
2474 * or the standard User-Agent header, otherwise.
2476 * @return string
2478 public function getUserAgent() {
2479 $agent = (string)$this->getRequest()->getHeader( 'Api-user-agent' );
2480 if ( $agent == '' ) {
2481 $agent = $this->getRequest()->getHeader( 'User-agent' );
2484 return $agent;
2489 * For really cool vim folding this needs to be at the end:
2490 * vim: foldmarker=@{,@} foldmethod=marker
2493 /** @deprecated class alias since 1.43 */
2494 class_alias( ApiMain::class, 'ApiMain' );