Update git submodules
[mediawiki.git] / includes / api / ApiMain.php
blobd509ad944a955805b1245c2106c9d0ea26c29f4b
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 use MediaWiki\Api\Validator\ApiParamValidator;
25 use MediaWiki\Html\Html;
26 use MediaWiki\Linker\Linker;
27 use MediaWiki\Logger\LoggerFactory;
28 use MediaWiki\MainConfigNames;
29 use MediaWiki\MediaWikiServices;
30 use MediaWiki\ParamValidator\TypeDef\UserDef;
31 use MediaWiki\Parser\Sanitizer;
32 use MediaWiki\Profiler\ProfilingContext;
33 use MediaWiki\Request\FauxRequest;
34 use MediaWiki\Request\WebRequest;
35 use MediaWiki\Request\WebRequestUpload;
36 use MediaWiki\Rest\HeaderParser\Origin;
37 use MediaWiki\Session\SessionManager;
38 use MediaWiki\StubObject\StubGlobalUser;
39 use MediaWiki\User\User;
40 use MediaWiki\User\UserRigorOptions;
41 use MediaWiki\Utils\MWTimestamp;
42 use MediaWiki\WikiMap\WikiMap;
43 use Wikimedia\AtEase\AtEase;
44 use Wikimedia\ParamValidator\ParamValidator;
45 use Wikimedia\ParamValidator\TypeDef\IntegerDef;
46 use Wikimedia\Timestamp\TimestampException;
48 /**
49 * This is the main API class, used for both external and internal processing.
50 * When executed, it will create the requested formatter object,
51 * instantiate and execute an object associated with the needed action,
52 * and use formatter to print results.
53 * In case of an exception, an error message will be printed using the same formatter.
55 * To use API from another application, run it using MediaWiki\Request\FauxRequest object, in which
56 * case any internal exceptions will not be handled but passed up to the caller.
57 * After successful execution, use getResult() for the resulting data.
59 * @newable
60 * @note marked as newable in 1.35 for lack of a better alternative,
61 * but should use a factory in the future.
62 * @ingroup API
64 class ApiMain extends ApiBase {
65 /**
66 * When no format parameter is given, this format will be used
68 private const API_DEFAULT_FORMAT = 'jsonfm';
70 /**
71 * When no uselang parameter is given, this language will be used
73 private const API_DEFAULT_USELANG = 'user';
75 /**
76 * List of available modules: action name => module class
78 private const MODULES = [
79 'login' => [
80 'class' => ApiLogin::class,
81 'services' => [
82 'AuthManager',
85 'clientlogin' => [
86 'class' => ApiClientLogin::class,
87 'services' => [
88 'AuthManager',
91 'logout' => [
92 'class' => ApiLogout::class,
94 'createaccount' => [
95 'class' => ApiAMCreateAccount::class,
96 'services' => [
97 'AuthManager',
100 'linkaccount' => [
101 'class' => ApiLinkAccount::class,
102 'services' => [
103 'AuthManager',
106 'unlinkaccount' => [
107 'class' => ApiRemoveAuthenticationData::class,
108 'services' => [
109 'AuthManager',
112 'changeauthenticationdata' => [
113 'class' => ApiChangeAuthenticationData::class,
114 'services' => [
115 'AuthManager',
118 'removeauthenticationdata' => [
119 'class' => ApiRemoveAuthenticationData::class,
120 'services' => [
121 'AuthManager',
124 'resetpassword' => [
125 'class' => ApiResetPassword::class,
126 'services' => [
127 'PasswordReset',
130 'query' => [
131 'class' => ApiQuery::class,
132 'services' => [
133 'ObjectFactory',
134 'WikiExporterFactory',
135 'TitleFormatter',
136 'TitleFactory',
139 'expandtemplates' => [
140 'class' => ApiExpandTemplates::class,
141 'services' => [
142 'RevisionStore',
143 'ParserFactory',
146 'parse' => [
147 'class' => ApiParse::class,
148 'services' => [
149 'RevisionLookup',
150 'SkinFactory',
151 'LanguageNameUtils',
152 'LinkBatchFactory',
153 'LinkCache',
154 'ContentHandlerFactory',
155 'ParserFactory',
156 'WikiPageFactory',
157 'ContentRenderer',
158 'ContentTransformer',
159 'CommentFormatter',
160 'TempUserCreator',
161 'UserFactory',
162 'UrlUtils',
163 'TitleFormatter',
166 'stashedit' => [
167 'class' => ApiStashEdit::class,
168 'services' => [
169 'ContentHandlerFactory',
170 'PageEditStash',
171 'RevisionLookup',
172 'StatsdDataFactory',
173 'WikiPageFactory',
174 'TempUserCreator',
175 'UserFactory',
178 'opensearch' => [
179 'class' => ApiOpenSearch::class,
180 'services' => [
181 'LinkBatchFactory',
182 'SearchEngineConfig',
183 'SearchEngineFactory',
184 'UrlUtils',
187 'feedcontributions' => [
188 'class' => ApiFeedContributions::class,
189 'services' => [
190 'RevisionStore',
191 'TitleParser',
192 'LinkRenderer',
193 'LinkBatchFactory',
194 'HookContainer',
195 'DBLoadBalancerFactory',
196 'NamespaceInfo',
197 'UserFactory',
198 'CommentFormatter',
201 'feedrecentchanges' => [
202 'class' => ApiFeedRecentChanges::class,
203 'services' => [
204 'SpecialPageFactory',
207 'feedwatchlist' => [
208 'class' => ApiFeedWatchlist::class,
209 'services' => [
210 'ParserFactory',
213 'help' => [
214 'class' => ApiHelp::class,
215 'services' => [
216 'SkinFactory',
219 'paraminfo' => [
220 'class' => ApiParamInfo::class,
221 'services' => [
222 'UserFactory',
225 'rsd' => [
226 'class' => ApiRsd::class,
228 'compare' => [
229 'class' => ApiComparePages::class,
230 'services' => [
231 'RevisionStore',
232 'ArchivedRevisionLookup',
233 'SlotRoleRegistry',
234 'ContentHandlerFactory',
235 'ContentTransformer',
236 'CommentFormatter',
237 'TempUserCreator',
238 'UserFactory',
241 'checktoken' => [
242 'class' => ApiCheckToken::class,
244 'cspreport' => [
245 'class' => ApiCSPReport::class,
247 'validatepassword' => [
248 'class' => ApiValidatePassword::class,
249 'services' => [
250 'AuthManager',
251 'UserFactory',
255 // Write modules
256 'purge' => [
257 'class' => ApiPurge::class,
258 'services' => [
259 'WikiPageFactory',
260 'TitleFormatter',
263 'setnotificationtimestamp' => [
264 'class' => ApiSetNotificationTimestamp::class,
265 'services' => [
266 'DBLoadBalancerFactory',
267 'RevisionStore',
268 'WatchedItemStore',
269 'TitleFormatter',
270 'TitleFactory',
273 'rollback' => [
274 'class' => ApiRollback::class,
275 'services' => [
276 'RollbackPageFactory',
277 'WatchlistManager',
278 'UserOptionsLookup',
281 'delete' => [
282 'class' => ApiDelete::class,
283 'services' => [
284 'RepoGroup',
285 'WatchlistManager',
286 'UserOptionsLookup',
287 'DeletePageFactory',
290 'undelete' => [
291 'class' => ApiUndelete::class,
292 'services' => [
293 'WatchlistManager',
294 'UserOptionsLookup',
295 'UndeletePageFactory',
296 'WikiPageFactory',
299 'protect' => [
300 'class' => ApiProtect::class,
301 'services' => [
302 'WatchlistManager',
303 'UserOptionsLookup',
304 'RestrictionStore',
307 'block' => [
308 'class' => ApiBlock::class,
309 'services' => [
310 'BlockPermissionCheckerFactory',
311 'BlockUserFactory',
312 'TitleFactory',
313 'UserIdentityLookup',
314 'WatchedItemStore',
315 'BlockUtils',
316 'BlockActionInfo',
317 'WatchlistManager',
318 'UserOptionsLookup',
321 'unblock' => [
322 'class' => ApiUnblock::class,
323 'services' => [
324 'BlockPermissionCheckerFactory',
325 'UnblockUserFactory',
326 'UserIdentityLookup',
327 'WatchedItemStore',
328 'WatchlistManager',
329 'UserOptionsLookup',
332 'move' => [
333 'class' => ApiMove::class,
334 'services' => [
335 'MovePageFactory',
336 'RepoGroup',
337 'WatchlistManager',
338 'UserOptionsLookup',
341 'edit' => [
342 'class' => ApiEditPage::class,
343 'services' => [
344 'ContentHandlerFactory',
345 'RevisionLookup',
346 'WatchedItemStore',
347 'WikiPageFactory',
348 'WatchlistManager',
349 'UserOptionsLookup',
350 'RedirectLookup',
351 'TempUserCreator',
352 'UserFactory',
355 'upload' => [
356 'class' => ApiUpload::class,
357 'services' => [
358 'JobQueueGroup',
359 'WatchlistManager',
360 'UserOptionsLookup',
363 'filerevert' => [
364 'class' => ApiFileRevert::class,
365 'services' => [
366 'RepoGroup',
369 'emailuser' => [
370 'class' => ApiEmailUser::class,
372 'watch' => [
373 'class' => ApiWatch::class,
374 'services' => [
375 'WatchlistManager',
376 'TitleFormatter',
379 'patrol' => [
380 'class' => ApiPatrol::class,
381 'services' => [
382 'RevisionStore',
385 'import' => [
386 'class' => ApiImport::class,
387 'services' => [
388 'WikiImporterFactory',
391 'clearhasmsg' => [
392 'class' => ApiClearHasMsg::class,
393 'services' => [
394 'TalkPageNotificationManager',
397 'userrights' => [
398 'class' => ApiUserrights::class,
399 'services' => [
400 'UserGroupManager',
401 'WatchedItemStore',
402 'WatchlistManager',
403 'UserOptionsLookup',
406 'options' => [
407 'class' => ApiOptions::class,
408 'services' => [
409 'UserOptionsManager',
410 'PreferencesFactory',
413 'imagerotate' => [
414 'class' => ApiImageRotate::class,
415 'services' => [
416 'RepoGroup',
417 'TempFSFileFactory',
418 'TitleFactory',
421 'revisiondelete' => [
422 'class' => ApiRevisionDelete::class,
424 'managetags' => [
425 'class' => ApiManageTags::class,
427 'tag' => [
428 'class' => ApiTag::class,
429 'services' => [
430 'DBLoadBalancerFactory',
431 'RevisionStore',
432 'ChangeTagsStore',
435 'mergehistory' => [
436 'class' => ApiMergeHistory::class,
437 'services' => [
438 'MergeHistoryFactory',
441 'setpagelanguage' => [
442 'class' => ApiSetPageLanguage::class,
443 'services' => [
444 'DBLoadBalancerFactory',
445 'LanguageNameUtils',
448 'changecontentmodel' => [
449 'class' => ApiChangeContentModel::class,
450 'services' => [
451 'ContentHandlerFactory',
452 'ContentModelChangeFactory',
455 'acquiretempusername' => [
456 'class' => ApiAcquireTempUserName::class,
457 'services' => [
458 'TempUserCreator',
464 * List of available formats: format name => format class
466 private const FORMATS = [
467 'json' => [
468 'class' => ApiFormatJson::class,
470 'jsonfm' => [
471 'class' => ApiFormatJson::class,
473 'php' => [
474 'class' => ApiFormatPhp::class,
476 'phpfm' => [
477 'class' => ApiFormatPhp::class,
479 'xml' => [
480 'class' => ApiFormatXml::class,
482 'xmlfm' => [
483 'class' => ApiFormatXml::class,
485 'rawfm' => [
486 'class' => ApiFormatJson::class,
488 'none' => [
489 'class' => ApiFormatNone::class,
494 * List of user roles that are specifically relevant to the API.
495 * [ 'right' => [ 'msg' => 'Some message with a $1',
496 * 'params' => [ $someVarToSubst ] ],
497 * ];
499 private const RIGHTS_MAP = [
500 'writeapi' => [
501 'msg' => 'right-writeapi',
502 'params' => []
504 'apihighlimits' => [
505 'msg' => 'api-help-right-apihighlimits',
506 'params' => [ ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ]
510 /** @var ApiFormatBase|null */
511 private $mPrinter;
513 /** @var ApiModuleManager */
514 private $mModuleMgr;
516 /** @var ApiResult */
517 private $mResult;
519 /** @var ApiErrorFormatter */
520 private $mErrorFormatter;
522 /** @var ApiParamValidator */
523 private $mParamValidator;
525 /** @var ApiContinuationManager|null */
526 private $mContinuationManager;
528 /** @var string|null */
529 private $mAction;
531 /** @var bool */
532 private $mEnableWrite;
534 /** @var bool */
535 private $mInternalMode;
537 /** @var ApiBase */
538 private $mModule;
540 /** @var string */
541 private $mCacheMode = 'private';
543 /** @var array */
544 private $mCacheControl = [];
546 /** @var array */
547 private $mParamsUsed = [];
549 /** @var array */
550 private $mParamsSensitive = [];
552 /** @var bool|null Cached return value from self::lacksSameOriginSecurity() */
553 private $lacksSameOriginSecurity = null;
556 * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
558 * @stable to call
559 * @param IContextSource|WebRequest|null $context If this is an instance of
560 * MediaWiki\Request\FauxRequest, errors are thrown and no printing occurs
561 * @param bool $enableWrite Should be set to true if the api may modify data
563 public function __construct( $context = null, $enableWrite = false ) {
564 if ( $context === null ) {
565 $context = RequestContext::getMain();
566 } elseif ( $context instanceof WebRequest ) {
567 // BC for pre-1.19
568 $request = $context;
569 $context = RequestContext::getMain();
571 // We set a derivative context so we can change stuff later
572 $derivativeContext = new DerivativeContext( $context );
573 $this->setContext( $derivativeContext );
575 if ( isset( $request ) ) {
576 $derivativeContext->setRequest( $request );
577 } else {
578 $request = $this->getRequest();
581 $this->mInternalMode = ( $request instanceof FauxRequest );
583 // Special handling for the main module: $parent === $this
584 parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
586 $config = $this->getConfig();
588 if ( !$this->mInternalMode ) {
589 // If we're in a mode that breaks the same-origin policy, strip
590 // user credentials for security.
591 if ( $this->lacksSameOriginSecurity() ) {
592 wfDebug( "API: stripping user credentials when the same-origin policy is not applied" );
593 $user = new User();
594 StubGlobalUser::setUser( $user );
595 $derivativeContext->setUser( $user );
596 $request->response()->header( 'MediaWiki-Login-Suppressed: true' );
600 // TODO inject stuff, see T265644
601 $services = MediaWikiServices::getInstance();
602 $this->mParamValidator = new ApiParamValidator(
603 $this,
604 $services->getObjectFactory()
607 $this->mResult =
608 new ApiResult( $this->getConfig()->get( MainConfigNames::APIMaxResultSize ) );
610 // Setup uselang. This doesn't use $this->getParameter()
611 // because we're not ready to handle errors yet.
612 // Optimisation: Avoid slow getVal(), this isn't user-generated content.
613 $uselang = $request->getRawVal( 'uselang', self::API_DEFAULT_USELANG );
614 if ( $uselang === 'user' ) {
615 // Assume the parent context is going to return the user language
616 // for uselang=user (see T85635).
617 } else {
618 if ( $uselang === 'content' ) {
619 $uselang = $services->getContentLanguage()->getCode();
621 $code = RequestContext::sanitizeLangCode( $uselang );
622 $derivativeContext->setLanguage( $code );
623 if ( !$this->mInternalMode ) {
624 global $wgLang;
625 $wgLang = $derivativeContext->getLanguage();
626 RequestContext::getMain()->setLanguage( $wgLang );
630 // Set up the error formatter. This doesn't use $this->getParameter()
631 // because we're not ready to handle errors yet.
632 // Optimisation: Avoid slow getVal(), this isn't user-generated content.
633 $errorFormat = $request->getRawVal( 'errorformat', 'bc' );
634 $errorLangCode = $request->getRawVal( 'errorlang', 'uselang' );
635 $errorsUseDB = $request->getCheck( 'errorsuselocal' );
636 if ( in_array( $errorFormat, [ 'plaintext', 'wikitext', 'html', 'raw', 'none' ], true ) ) {
637 if ( $errorLangCode === 'uselang' ) {
638 $errorLang = $this->getLanguage();
639 } elseif ( $errorLangCode === 'content' ) {
640 $errorLang = $services->getContentLanguage();
641 } else {
642 $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode );
643 $errorLang = $services->getLanguageFactory()->getLanguage( $errorLangCode );
645 $this->mErrorFormatter = new ApiErrorFormatter(
646 $this->mResult,
647 $errorLang,
648 $errorFormat,
649 $errorsUseDB
651 } else {
652 $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
654 $this->mResult->setErrorFormatter( $this->getErrorFormatter() );
656 $this->mModuleMgr = new ApiModuleManager(
657 $this,
658 $services->getObjectFactory()
660 $this->mModuleMgr->addModules( self::MODULES, 'action' );
661 $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIModules ), 'action' );
662 $this->mModuleMgr->addModules( self::FORMATS, 'format' );
663 $this->mModuleMgr->addModules( $config->get( MainConfigNames::APIFormatModules ), 'format' );
665 $this->getHookRunner()->onApiMain__moduleManager( $this->mModuleMgr );
667 $this->mContinuationManager = null;
668 $this->mEnableWrite = $enableWrite;
672 * Return true if the API was started by other PHP code using MediaWiki\Request\FauxRequest
673 * @return bool
675 public function isInternalMode() {
676 return $this->mInternalMode;
680 * Get the ApiResult object associated with current request
682 * @return ApiResult
684 public function getResult() {
685 return $this->mResult;
689 * Get the security flag for the current request
690 * @return bool
692 public function lacksSameOriginSecurity() {
693 if ( $this->lacksSameOriginSecurity !== null ) {
694 return $this->lacksSameOriginSecurity;
697 $request = $this->getRequest();
699 // JSONP mode
700 if ( $request->getCheck( 'callback' ) ||
701 // Anonymous CORS
702 $request->getVal( 'origin' ) === '*' ||
703 // Header to be used from XMLHTTPRequest when the request might
704 // otherwise be used for XSS.
705 $request->getHeader( 'Treat-as-Untrusted' ) !== false
707 $this->lacksSameOriginSecurity = true;
708 return true;
711 // Allow extensions to override.
712 $this->lacksSameOriginSecurity = !$this->getHookRunner()
713 ->onRequestHasSameOriginSecurity( $request );
714 return $this->lacksSameOriginSecurity;
718 * Get the ApiErrorFormatter object associated with current request
719 * @return ApiErrorFormatter
721 public function getErrorFormatter() {
722 return $this->mErrorFormatter;
726 * @return ApiContinuationManager|null
728 public function getContinuationManager() {
729 return $this->mContinuationManager;
733 * @param ApiContinuationManager|null $manager
735 public function setContinuationManager( ApiContinuationManager $manager = null ) {
736 if ( $manager !== null && $this->mContinuationManager !== null ) {
737 throw new UnexpectedValueException(
738 __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
739 ' when a manager is already set from ' . $this->mContinuationManager->getSource()
742 $this->mContinuationManager = $manager;
746 * Get the parameter validator
747 * @return ApiParamValidator
749 public function getParamValidator(): ApiParamValidator {
750 return $this->mParamValidator;
754 * Get the API module object. Only works after executeAction()
756 * @return ApiBase
758 public function getModule() {
759 return $this->mModule;
763 * Get the result formatter object. Only works after setupExecuteAction()
765 * @return ApiFormatBase
767 public function getPrinter() {
768 return $this->mPrinter;
772 * Set how long the response should be cached.
774 * @param int $maxage
776 public function setCacheMaxAge( $maxage ) {
777 $this->setCacheControl( [
778 'max-age' => $maxage,
779 's-maxage' => $maxage
780 ] );
784 * Set the type of caching headers which will be sent.
786 * @param string $mode One of:
787 * - 'public': Cache this object in public caches, if the maxage or smaxage
788 * parameter is set, or if setCacheMaxAge() was called. If a maximum age is
789 * not provided by any of these means, the object will be private.
790 * - 'private': Cache this object only in private client-side caches.
791 * - 'anon-public-user-private': Make this object cacheable for logged-out
792 * users, but private for logged-in users. IMPORTANT: If this is set, it must be
793 * set consistently for a given URL, it cannot be set differently depending on
794 * things like the contents of the database, or whether the user is logged in.
796 * If the wiki does not allow anonymous users to read it, the mode set here
797 * will be ignored, and private caching headers will always be sent. In other words,
798 * the "public" mode is equivalent to saying that the data sent is as public as a page
799 * view.
801 * For user-dependent data, the private mode should generally be used. The
802 * anon-public-user-private mode should only be used where there is a particularly
803 * good performance reason for caching the anonymous response, but where the
804 * response to logged-in users may differ, or may contain private data.
806 * If this function is never called, then the default will be the private mode.
808 public function setCacheMode( $mode ) {
809 if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
810 wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"" );
812 // Ignore for forwards-compatibility
813 return;
816 if ( !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) ) {
817 // Private wiki, only private headers
818 if ( $mode !== 'private' ) {
819 wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki" );
821 return;
825 if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
826 // User language is used for i18n, so we don't want to publicly
827 // cache. Anons are ok, because if they have non-default language
828 // then there's an appropriate Vary header set by whatever set
829 // their non-default language.
830 wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
831 "'anon-public-user-private' due to uselang=user" );
832 $mode = 'anon-public-user-private';
835 wfDebug( __METHOD__ . ": setting cache mode $mode" );
836 $this->mCacheMode = $mode;
839 public function getCacheMode() {
840 return $this->mCacheMode;
844 * Set directives (key/value pairs) for the Cache-Control header.
845 * Boolean values will be formatted as such, by including or omitting
846 * without an equals sign.
848 * Cache control values set here will only be used if the cache mode is not
849 * private, see setCacheMode().
851 * @param array $directives
853 public function setCacheControl( $directives ) {
854 $this->mCacheControl = $directives + $this->mCacheControl;
858 * Create an instance of an output formatter by its name
860 * @param string $format
862 * @return ApiFormatBase
864 public function createPrinterByName( $format ) {
865 $printer = $this->mModuleMgr->getModule( $format, 'format', /* $ignoreCache */ true );
866 if ( $printer === null ) {
867 $this->dieWithError(
868 [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
872 // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
873 return $printer;
877 * Execute api request. Any errors will be handled if the API was called by the remote client.
879 public function execute() {
880 if ( $this->mInternalMode ) {
881 $this->executeAction();
882 } else {
883 $this->executeActionWithErrorHandling();
888 * Execute an action, and in case of an error, erase whatever partial results
889 * have been accumulated, and replace it with an error message and a help screen.
891 protected function executeActionWithErrorHandling() {
892 // Verify the CORS header before executing the action
893 if ( !$this->handleCORS() ) {
894 // handleCORS() has sent a 403, abort
895 return;
898 // Exit here if the request method was OPTIONS
899 // (assume there will be a followup GET or POST)
900 if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
901 return;
904 // In case an error occurs during data output,
905 // clear the output buffer and print just the error information
906 $obLevel = ob_get_level();
907 ob_start();
909 $t = microtime( true );
910 $isError = false;
911 try {
912 $this->executeAction();
913 $runTime = microtime( true ) - $t;
914 $this->logRequest( $runTime );
915 MediaWikiServices::getInstance()->getStatsdDataFactory()->timing(
916 'api.' . $this->mModule->getModuleName() . '.executeTiming', 1000 * $runTime
918 } catch ( Throwable $e ) {
919 $this->handleException( $e );
920 $this->logRequest( microtime( true ) - $t, $e );
921 $isError = true;
924 // Disable the client cache on the output so that BlockManager::trackBlockWithCookie is executed
925 // as part of MediaWiki::preOutputCommit().
926 if (
927 $this->mCacheMode === 'private'
928 || (
929 $this->mCacheMode === 'anon-public-user-private'
930 && SessionManager::getGlobalSession()->isPersistent()
933 $this->getContext()->getOutput()->disableClientCache();
934 $this->getContext()->getOutput()->considerCacheSettingsFinal();
937 // Commit DBs and send any related cookies and headers
938 MediaWiki::preOutputCommit( $this->getContext() );
940 // Send cache headers after any code which might generate an error, to
941 // avoid sending public cache headers for errors.
942 $this->sendCacheHeaders( $isError );
944 // Executing the action might have already messed with the output
945 // buffers.
946 while ( ob_get_level() > $obLevel ) {
947 ob_end_flush();
952 * Handle a throwable as an API response
954 * @since 1.23
955 * @param Throwable $e
957 protected function handleException( Throwable $e ) {
958 // T65145: Rollback any open database transactions
959 if ( !$e instanceof ApiUsageException ) {
960 // ApiUsageExceptions are intentional, so don't rollback if that's the case
961 MWExceptionHandler::rollbackPrimaryChangesAndLog(
963 MWExceptionHandler::CAUGHT_BY_ENTRYPOINT
967 // Allow extra cleanup and logging
968 $this->getHookRunner()->onApiMain__onException( $this, $e );
970 // Handle any kind of exception by outputting properly formatted error message.
971 // If this fails, an unhandled exception should be thrown so that global error
972 // handler will process and log it.
974 $errCodes = $this->substituteResultWithError( $e );
976 // Error results should not be cached
977 $this->setCacheMode( 'private' );
979 $response = $this->getRequest()->response();
980 $headerStr = 'MediaWiki-API-Error: ' . implode( ', ', $errCodes );
981 $response->header( $headerStr );
983 // Reset and print just the error message
984 ob_clean();
986 // Printer may not be initialized if the extractRequestParams() fails for the main module
987 $this->createErrorPrinter();
989 // Get desired HTTP code from an ApiUsageException. Don't use codes from other
990 // exception types, as they are unlikely to be intended as an HTTP code.
991 $httpCode = $e instanceof ApiUsageException ? $e->getCode() : 0;
993 $failed = false;
994 try {
995 $this->printResult( $httpCode );
996 } catch ( ApiUsageException $ex ) {
997 // The error printer itself is failing. Try suppressing its request
998 // parameters and redo.
999 $failed = true;
1000 $this->addWarning( 'apiwarn-errorprinterfailed' );
1001 foreach ( $ex->getStatusValue()->getErrors() as $error ) {
1002 try {
1003 $this->mPrinter->addWarning( $error );
1004 } catch ( Throwable $ex2 ) {
1005 // WTF?
1006 $this->addWarning( $error );
1010 if ( $failed ) {
1011 $this->mPrinter = null;
1012 $this->createErrorPrinter();
1013 // @phan-suppress-next-line PhanNonClassMethodCall False positive
1014 $this->mPrinter->forceDefaultParams();
1015 if ( $httpCode ) {
1016 $response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200
1018 $this->printResult( $httpCode );
1023 * Handle a throwable from the ApiBeforeMain hook.
1025 * This tries to print the throwable as an API response, to be more
1026 * friendly to clients. If it fails, it will rethrow the throwable.
1028 * @since 1.23
1029 * @param Throwable $e
1030 * @throws Throwable
1032 public static function handleApiBeforeMainException( Throwable $e ) {
1033 ob_start();
1035 try {
1036 $main = new self( RequestContext::getMain(), false );
1037 $main->handleException( $e );
1038 $main->logRequest( 0, $e );
1039 } catch ( Throwable $e2 ) {
1040 // Nope, even that didn't work. Punt.
1041 throw $e;
1044 // Reset cache headers
1045 $main->sendCacheHeaders( true );
1047 ob_end_flush();
1051 * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
1053 * If no origin parameter is present, nothing happens.
1054 * If an origin parameter is present but doesn't match the Origin header, a 403 status code
1055 * is set and false is returned.
1056 * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
1057 * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
1058 * headers are set.
1059 * https://www.w3.org/TR/cors/#resource-requests
1060 * https://www.w3.org/TR/cors/#resource-preflight-requests
1062 * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
1064 protected function handleCORS() {
1065 $originParam = $this->getParameter( 'origin' ); // defaults to null
1066 if ( $originParam === null ) {
1067 // No origin parameter, nothing to do
1068 return true;
1071 $request = $this->getRequest();
1072 $response = $request->response();
1074 $allowTiming = false;
1075 $varyOrigin = true;
1077 if ( $originParam === '*' ) {
1078 // Request for anonymous CORS
1079 // Technically we should check for the presence of an Origin header
1080 // and not process it as CORS if it's not set, but that would
1081 // require us to vary on Origin for all 'origin=*' requests which
1082 // we don't want to do.
1083 $matchedOrigin = true;
1084 $allowOrigin = '*';
1085 $allowCredentials = 'false';
1086 $varyOrigin = false; // No need to vary
1087 } else {
1088 // Non-anonymous CORS, check we allow the domain
1090 // Origin: header is a space-separated list of origins, check all of them
1091 $originHeader = $request->getHeader( 'Origin' );
1092 if ( $originHeader === false ) {
1093 $origins = [];
1094 } else {
1095 $originHeader = trim( $originHeader );
1096 $origins = preg_split( '/\s+/', $originHeader );
1099 if ( !in_array( $originParam, $origins ) ) {
1100 // origin parameter set but incorrect
1101 // Send a 403 response
1102 $response->statusHeader( 403 );
1103 $response->header( 'Cache-Control: no-cache' );
1104 echo "'origin' parameter does not match Origin header\n";
1106 return false;
1109 $config = $this->getConfig();
1110 $origin = Origin::parseHeaderList( $origins );
1111 $matchedOrigin = $origin->match(
1112 $config->get( MainConfigNames::CrossSiteAJAXdomains ),
1113 $config->get( MainConfigNames::CrossSiteAJAXdomainExceptions )
1116 $allowOrigin = $originHeader;
1117 $allowCredentials = 'true';
1118 $allowTiming = $originHeader;
1121 if ( $matchedOrigin ) {
1122 $requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
1123 $preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
1124 if ( $preflight ) {
1125 // We allow the actual request to send the following headers
1126 $requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
1127 $allowedHeaders = $this->getConfig()->get( MainConfigNames::AllowedCorsHeaders );
1128 if ( $requestedHeaders !== false ) {
1129 if ( !self::matchRequestedHeaders( $requestedHeaders, $allowedHeaders ) ) {
1130 $response->header( 'MediaWiki-CORS-Rejection: Unsupported header requested in preflight' );
1131 return true;
1133 $response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
1136 // We only allow the actual request to be GET, POST, or HEAD
1137 $response->header( 'Access-Control-Allow-Methods: POST, GET, HEAD' );
1140 $response->header( "Access-Control-Allow-Origin: $allowOrigin" );
1141 $response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
1142 // https://www.w3.org/TR/resource-timing/#timing-allow-origin
1143 if ( $allowTiming !== false ) {
1144 $response->header( "Timing-Allow-Origin: $allowTiming" );
1147 if ( !$preflight ) {
1148 $response->header(
1149 'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag, '
1150 . 'MediaWiki-Login-Suppressed'
1153 } else {
1154 $response->header( 'MediaWiki-CORS-Rejection: Origin mismatch' );
1157 if ( $varyOrigin ) {
1158 $this->getOutput()->addVaryHeader( 'Origin' );
1161 return true;
1165 * Attempt to validate the value of Access-Control-Request-Headers against a list
1166 * of headers that we allow the follow up request to send.
1168 * @param string $requestedHeaders Comma separated list of HTTP headers
1169 * @param string[] $allowedHeaders List of allowed HTTP headers
1170 * @return bool True if all requested headers are in the list of allowed headers
1172 protected static function matchRequestedHeaders( $requestedHeaders, $allowedHeaders ) {
1173 if ( trim( $requestedHeaders ) === '' ) {
1174 return true;
1176 $requestedHeaders = explode( ',', $requestedHeaders );
1177 $allowedHeaders = array_change_key_case(
1178 array_fill_keys( $allowedHeaders, true ), CASE_LOWER );
1179 foreach ( $requestedHeaders as $rHeader ) {
1180 $rHeader = strtolower( trim( $rHeader ) );
1181 if ( !isset( $allowedHeaders[$rHeader] ) ) {
1182 LoggerFactory::getInstance( 'api-warning' )->warning(
1183 'CORS preflight failed on requested header: {header}', [
1184 'header' => $rHeader
1187 return false;
1190 return true;
1194 * Send caching headers
1195 * @param bool $isError Whether an error response is being output
1196 * @since 1.26 added $isError parameter
1198 protected function sendCacheHeaders( $isError ) {
1199 $response = $this->getRequest()->response();
1200 $out = $this->getOutput();
1202 $out->addVaryHeader( 'Treat-as-Untrusted' );
1204 $config = $this->getConfig();
1206 if ( $config->get( MainConfigNames::VaryOnXFP ) ) {
1207 $out->addVaryHeader( 'X-Forwarded-Proto' );
1210 if ( !$isError && $this->mModule &&
1211 ( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
1213 $etag = $this->mModule->getConditionalRequestData( 'etag' );
1214 if ( $etag !== null ) {
1215 $response->header( "ETag: $etag" );
1217 $lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
1218 if ( $lastMod !== null ) {
1219 $response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
1223 // The logic should be:
1224 // $this->mCacheControl['max-age'] is set?
1225 // Use it, the module knows better than our guess.
1226 // !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
1227 // Use 0 because we can guess caching is probably the wrong thing to do.
1228 // Use $this->getParameter( 'maxage' ), which already defaults to 0.
1229 $maxage = 0;
1230 if ( isset( $this->mCacheControl['max-age'] ) ) {
1231 $maxage = $this->mCacheControl['max-age'];
1232 } elseif ( ( $this->mModule && !$this->mModule->isWriteMode() ) ||
1233 $this->mCacheMode !== 'private'
1235 $maxage = $this->getParameter( 'maxage' );
1237 $privateCache = 'private, must-revalidate, max-age=' . $maxage;
1239 if ( $this->mCacheMode == 'private' ) {
1240 $response->header( "Cache-Control: $privateCache" );
1241 return;
1244 if ( $this->mCacheMode == 'anon-public-user-private' ) {
1245 $out->addVaryHeader( 'Cookie' );
1246 $response->header( $out->getVaryHeader() );
1247 if ( SessionManager::getGlobalSession()->isPersistent() ) {
1248 // Logged in or otherwise has session (e.g. anonymous users who have edited)
1249 // Mark request private
1250 $response->header( "Cache-Control: $privateCache" );
1252 return;
1253 } // else anonymous, send public headers below
1256 // Send public headers
1257 $response->header( $out->getVaryHeader() );
1259 // If nobody called setCacheMaxAge(), use the (s)maxage parameters
1260 if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
1261 $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
1263 if ( !isset( $this->mCacheControl['max-age'] ) ) {
1264 $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
1267 if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
1268 // Public cache not requested
1269 // Sending a Vary header in this case is harmless, and protects us
1270 // against conditional calls of setCacheMaxAge().
1271 $response->header( "Cache-Control: $privateCache" );
1273 return;
1276 $this->mCacheControl['public'] = true;
1278 // Send an Expires header
1279 $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
1280 $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
1281 $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
1283 // Construct the Cache-Control header
1284 $ccHeader = '';
1285 $separator = '';
1286 foreach ( $this->mCacheControl as $name => $value ) {
1287 if ( is_bool( $value ) ) {
1288 if ( $value ) {
1289 $ccHeader .= $separator . $name;
1290 $separator = ', ';
1292 } else {
1293 $ccHeader .= $separator . "$name=$value";
1294 $separator = ', ';
1298 $response->header( "Cache-Control: $ccHeader" );
1302 * Create the printer for error output
1304 private function createErrorPrinter() {
1305 if ( !isset( $this->mPrinter ) ) {
1306 $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
1307 if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
1308 $value = self::API_DEFAULT_FORMAT;
1310 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable getVal does not return null here
1311 $this->mPrinter = $this->createPrinterByName( $value );
1314 // Printer may not be able to handle errors. This is particularly
1315 // likely if the module returns something for getCustomPrinter().
1316 if ( !$this->mPrinter->canPrintErrors() ) {
1317 $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
1322 * Create an error message for the given throwable.
1324 * If an ApiUsageException, errors/warnings will be extracted from the
1325 * embedded StatusValue.
1327 * Any other throwable will be returned with a generic code and wrapper
1328 * text around the throwable's (presumably English) message as a single
1329 * error (no warnings).
1331 * @param Throwable $e
1332 * @param string $type 'error' or 'warning'
1333 * @return ApiMessage[]
1334 * @since 1.27
1336 protected function errorMessagesFromException( Throwable $e, $type = 'error' ) {
1337 $messages = [];
1338 if ( $e instanceof ApiUsageException ) {
1339 foreach ( $e->getStatusValue()->getErrorsByType( $type ) as $error ) {
1340 $messages[] = ApiMessage::create( $error );
1342 } elseif ( $type !== 'error' ) {
1343 // None of the rest have any messages for non-error types
1344 } else {
1345 // TODO: Avoid embedding arbitrary class names in the error code.
1346 $class = preg_replace( '#^Wikimedia\\\\Rdbms\\\\#', '', get_class( $e ) );
1347 $code = 'internal_api_error_' . $class;
1348 $data = [ 'errorclass' => get_class( $e ) ];
1349 if ( MWExceptionRenderer::shouldShowExceptionDetails() ) {
1350 if ( $e instanceof ILocalizedException ) {
1351 $msg = $e->getMessageObject();
1352 } elseif ( $e instanceof MessageSpecifier ) {
1353 $msg = Message::newFromSpecifier( $e );
1354 } else {
1355 $msg = wfEscapeWikiText( $e->getMessage() );
1357 $params = [ 'apierror-exceptioncaught', WebRequest::getRequestId(), $msg ];
1358 } else {
1359 $params = [ 'apierror-exceptioncaughttype', WebRequest::getRequestId(), get_class( $e ) ];
1362 $messages[] = ApiMessage::create( $params, $code, $data );
1364 return $messages;
1368 * Replace the result data with the information about a throwable.
1369 * @param Throwable $e
1370 * @return string[] Error codes
1372 protected function substituteResultWithError( Throwable $e ) {
1373 $result = $this->getResult();
1374 $formatter = $this->getErrorFormatter();
1375 $config = $this->getConfig();
1376 $errorCodes = [];
1378 // Remember existing warnings and errors across the reset
1379 $errors = $result->getResultData( [ 'errors' ] );
1380 $warnings = $result->getResultData( [ 'warnings' ] );
1381 $result->reset();
1382 if ( $warnings !== null ) {
1383 $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
1385 if ( $errors !== null ) {
1386 $result->addValue( null, 'errors', $errors, ApiResult::NO_SIZE_CHECK );
1388 // Collect the copied error codes for the return value
1389 foreach ( $errors as $error ) {
1390 if ( isset( $error['code'] ) ) {
1391 $errorCodes[$error['code']] = true;
1396 // Add errors from the exception
1397 $modulePath = $e instanceof ApiUsageException ? $e->getModulePath() : null;
1398 foreach ( $this->errorMessagesFromException( $e, 'error' ) as $msg ) {
1399 if ( ApiErrorFormatter::isValidApiCode( $msg->getApiCode() ) ) {
1400 $errorCodes[$msg->getApiCode()] = true;
1401 } else {
1402 LoggerFactory::getInstance( 'api-warning' )->error( 'Invalid API error code "{code}"', [
1403 'code' => $msg->getApiCode(),
1404 'exception' => $e,
1405 ] );
1406 $errorCodes['<invalid-code>'] = true;
1408 $formatter->addError( $modulePath, $msg );
1410 foreach ( $this->errorMessagesFromException( $e, 'warning' ) as $msg ) {
1411 $formatter->addWarning( $modulePath, $msg );
1414 // Add additional data. Path depends on whether we're in BC mode or not.
1415 // Data depends on the type of exception.
1416 if ( $formatter instanceof ApiErrorFormatter_BackCompat ) {
1417 $path = [ 'error' ];
1418 } else {
1419 $path = null;
1421 if ( $e instanceof ApiUsageException ) {
1422 $link = (string)MediaWikiServices::getInstance()->getUrlUtils()->expand( wfScript( 'api' ) );
1423 $result->addContentValue(
1424 $path,
1425 'docref',
1426 trim(
1427 $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text()
1428 . ' '
1429 . $this->msg( 'api-usage-mailinglist-ref' )->inLanguage( $formatter->getLanguage() )->text()
1432 } elseif ( $config->get( MainConfigNames::ShowExceptionDetails ) ) {
1433 $result->addContentValue(
1434 $path,
1435 'trace',
1436 $this->msg( 'api-exception-trace',
1437 get_class( $e ),
1438 $e->getFile(),
1439 $e->getLine(),
1440 MWExceptionHandler::getRedactedTraceAsString( $e )
1441 )->inLanguage( $formatter->getLanguage() )->text()
1445 // Add the id and such
1446 $this->addRequestedFields( [ 'servedby' ] );
1448 return array_keys( $errorCodes );
1452 * Add requested fields to the result
1453 * @param string[] $force Which fields to force even if not requested. Accepted values are:
1454 * - servedby
1456 protected function addRequestedFields( $force = [] ) {
1457 $result = $this->getResult();
1459 $requestid = $this->getParameter( 'requestid' );
1460 if ( $requestid !== null ) {
1461 $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
1464 if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) && (
1465 in_array( 'servedby', $force, true ) || $this->getParameter( 'servedby' )
1466 ) ) {
1467 $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
1470 if ( $this->getParameter( 'curtimestamp' ) ) {
1471 $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601 ), ApiResult::NO_SIZE_CHECK );
1474 if ( $this->getParameter( 'responselanginfo' ) ) {
1475 $result->addValue(
1476 null,
1477 'uselang',
1478 $this->getLanguage()->getCode(),
1479 ApiResult::NO_SIZE_CHECK
1481 $result->addValue(
1482 null,
1483 'errorlang',
1484 $this->getErrorFormatter()->getLanguage()->getCode(),
1485 ApiResult::NO_SIZE_CHECK
1491 * Set up for the execution.
1492 * @return array
1494 protected function setupExecuteAction() {
1495 $this->addRequestedFields();
1497 $params = $this->extractRequestParams();
1498 $this->mAction = $params['action'];
1500 return $params;
1504 * Set up the module for response
1505 * @return ApiBase The module that will handle this action
1506 * @throws ApiUsageException
1508 protected function setupModule() {
1509 // Instantiate the module requested by the user
1510 $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
1511 if ( $module === null ) {
1512 // Probably can't happen
1513 // @codeCoverageIgnoreStart
1514 $this->dieWithError(
1515 [ 'apierror-unknownaction', wfEscapeWikiText( $this->mAction ) ],
1516 'unknown_action'
1518 // @codeCoverageIgnoreEnd
1520 $moduleParams = $module->extractRequestParams();
1522 // Check token, if necessary
1523 if ( $module->needsToken() === true ) {
1524 throw new LogicException(
1525 "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
1526 'See documentation for ApiBase::needsToken for details.'
1529 if ( $module->needsToken() ) {
1530 if ( !$module->mustBePosted() ) {
1531 throw new LogicException(
1532 "Module '{$module->getModuleName()}' must require POST to use tokens."
1536 if ( !isset( $moduleParams['token'] ) ) {
1537 // Probably can't happen
1538 // @codeCoverageIgnoreStart
1539 $module->dieWithError( [ 'apierror-missingparam', 'token' ] );
1540 // @codeCoverageIgnoreEnd
1543 $module->requirePostedParameters( [ 'token' ] );
1545 if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
1546 $module->dieWithError( 'apierror-badtoken' );
1550 // @phan-suppress-next-line PhanTypeMismatchReturnNullable T240141
1551 return $module;
1555 * @return array
1557 private function getMaxLag() {
1558 $services = MediaWikiServices::getInstance();
1559 $dbLag = $services->getDBLoadBalancer()->getMaxLag();
1560 $lagInfo = [
1561 'host' => $dbLag[0],
1562 'lag' => $dbLag[1],
1563 'type' => 'db'
1566 $jobQueueLagFactor =
1567 $this->getConfig()->get( MainConfigNames::JobQueueIncludeInMaxLagFactor );
1568 if ( $jobQueueLagFactor ) {
1569 // Turn total number of jobs into seconds by using the configured value
1570 $totalJobs = array_sum( $services->getJobQueueGroup()->getQueueSizes() );
1571 $jobQueueLag = $totalJobs / (float)$jobQueueLagFactor;
1572 if ( $jobQueueLag > $lagInfo['lag'] ) {
1573 $lagInfo = [
1574 'host' => wfHostname(), // XXX: Is there a better value that could be used?
1575 'lag' => $jobQueueLag,
1576 'type' => 'jobqueue',
1577 'jobs' => $totalJobs,
1582 $this->getHookRunner()->onApiMaxLagInfo( $lagInfo );
1584 return $lagInfo;
1588 * Check the max lag if necessary
1589 * @param ApiBase $module Api module being used
1590 * @param array $params Array an array containing the request parameters.
1591 * @return bool True on success, false should exit immediately
1593 protected function checkMaxLag( $module, $params ) {
1594 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
1595 $maxLag = $params['maxlag'];
1596 $lagInfo = $this->getMaxLag();
1597 if ( $lagInfo['lag'] > $maxLag ) {
1598 $response = $this->getRequest()->response();
1600 $response->header( 'Retry-After: ' . max( (int)$maxLag, 5 ) );
1601 $response->header( 'X-Database-Lag: ' . (int)$lagInfo['lag'] );
1603 if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) {
1604 $this->dieWithError(
1605 [ 'apierror-maxlag', $lagInfo['lag'], $lagInfo['host'] ],
1606 'maxlag',
1607 $lagInfo
1611 $this->dieWithError( [ 'apierror-maxlag-generic', $lagInfo['lag'] ], 'maxlag', $lagInfo );
1615 return true;
1619 * Check selected RFC 7232 precondition headers
1621 * RFC 7232 envisions a particular model where you send your request to "a
1622 * resource", and for write requests that you can read "the resource" by
1623 * changing the method to GET. When the API receives a GET request, it
1624 * works out even though "the resource" from RFC 7232's perspective might
1625 * be many resources from MediaWiki's perspective. But it totally fails for
1626 * a POST, since what HTTP sees as "the resource" is probably just
1627 * "/api.php" with all the interesting bits in the body.
1629 * Therefore, we only support RFC 7232 precondition headers for GET (and
1630 * HEAD). That means we don't need to bother with If-Match and
1631 * If-Unmodified-Since since they only apply to modification requests.
1633 * And since we don't support Range, If-Range is ignored too.
1635 * @since 1.26
1636 * @param ApiBase $module Api module being used
1637 * @return bool True on success, false should exit immediately
1639 protected function checkConditionalRequestHeaders( $module ) {
1640 if ( $this->mInternalMode ) {
1641 // No headers to check in internal mode
1642 return true;
1645 if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) {
1646 // Don't check POSTs
1647 return true;
1650 $return304 = false;
1652 $ifNoneMatch = array_diff(
1653 $this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: [],
1654 [ '' ]
1656 if ( $ifNoneMatch ) {
1657 // @phan-suppress-next-line PhanImpossibleTypeComparison
1658 if ( $ifNoneMatch === [ '*' ] ) {
1659 // API responses always "exist"
1660 $etag = '*';
1661 } else {
1662 $etag = $module->getConditionalRequestData( 'etag' );
1665 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $etag is declared when $ifNoneMatch is true
1666 if ( $ifNoneMatch && $etag !== null ) {
1667 $test = str_starts_with( $etag, 'W/' ) ? substr( $etag, 2 ) : $etag;
1668 $match = array_map( static function ( $s ) {
1669 return str_starts_with( $s, 'W/' ) ? substr( $s, 2 ) : $s;
1670 }, $ifNoneMatch );
1671 $return304 = in_array( $test, $match, true );
1672 } else {
1673 $value = trim( $this->getRequest()->getHeader( 'If-Modified-Since' ) );
1675 // Some old browsers sends sizes after the date, like this:
1676 // Wed, 20 Aug 2003 06:51:19 GMT; length=5202
1677 // Ignore that.
1678 $i = strpos( $value, ';' );
1679 if ( $i !== false ) {
1680 $value = trim( substr( $value, 0, $i ) );
1683 if ( $value !== '' ) {
1684 try {
1685 $ts = new MWTimestamp( $value );
1686 if (
1687 // RFC 7231 IMF-fixdate
1688 $ts->getTimestamp( TS_RFC2822 ) === $value ||
1689 // RFC 850
1690 $ts->format( 'l, d-M-y H:i:s' ) . ' GMT' === $value ||
1691 // asctime (with and without space-padded day)
1692 $ts->format( 'D M j H:i:s Y' ) === $value ||
1693 $ts->format( 'D M j H:i:s Y' ) === $value
1695 $config = $this->getConfig();
1696 $lastMod = $module->getConditionalRequestData( 'last-modified' );
1697 if ( $lastMod !== null ) {
1698 // Mix in some MediaWiki modification times
1699 $modifiedTimes = [
1700 'page' => $lastMod,
1701 'user' => $this->getUser()->getTouched(),
1702 'epoch' => $config->get( MainConfigNames::CacheEpoch ),
1705 if ( $config->get( MainConfigNames::UseCdn ) ) {
1706 // T46570: the core page itself may not change, but resources might
1707 $modifiedTimes['sepoch'] = wfTimestamp(
1708 TS_MW, time() - $config->get( MainConfigNames::CdnMaxAge )
1711 $this->getHookRunner()->onOutputPageCheckLastModified( $modifiedTimes, $this->getOutput() );
1712 $lastMod = max( $modifiedTimes );
1713 $return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
1716 } catch ( TimestampException $e ) {
1717 // Invalid timestamp, ignore it
1722 if ( $return304 ) {
1723 $this->getRequest()->response()->statusHeader( 304 );
1725 // Avoid outputting the compressed representation of a zero-length body
1726 AtEase::suppressWarnings();
1727 // @phan-suppress-next-line PhanTypeMismatchArgumentInternal Scalar okay with php8.1
1728 ini_set( 'zlib.output_compression', 0 );
1729 AtEase::restoreWarnings();
1730 wfResetOutputBuffers( false );
1732 return false;
1735 return true;
1739 * Check for sufficient permissions to execute
1740 * @param ApiBase $module An Api module
1742 protected function checkExecutePermissions( $module ) {
1743 $user = $this->getUser();
1744 if ( $module->isReadMode() && !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) &&
1745 !$this->getAuthority()->isAllowed( 'read' )
1747 $this->dieWithError( 'apierror-readapidenied' );
1750 if ( $module->isWriteMode() ) {
1751 if ( !$this->mEnableWrite ) {
1752 $this->dieWithError( 'apierror-noapiwrite' );
1753 } elseif ( !$this->getAuthority()->isAllowed( 'writeapi' ) ) {
1754 $this->dieWithError( 'apierror-writeapidenied' );
1755 } elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) {
1756 $this->dieWithError( 'apierror-promised-nonwrite-api' );
1759 $this->checkReadOnly( $module );
1762 // Allow extensions to stop execution for arbitrary reasons.
1763 // TODO: change hook to accept Authority
1764 $message = 'hookaborted';
1765 if ( !$this->getHookRunner()->onApiCheckCanExecute( $module, $user, $message ) ) {
1766 $this->dieWithError( $message );
1771 * Check if the DB is read-only for this user
1772 * @param ApiBase $module An Api module
1774 protected function checkReadOnly( $module ) {
1775 if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
1776 $this->dieReadOnly();
1779 if ( $module->isWriteMode()
1780 && $this->getUser()->isBot()
1781 && MediaWikiServices::getInstance()->getDBLoadBalancer()->hasReplicaServers()
1783 $this->checkBotReadOnly();
1788 * Check whether we are readonly for bots
1790 private function checkBotReadOnly() {
1791 // Figure out how many servers have passed the lag threshold
1792 $numLagged = 0;
1793 $lagLimit = $this->getConfig()->get( MainConfigNames::APIMaxLagThreshold );
1794 $laggedServers = [];
1795 $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
1796 foreach ( $loadBalancer->getLagTimes() as $serverIndex => $lag ) {
1797 if ( $lag > $lagLimit ) {
1798 ++$numLagged;
1799 $laggedServers[] = $loadBalancer->getServerName( $serverIndex ) . " ({$lag}s)";
1803 // If a majority of replica DBs are too lagged then disallow writes
1804 $replicaCount = $loadBalancer->getServerCount() - 1;
1805 if ( $numLagged >= ceil( $replicaCount / 2 ) ) {
1806 $laggedServers = implode( ', ', $laggedServers );
1807 wfDebugLog(
1808 'api-readonly', // Deprecate this channel in favor of api-warning?
1809 "Api request failed as read only because the following DBs are lagged: $laggedServers"
1811 LoggerFactory::getInstance( 'api-warning' )->warning(
1812 "Api request failed as read only because the following DBs are lagged: {laggeddbs}", [
1813 'laggeddbs' => $laggedServers,
1817 $this->dieWithError(
1818 'readonly_lag',
1819 'readonly',
1820 [ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ]
1826 * Check asserts of the user's rights
1827 * @param array $params
1829 protected function checkAsserts( $params ) {
1830 if ( isset( $params['assert'] ) ) {
1831 $user = $this->getUser();
1832 switch ( $params['assert'] ) {
1833 case 'anon':
1834 if ( $user->isRegistered() ) {
1835 $this->dieWithError( 'apierror-assertanonfailed' );
1837 break;
1838 case 'user':
1839 if ( !$user->isRegistered() ) {
1840 $this->dieWithError( 'apierror-assertuserfailed' );
1842 break;
1843 case 'bot':
1844 if ( !$this->getAuthority()->isAllowed( 'bot' ) ) {
1845 $this->dieWithError( 'apierror-assertbotfailed' );
1847 break;
1850 if ( isset( $params['assertuser'] ) ) {
1851 // TODO inject stuff, see T265644
1852 $assertUser = MediaWikiServices::getInstance()->getUserFactory()
1853 ->newFromName( $params['assertuser'], UserRigorOptions::RIGOR_NONE );
1854 if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) {
1855 $this->dieWithError(
1856 [ 'apierror-assertnameduserfailed', wfEscapeWikiText( $params['assertuser'] ) ]
1863 * Check POST for external response and setup result printer
1864 * @param ApiBase $module An Api module
1865 * @param array $params An array with the request parameters
1867 protected function setupExternalResponse( $module, $params ) {
1868 $validMethods = [ 'GET', 'HEAD', 'POST', 'OPTIONS' ];
1869 $request = $this->getRequest();
1871 if ( !in_array( $request->getMethod(), $validMethods ) ) {
1872 $this->dieWithError( 'apierror-invalidmethod', null, null, 405 );
1875 if ( !$request->wasPosted() && $module->mustBePosted() ) {
1876 // Module requires POST. GET request might still be allowed
1877 // if $wgDebugApi is true, otherwise fail.
1878 $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] );
1881 if ( $request->wasPosted() && !$request->getHeader( 'Content-Type' ) ) {
1882 $this->addDeprecation(
1883 'apiwarn-deprecation-post-without-content-type', 'post-without-content-type'
1887 // See if custom printer is used
1888 $this->mPrinter = $module->getCustomPrinter() ??
1889 // Create an appropriate printer if not set
1890 $this->createPrinterByName( $params['format'] );
1892 if ( $request->getProtocol() === 'http' &&
1894 $this->getConfig()->get( MainConfigNames::ForceHTTPS ) ||
1895 $request->getSession()->shouldForceHTTPS() ||
1896 $this->getUser()->requiresHTTPS()
1899 $this->addDeprecation( 'apiwarn-deprecation-httpsexpected', 'https-expected' );
1904 * Execute the actual module, without any error handling
1906 protected function executeAction() {
1907 $params = $this->setupExecuteAction();
1909 // Check asserts early so e.g. errors in parsing a module's parameters due to being
1910 // logged out don't override the client's intended "am I logged in?" check.
1911 $this->checkAsserts( $params );
1913 $module = $this->setupModule();
1914 $this->mModule = $module;
1916 if ( !$this->mInternalMode ) {
1917 ProfilingContext::singleton()->init( MW_ENTRY_POINT, $module->getModuleName() );
1918 $this->setRequestExpectations( $module );
1921 $this->checkExecutePermissions( $module );
1923 if ( !$this->checkMaxLag( $module, $params ) ) {
1924 return;
1927 if ( !$this->checkConditionalRequestHeaders( $module ) ) {
1928 return;
1931 if ( !$this->mInternalMode ) {
1932 $this->setupExternalResponse( $module, $params );
1935 $module->execute();
1936 $this->getHookRunner()->onAPIAfterExecute( $module );
1938 $this->reportUnusedParams();
1940 if ( !$this->mInternalMode ) {
1941 MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
1943 $this->printResult();
1948 * Set database connection, query, and write expectations given this module request
1949 * @param ApiBase $module
1951 protected function setRequestExpectations( ApiBase $module ) {
1952 $request = $this->getRequest();
1954 $trxLimits = $this->getConfig()->get( MainConfigNames::TrxProfilerLimits );
1955 $trxProfiler = Profiler::instance()->getTransactionProfiler();
1956 $trxProfiler->setLogger( LoggerFactory::getInstance( 'rdbms' ) );
1957 $statsFactory = MediaWikiServices::getInstance()->getStatsdDataFactory();
1958 $trxProfiler->setStatsdDataFactory( $statsFactory );
1959 $trxProfiler->setRequestMethod( $request->getMethod() );
1960 if ( $request->hasSafeMethod() ) {
1961 $trxProfiler->setExpectations( $trxLimits['GET'], __METHOD__ );
1962 } elseif ( $request->wasPosted() && !$module->isWriteMode() ) {
1963 $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ );
1964 } else {
1965 $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ );
1970 * Log the preceding request
1971 * @param float $time Time in seconds
1972 * @param Throwable|null $e Throwable caught while processing the request
1974 protected function logRequest( $time, Throwable $e = null ) {
1975 $request = $this->getRequest();
1977 $user = $this->getUser();
1978 $performer = [
1979 'user_text' => $user->getName(),
1981 if ( $user->isRegistered() ) {
1982 $performer['user_id'] = $user->getId();
1984 $logCtx = [
1985 // https://gerrit.wikimedia.org/g/mediawiki/event-schemas/+/master/jsonschema/mediawiki/api/request
1986 '$schema' => '/mediawiki/api/request/1.0.0',
1987 'meta' => [
1988 'request_id' => WebRequest::getRequestId(),
1989 'id' => MediaWikiServices::getInstance()
1990 ->getGlobalIdGenerator()->newUUIDv4(),
1991 'dt' => wfTimestamp( TS_ISO_8601 ),
1992 'domain' => $this->getConfig()->get( MainConfigNames::ServerName ),
1993 // If using the EventBus extension (as intended) with this log channel,
1994 // this stream name will map to a Kafka topic.
1995 'stream' => 'mediawiki.api-request'
1997 'http' => [
1998 'method' => $request->getMethod(),
1999 'client_ip' => $request->getIP()
2001 'performer' => $performer,
2002 'database' => WikiMap::getCurrentWikiDbDomain()->getId(),
2003 'backend_time_ms' => (int)round( $time * 1000 ),
2006 // If set, these headers will be logged in http.request_headers.
2007 $httpRequestHeadersToLog = [ 'accept-language', 'referer', 'user-agent' ];
2008 foreach ( $httpRequestHeadersToLog as $header ) {
2009 if ( $request->getHeader( $header ) ) {
2010 // Set the header in http.request_headers
2011 $logCtx['http']['request_headers'][$header] = $request->getHeader( $header );
2015 if ( $e ) {
2016 $logCtx['api_error_codes'] = [];
2017 foreach ( $this->errorMessagesFromException( $e ) as $msg ) {
2018 $logCtx['api_error_codes'][] = $msg->getApiCode();
2022 // Construct space separated message for 'api' log channel
2023 $msg = "API {$request->getMethod()} " .
2024 wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
2025 " {$logCtx['http']['client_ip']} " .
2026 "T={$logCtx['backend_time_ms']}ms";
2028 $sensitive = array_fill_keys( $this->getSensitiveParams(), true );
2029 foreach ( $this->getParamsUsed() as $name ) {
2030 $value = $request->getVal( $name );
2031 if ( $value === null ) {
2032 continue;
2035 if ( isset( $sensitive[$name] ) ) {
2036 $value = '[redacted]';
2037 $encValue = '[redacted]';
2038 } elseif ( strlen( $value ) > 256 ) {
2039 $value = substr( $value, 0, 256 );
2040 $encValue = $this->encodeRequestLogValue( $value ) . '[...]';
2041 } else {
2042 $encValue = $this->encodeRequestLogValue( $value );
2045 $logCtx['params'][$name] = $value;
2046 $msg .= " {$name}={$encValue}";
2049 // Log an unstructured message to the api channel.
2050 wfDebugLog( 'api', $msg, 'private' );
2052 // The api-request channel a structured data log channel.
2053 wfDebugLog( 'api-request', '', 'private', $logCtx );
2057 * Encode a value in a format suitable for a space-separated log line.
2058 * @param string $s
2059 * @return string
2061 protected function encodeRequestLogValue( $s ) {
2062 static $table = [];
2063 if ( !$table ) {
2064 $chars = ';@$!*(),/:';
2065 $numChars = strlen( $chars );
2066 for ( $i = 0; $i < $numChars; $i++ ) {
2067 $table[rawurlencode( $chars[$i] )] = $chars[$i];
2071 return strtr( rawurlencode( $s ), $table );
2075 * Get the request parameters used in the course of the preceding execute() request
2076 * @return array
2078 protected function getParamsUsed() {
2079 return array_keys( $this->mParamsUsed );
2083 * Mark parameters as used
2084 * @param string|string[] $params
2086 public function markParamsUsed( $params ) {
2087 $this->mParamsUsed += array_fill_keys( (array)$params, true );
2091 * Get the request parameters that should be considered sensitive
2092 * @since 1.29
2093 * @return array
2095 protected function getSensitiveParams() {
2096 return array_keys( $this->mParamsSensitive );
2100 * Mark parameters as sensitive
2102 * This is called automatically for you when declaring a parameter
2103 * with ApiBase::PARAM_SENSITIVE.
2105 * @since 1.29
2106 * @param string|string[] $params
2108 public function markParamsSensitive( $params ) {
2109 $this->mParamsSensitive += array_fill_keys( (array)$params, true );
2113 * Get a request value, and register the fact that it was used, for logging.
2114 * @param string $name
2115 * @param string|null $default
2116 * @return string|null
2118 public function getVal( $name, $default = null ) {
2119 $this->mParamsUsed[$name] = true;
2121 $ret = $this->getRequest()->getVal( $name );
2122 if ( $ret === null ) {
2123 if ( $this->getRequest()->getArray( $name ) !== null ) {
2124 // See T12262 for why we don't just implode( '|', ... ) the
2125 // array.
2126 $this->addWarning( [ 'apiwarn-unsupportedarray', $name ] );
2128 $ret = $default;
2130 return $ret;
2134 * Get a boolean request value, and register the fact that the parameter
2135 * was used, for logging.
2136 * @param string $name
2137 * @return bool
2139 public function getCheck( $name ) {
2140 $this->mParamsUsed[$name] = true;
2141 return $this->getRequest()->getCheck( $name );
2145 * Get a request upload, and register the fact that it was used, for logging.
2147 * @since 1.21
2148 * @param string $name Parameter name
2149 * @return WebRequestUpload
2151 public function getUpload( $name ) {
2152 $this->mParamsUsed[$name] = true;
2154 return $this->getRequest()->getUpload( $name );
2158 * Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,
2159 * for example in case of spelling mistakes or a missing 'g' prefix for generators.
2161 protected function reportUnusedParams() {
2162 $paramsUsed = $this->getParamsUsed();
2163 $allParams = $this->getRequest()->getValueNames();
2165 if ( !$this->mInternalMode ) {
2166 // Printer has not yet executed; don't warn that its parameters are unused
2167 $printerParams = $this->mPrinter->encodeParamName(
2168 array_keys( $this->mPrinter->getFinalParams() ?: [] )
2170 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
2171 } else {
2172 $unusedParams = array_diff( $allParams, $paramsUsed );
2175 if ( count( $unusedParams ) ) {
2176 $this->addWarning( [
2177 'apierror-unrecognizedparams',
2178 Message::listParam( array_map( 'wfEscapeWikiText', $unusedParams ), 'comma' ),
2179 count( $unusedParams )
2180 ] );
2185 * Print results using the current printer
2187 * @param int $httpCode HTTP status code, or 0 to not change
2189 protected function printResult( $httpCode = 0 ) {
2190 if ( $this->getConfig()->get( MainConfigNames::DebugAPI ) !== false ) {
2191 $this->addWarning( 'apiwarn-wgdebugapi' );
2194 $printer = $this->mPrinter;
2195 $printer->initPrinter( false );
2196 if ( $httpCode ) {
2197 $printer->setHttpStatus( $httpCode );
2199 $printer->execute();
2200 $printer->closePrinter();
2204 * @return bool
2206 public function isReadMode() {
2207 return false;
2211 * See ApiBase for description.
2213 * @return array
2215 public function getAllowedParams() {
2216 return [
2217 'action' => [
2218 ParamValidator::PARAM_DEFAULT => 'help',
2219 ParamValidator::PARAM_TYPE => 'submodule',
2221 'format' => [
2222 ParamValidator::PARAM_DEFAULT => self::API_DEFAULT_FORMAT,
2223 ParamValidator::PARAM_TYPE => 'submodule',
2225 'maxlag' => [
2226 ParamValidator::PARAM_TYPE => 'integer'
2228 'smaxage' => [
2229 ParamValidator::PARAM_TYPE => 'integer',
2230 ParamValidator::PARAM_DEFAULT => 0,
2231 IntegerDef::PARAM_MIN => 0,
2233 'maxage' => [
2234 ParamValidator::PARAM_TYPE => 'integer',
2235 ParamValidator::PARAM_DEFAULT => 0,
2236 IntegerDef::PARAM_MIN => 0,
2238 'assert' => [
2239 ParamValidator::PARAM_TYPE => [ 'anon', 'user', 'bot' ]
2241 'assertuser' => [
2242 ParamValidator::PARAM_TYPE => 'user',
2243 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
2245 'requestid' => null,
2246 'servedby' => false,
2247 'curtimestamp' => false,
2248 'responselanginfo' => false,
2249 'origin' => null,
2250 'uselang' => [
2251 ParamValidator::PARAM_DEFAULT => self::API_DEFAULT_USELANG,
2253 'variant' => null,
2254 'errorformat' => [
2255 ParamValidator::PARAM_TYPE => [ 'plaintext', 'wikitext', 'html', 'raw', 'none', 'bc' ],
2256 ParamValidator::PARAM_DEFAULT => 'bc',
2257 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
2259 'errorlang' => [
2260 ParamValidator::PARAM_DEFAULT => 'uselang',
2262 'errorsuselocal' => [
2263 ParamValidator::PARAM_DEFAULT => false,
2268 /** @inheritDoc */
2269 protected function getExamplesMessages() {
2270 return [
2271 'action=help'
2272 => 'apihelp-help-example-main',
2273 'action=help&recursivesubmodules=1'
2274 => 'apihelp-help-example-recursive',
2279 * @inheritDoc
2280 * @phan-param array{nolead?:bool,headerlevel?:int,tocnumber?:int[]} $options
2282 public function modifyHelp( array &$help, array $options, array &$tocData ) {
2283 // Wish PHP had an "array_insert_before". Instead, we have to manually
2284 // reindex the array to get 'permissions' in the right place.
2285 $oldHelp = $help;
2286 $help = [];
2287 foreach ( $oldHelp as $k => $v ) {
2288 if ( $k === 'submodules' ) {
2289 $help['permissions'] = '';
2291 $help[$k] = $v;
2293 $help['datatypes'] = '';
2294 $help['templatedparams'] = '';
2295 $help['credits'] = '';
2297 // Fill 'permissions'
2298 $help['permissions'] .= Html::openElement( 'div',
2299 [ 'class' => [ 'apihelp-block', 'apihelp-permissions' ] ] );
2300 $m = $this->msg( 'api-help-permissions' );
2301 if ( !$m->isDisabled() ) {
2302 $help['permissions'] .= Html::rawElement( 'div', [ 'class' => 'apihelp-block-head' ],
2303 $m->numParams( count( self::RIGHTS_MAP ) )->parse()
2306 $help['permissions'] .= Html::openElement( 'dl' );
2307 // TODO inject stuff, see T265644
2308 $groupPermissionsLookup = MediaWikiServices::getInstance()->getGroupPermissionsLookup();
2309 foreach ( self::RIGHTS_MAP as $right => $rightMsg ) {
2310 $help['permissions'] .= Html::element( 'dt', [], $right );
2312 $rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
2313 $help['permissions'] .= Html::rawElement( 'dd', [], $rightMsg );
2315 $groups = array_map( static function ( $group ) {
2316 return $group == '*' ? 'all' : $group;
2317 }, $groupPermissionsLookup->getGroupsWithPermission( $right ) );
2319 $help['permissions'] .= Html::rawElement( 'dd', [],
2320 $this->msg( 'api-help-permissions-granted-to' )
2321 ->numParams( count( $groups ) )
2322 ->params( Message::listParam( $groups ) )
2323 ->parse()
2326 $help['permissions'] .= Html::closeElement( 'dl' );
2327 $help['permissions'] .= Html::closeElement( 'div' );
2329 // Fill 'datatypes', 'templatedparams', and 'credits', if applicable
2330 if ( empty( $options['nolead'] ) ) {
2331 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Must set when nolead is not set
2332 $level = $options['headerlevel'];
2333 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Must set when nolead is not set
2334 $tocnumber = &$options['tocnumber'];
2336 $header = $this->msg( 'api-help-datatypes-header' )->parse();
2338 $id = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_PRIMARY );
2339 $idFallback = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_FALLBACK );
2340 $headline = Linker::makeHeadline( min( 6, $level ),
2341 ' class="apihelp-header">',
2342 $id,
2343 $header,
2345 $idFallback
2347 // Ensure we have a sensible anchor
2348 if ( $id !== 'main/datatypes' && $idFallback !== 'main/datatypes' ) {
2349 $headline = '<div id="main/datatypes"></div>' . $headline;
2351 $help['datatypes'] .= $headline;
2352 $help['datatypes'] .= $this->msg( 'api-help-datatypes-top' )->parseAsBlock();
2353 $help['datatypes'] .= '<dl>';
2354 foreach ( $this->getParamValidator()->knownTypes() as $type ) {
2355 $m = $this->msg( "api-help-datatype-$type" );
2356 if ( !$m->isDisabled() ) {
2357 $id = "main/datatype/$type";
2358 $help['datatypes'] .= '<dt id="' . htmlspecialchars( $id ) . '">';
2359 $encId = Sanitizer::escapeIdForAttribute( $id, Sanitizer::ID_PRIMARY );
2360 if ( $encId !== $id ) {
2361 $help['datatypes'] .= '<span id="' . htmlspecialchars( $encId ) . '"></span>';
2363 $encId2 = Sanitizer::escapeIdForAttribute( $id, Sanitizer::ID_FALLBACK );
2364 if ( $encId2 !== $id && $encId2 !== $encId ) {
2365 $help['datatypes'] .= '<span id="' . htmlspecialchars( $encId2 ) . '"></span>';
2367 $help['datatypes'] .= htmlspecialchars( $type ) . '</dt><dd>' . $m->parseAsBlock() . "</dd>";
2370 $help['datatypes'] .= '</dl>';
2371 if ( !isset( $tocData['main/datatypes'] ) ) {
2372 $tocnumber[$level]++;
2373 $tocData['main/datatypes'] = [
2374 'toclevel' => count( $tocnumber ),
2375 'level' => $level,
2376 'anchor' => 'main/datatypes',
2377 'line' => $header,
2378 'number' => implode( '.', $tocnumber ),
2379 'index' => '',
2383 $header = $this->msg( 'api-help-templatedparams-header' )->parse();
2385 $id = Sanitizer::escapeIdForAttribute( 'main/templatedparams', Sanitizer::ID_PRIMARY );
2386 $idFallback = Sanitizer::escapeIdForAttribute( 'main/templatedparams', Sanitizer::ID_FALLBACK );
2387 $headline = Linker::makeHeadline( min( 6, $level ),
2388 ' class="apihelp-header">',
2389 $id,
2390 $header,
2392 $idFallback
2394 // Ensure we have a sensible anchor
2395 if ( $id !== 'main/templatedparams' && $idFallback !== 'main/templatedparams' ) {
2396 $headline = '<div id="main/templatedparams"></div>' . $headline;
2398 $help['templatedparams'] .= $headline;
2399 $help['templatedparams'] .= $this->msg( 'api-help-templatedparams' )->parseAsBlock();
2400 if ( !isset( $tocData['main/templatedparams'] ) ) {
2401 $tocnumber[$level]++;
2402 $tocData['main/templatedparams'] = [
2403 'toclevel' => count( $tocnumber ),
2404 'level' => $level,
2405 'anchor' => 'main/templatedparams',
2406 'line' => $header,
2407 'number' => implode( '.', $tocnumber ),
2408 'index' => '',
2412 $header = $this->msg( 'api-credits-header' )->parse();
2413 $id = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_PRIMARY );
2414 $idFallback = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_FALLBACK );
2415 $headline = Linker::makeHeadline( min( 6, $level ),
2416 ' class="apihelp-header">',
2417 $id,
2418 $header,
2420 $idFallback
2422 // Ensure we have a sensible anchor
2423 if ( $id !== 'main/credits' && $idFallback !== 'main/credits' ) {
2424 $headline = '<div id="main/credits"></div>' . $headline;
2426 $help['credits'] .= $headline;
2427 $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
2428 if ( !isset( $tocData['main/credits'] ) ) {
2429 $tocnumber[$level]++;
2430 $tocData['main/credits'] = [
2431 'toclevel' => count( $tocnumber ),
2432 'level' => $level,
2433 'anchor' => 'main/credits',
2434 'line' => $header,
2435 'number' => implode( '.', $tocnumber ),
2436 'index' => '',
2442 private $mCanApiHighLimits = null;
2445 * Check whether the current user is allowed to use high limits
2446 * @return bool
2448 public function canApiHighLimits() {
2449 if ( !isset( $this->mCanApiHighLimits ) ) {
2450 $this->mCanApiHighLimits = $this->getAuthority()->isAllowed( 'apihighlimits' );
2453 return $this->mCanApiHighLimits;
2457 * Overrides to return this instance's module manager.
2458 * @return ApiModuleManager
2460 public function getModuleManager() {
2461 return $this->mModuleMgr;
2465 * Fetches the user agent used for this request
2467 * The value will be the combination of the 'Api-User-Agent' header (if
2468 * any) and the standard User-Agent header (if any).
2470 * @return string
2472 public function getUserAgent() {
2473 return trim(
2474 $this->getRequest()->getHeader( 'Api-user-agent' ) . ' ' .
2475 $this->getRequest()->getHeader( 'User-agent' )
2481 * For really cool vim folding this needs to be at the end:
2482 * vim: foldmarker=@{,@} foldmethod=marker