MediaWiki REL1_34
ApiMain.php
Go to the documentation of this file.
1<?php
26use Wikimedia\Timestamp\TimestampException;
27
41class ApiMain extends ApiBase {
45 const API_DEFAULT_FORMAT = 'jsonfm';
46
50 const API_DEFAULT_USELANG = 'user';
51
55 private static $Modules = [
56 'login' => ApiLogin::class,
57 'clientlogin' => ApiClientLogin::class,
58 'logout' => ApiLogout::class,
59 'createaccount' => ApiAMCreateAccount::class,
60 'linkaccount' => ApiLinkAccount::class,
61 'unlinkaccount' => ApiRemoveAuthenticationData::class,
62 'changeauthenticationdata' => ApiChangeAuthenticationData::class,
63 'removeauthenticationdata' => ApiRemoveAuthenticationData::class,
64 'resetpassword' => ApiResetPassword::class,
65 'query' => ApiQuery::class,
66 'expandtemplates' => ApiExpandTemplates::class,
67 'parse' => ApiParse::class,
68 'stashedit' => ApiStashEdit::class,
69 'opensearch' => ApiOpenSearch::class,
70 'feedcontributions' => ApiFeedContributions::class,
71 'feedrecentchanges' => ApiFeedRecentChanges::class,
72 'feedwatchlist' => ApiFeedWatchlist::class,
73 'help' => ApiHelp::class,
74 'paraminfo' => ApiParamInfo::class,
75 'rsd' => ApiRsd::class,
76 'compare' => ApiComparePages::class,
77 'tokens' => ApiTokens::class,
78 'checktoken' => ApiCheckToken::class,
79 'cspreport' => ApiCSPReport::class,
80 'validatepassword' => ApiValidatePassword::class,
81
82 // Write modules
83 'purge' => ApiPurge::class,
84 'setnotificationtimestamp' => ApiSetNotificationTimestamp::class,
85 'rollback' => ApiRollback::class,
86 'delete' => ApiDelete::class,
87 'undelete' => ApiUndelete::class,
88 'protect' => ApiProtect::class,
89 'block' => ApiBlock::class,
90 'unblock' => ApiUnblock::class,
91 'move' => ApiMove::class,
92 'edit' => ApiEditPage::class,
93 'upload' => ApiUpload::class,
94 'filerevert' => ApiFileRevert::class,
95 'emailuser' => ApiEmailUser::class,
96 'watch' => ApiWatch::class,
97 'patrol' => ApiPatrol::class,
98 'import' => ApiImport::class,
99 'clearhasmsg' => ApiClearHasMsg::class,
100 'userrights' => ApiUserrights::class,
101 'options' => ApiOptions::class,
102 'imagerotate' => ApiImageRotate::class,
103 'revisiondelete' => ApiRevisionDelete::class,
104 'managetags' => ApiManageTags::class,
105 'tag' => ApiTag::class,
106 'mergehistory' => ApiMergeHistory::class,
107 'setpagelanguage' => ApiSetPageLanguage::class,
108 ];
109
113 private static $Formats = [
114 'json' => ApiFormatJson::class,
115 'jsonfm' => ApiFormatJson::class,
116 'php' => ApiFormatPhp::class,
117 'phpfm' => ApiFormatPhp::class,
118 'xml' => ApiFormatXml::class,
119 'xmlfm' => ApiFormatXml::class,
120 'rawfm' => ApiFormatJson::class,
121 'none' => ApiFormatNone::class,
122 ];
123
130 private static $mRights = [
131 'writeapi' => [
132 'msg' => 'right-writeapi',
133 'params' => []
134 ],
135 'apihighlimits' => [
136 'msg' => 'api-help-right-apihighlimits',
138 ]
139 ];
140
144 private $mPrinter;
145
149 private $mAction;
153 private $mModule;
154
155 private $mCacheMode = 'private';
157 private $mCacheControl = [];
158 private $mParamsUsed = [];
159 private $mParamsSensitive = [];
160
163
172 public function __construct( $context = null, $enableWrite = false ) {
173 if ( $context === null ) {
174 $context = RequestContext::getMain();
175 } elseif ( $context instanceof WebRequest ) {
176 // BC for pre-1.19
177 $request = $context;
178 $context = RequestContext::getMain();
179 }
180 // We set a derivative context so we can change stuff later
181 $this->setContext( new DerivativeContext( $context ) );
182
183 if ( isset( $request ) ) {
184 $this->getContext()->setRequest( $request );
185 } else {
186 $request = $this->getRequest();
187 }
188
189 $this->mInternalMode = ( $request instanceof FauxRequest );
190
191 // Special handling for the main module: $parent === $this
192 parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
193
194 $config = $this->getConfig();
195
196 if ( !$this->mInternalMode ) {
197 // Log if a request with a non-whitelisted Origin header is seen
198 // with session cookies.
199 $originHeader = $request->getHeader( 'Origin' );
200 if ( $originHeader === false ) {
201 $origins = [];
202 } else {
203 $originHeader = trim( $originHeader );
204 $origins = preg_split( '/\s+/', $originHeader );
205 }
206 $sessionCookies = array_intersect(
207 array_keys( $_COOKIE ),
208 MediaWiki\Session\SessionManager::singleton()->getVaryCookies()
209 );
210 if ( $origins && $sessionCookies && (
211 count( $origins ) !== 1 || !self::matchOrigin(
212 $origins[0],
213 $config->get( 'CrossSiteAJAXdomains' ),
214 $config->get( 'CrossSiteAJAXdomainExceptions' )
215 )
216 ) ) {
217 LoggerFactory::getInstance( 'cors' )->warning(
218 'Non-whitelisted CORS request with session cookies', [
219 'origin' => $originHeader,
220 'cookies' => $sessionCookies,
221 'ip' => $request->getIP(),
222 'userAgent' => $this->getUserAgent(),
223 'wiki' => WikiMap::getCurrentWikiDbDomain()->getId(),
224 ]
225 );
226 }
227
228 // If we're in a mode that breaks the same-origin policy, strip
229 // user credentials for security.
230 if ( $this->lacksSameOriginSecurity() ) {
231 global $wgUser;
232 wfDebug( "API: stripping user credentials when the same-origin policy is not applied\n" );
233 $wgUser = new User();
234 $this->getContext()->setUser( $wgUser );
235 $request->response()->header( 'MediaWiki-Login-Suppressed: true' );
236 }
237 }
238
239 $this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
240
241 // Setup uselang. This doesn't use $this->getParameter()
242 // because we're not ready to handle errors yet.
243 // Optimisation: Avoid slow getVal(), this isn't user-generated content.
244 $uselang = $request->getRawVal( 'uselang', self::API_DEFAULT_USELANG );
245 if ( $uselang === 'user' ) {
246 // Assume the parent context is going to return the user language
247 // for uselang=user (see T85635).
248 } else {
249 if ( $uselang === 'content' ) {
250 $uselang = MediaWikiServices::getInstance()->getContentLanguage()->getCode();
251 }
252 $code = RequestContext::sanitizeLangCode( $uselang );
253 $this->getContext()->setLanguage( $code );
254 if ( !$this->mInternalMode ) {
255 global $wgLang;
256 $wgLang = $this->getContext()->getLanguage();
257 RequestContext::getMain()->setLanguage( $wgLang );
258 }
259 }
260
261 // Set up the error formatter. This doesn't use $this->getParameter()
262 // because we're not ready to handle errors yet.
263 // Optimisation: Avoid slow getVal(), this isn't user-generated content.
264 $errorFormat = $request->getRawVal( 'errorformat', 'bc' );
265 $errorLangCode = $request->getRawVal( 'errorlang', 'uselang' );
266 $errorsUseDB = $request->getCheck( 'errorsuselocal' );
267 if ( in_array( $errorFormat, [ 'plaintext', 'wikitext', 'html', 'raw', 'none' ], true ) ) {
268 if ( $errorLangCode === 'uselang' ) {
269 $errorLang = $this->getLanguage();
270 } elseif ( $errorLangCode === 'content' ) {
271 $errorLang = MediaWikiServices::getInstance()->getContentLanguage();
272 } else {
273 $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode );
274 $errorLang = Language::factory( $errorLangCode );
275 }
276 $this->mErrorFormatter = new ApiErrorFormatter(
277 $this->mResult, $errorLang, $errorFormat, $errorsUseDB
278 );
279 } else {
280 $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
281 }
282 $this->mResult->setErrorFormatter( $this->getErrorFormatter() );
283
284 $this->mModuleMgr = new ApiModuleManager(
285 $this,
286 MediaWikiServices::getInstance()->getObjectFactory()
287 );
288 $this->mModuleMgr->addModules( self::$Modules, 'action' );
289 $this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
290 $this->mModuleMgr->addModules( self::$Formats, 'format' );
291 $this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' );
292
293 Hooks::run( 'ApiMain::moduleManager', [ $this->mModuleMgr ] );
294
295 $this->mContinuationManager = null;
296 $this->mEnableWrite = $enableWrite;
297
298 $this->mCdnMaxAge = -1; // flag for executeActionWithErrorHandling()
299 }
300
305 public function isInternalMode() {
306 return $this->mInternalMode;
307 }
308
314 public function getResult() {
315 return $this->mResult;
316 }
317
322 public function lacksSameOriginSecurity() {
323 if ( $this->lacksSameOriginSecurity !== null ) {
324 return $this->lacksSameOriginSecurity;
325 }
326
327 $request = $this->getRequest();
328
329 // JSONP mode
330 if ( $request->getCheck( 'callback' ) ) {
331 $this->lacksSameOriginSecurity = true;
332 return true;
333 }
334
335 // Anonymous CORS
336 if ( $request->getVal( 'origin' ) === '*' ) {
337 $this->lacksSameOriginSecurity = true;
338 return true;
339 }
340
341 // Header to be used from XMLHTTPRequest when the request might
342 // otherwise be used for XSS.
343 if ( $request->getHeader( 'Treat-as-Untrusted' ) !== false ) {
344 $this->lacksSameOriginSecurity = true;
345 return true;
346 }
347
348 // Allow extensions to override.
349 $this->lacksSameOriginSecurity = !Hooks::run( 'RequestHasSameOriginSecurity', [ $request ] );
350 return $this->lacksSameOriginSecurity;
351 }
352
357 public function getErrorFormatter() {
358 return $this->mErrorFormatter;
359 }
360
365 public function getContinuationManager() {
366 return $this->mContinuationManager;
367 }
368
373 public function setContinuationManager( ApiContinuationManager $manager = null ) {
374 if ( $manager !== null && $this->mContinuationManager !== null ) {
375 throw new UnexpectedValueException(
376 __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
377 ' when a manager is already set from ' . $this->mContinuationManager->getSource()
378 );
379 }
380 $this->mContinuationManager = $manager;
381 }
382
388 public function getModule() {
389 return $this->mModule;
390 }
391
397 public function getPrinter() {
398 return $this->mPrinter;
399 }
400
406 public function setCacheMaxAge( $maxage ) {
407 $this->setCacheControl( [
408 'max-age' => $maxage,
409 's-maxage' => $maxage
410 ] );
411 }
412
438 public function setCacheMode( $mode ) {
439 if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
440 wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" );
441
442 // Ignore for forwards-compatibility
443 return;
444 }
445
446 if ( !User::isEveryoneAllowed( 'read' ) ) {
447 // Private wiki, only private headers
448 if ( $mode !== 'private' ) {
449 wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
450
451 return;
452 }
453 }
454
455 if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
456 // User language is used for i18n, so we don't want to publicly
457 // cache. Anons are ok, because if they have non-default language
458 // then there's an appropriate Vary header set by whatever set
459 // their non-default language.
460 wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
461 "'anon-public-user-private' due to uselang=user\n" );
462 $mode = 'anon-public-user-private';
463 }
464
465 wfDebug( __METHOD__ . ": setting cache mode $mode\n" );
466 $this->mCacheMode = $mode;
467 }
468
479 public function setCacheControl( $directives ) {
480 $this->mCacheControl = $directives + $this->mCacheControl;
481 }
482
490 public function createPrinterByName( $format ) {
491 $printer = $this->mModuleMgr->getModule( $format, 'format', /* $ignoreCache */ true );
492 if ( $printer === null ) {
493 $this->dieWithError(
494 [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
495 );
496 }
497
498 return $printer;
499 }
500
504 public function execute() {
505 if ( $this->mInternalMode ) {
506 $this->executeAction();
507 } else {
509 }
510 }
511
516 protected function executeActionWithErrorHandling() {
517 // Verify the CORS header before executing the action
518 if ( !$this->handleCORS() ) {
519 // handleCORS() has sent a 403, abort
520 return;
521 }
522
523 // Exit here if the request method was OPTIONS
524 // (assume there will be a followup GET or POST)
525 if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
526 return;
527 }
528
529 // In case an error occurs during data output,
530 // clear the output buffer and print just the error information
531 $obLevel = ob_get_level();
532 ob_start();
533
534 $t = microtime( true );
535 $isError = false;
536 try {
537 $this->executeAction();
538 $runTime = microtime( true ) - $t;
539 $this->logRequest( $runTime );
540 MediaWikiServices::getInstance()->getStatsdDataFactory()->timing(
541 'api.' . $this->mModule->getModuleName() . '.executeTiming', 1000 * $runTime
542 );
543 } catch ( Exception $e ) { // @todo Remove this block when HHVM is no longer supported
544 $this->handleException( $e );
545 $this->logRequest( microtime( true ) - $t, $e );
546 $isError = true;
547 } catch ( Throwable $e ) {
548 $this->handleException( $e );
549 $this->logRequest( microtime( true ) - $t, $e );
550 $isError = true;
551 }
552
553 // Commit DBs and send any related cookies and headers
554 MediaWiki::preOutputCommit( $this->getContext() );
555
556 // Send cache headers after any code which might generate an error, to
557 // avoid sending public cache headers for errors.
558 $this->sendCacheHeaders( $isError );
559
560 // Executing the action might have already messed with the output
561 // buffers.
562 while ( ob_get_level() > $obLevel ) {
563 ob_end_flush();
564 }
565 }
566
573 protected function handleException( $e ) {
574 // T65145: Rollback any open database transactions
575 if ( !$e instanceof ApiUsageException ) {
576 // ApiUsageExceptions are intentional, so don't rollback if that's the case
577 MWExceptionHandler::rollbackMasterChangesAndLog( $e );
578 }
579
580 // Allow extra cleanup and logging
581 Hooks::run( 'ApiMain::onException', [ $this, $e ] );
582
583 // Handle any kind of exception by outputting properly formatted error message.
584 // If this fails, an unhandled exception should be thrown so that global error
585 // handler will process and log it.
586
587 $errCodes = $this->substituteResultWithError( $e );
588
589 // Error results should not be cached
590 $this->setCacheMode( 'private' );
591
592 $response = $this->getRequest()->response();
593 $headerStr = 'MediaWiki-API-Error: ' . implode( ', ', $errCodes );
594 $response->header( $headerStr );
595
596 // Reset and print just the error message
597 ob_clean();
598
599 // Printer may not be initialized if the extractRequestParams() fails for the main module
600 $this->createErrorPrinter();
601
602 // Get desired HTTP code from an ApiUsageException. Don't use codes from other
603 // exception types, as they are unlikely to be intended as an HTTP code.
604 $httpCode = $e instanceof ApiUsageException ? $e->getCode() : 0;
605
606 $failed = false;
607 try {
608 $this->printResult( $httpCode );
609 } catch ( ApiUsageException $ex ) {
610 // The error printer itself is failing. Try suppressing its request
611 // parameters and redo.
612 $failed = true;
613 $this->addWarning( 'apiwarn-errorprinterfailed' );
614 foreach ( $ex->getStatusValue()->getErrors() as $error ) {
615 try {
616 $this->mPrinter->addWarning( $error );
617 } catch ( Exception $ex2 ) { // @todo Remove this block when HHVM is no longer supported
618 // WTF?
619 $this->addWarning( $error );
620 } catch ( Throwable $ex2 ) {
621 // WTF?
622 $this->addWarning( $error );
623 }
624 }
625 }
626 if ( $failed ) {
627 $this->mPrinter = null;
628 $this->createErrorPrinter();
629 $this->mPrinter->forceDefaultParams();
630 if ( $httpCode ) {
631 $response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200
632 }
633 $this->printResult( $httpCode );
634 }
635 }
636
647 public static function handleApiBeforeMainException( $e ) {
648 ob_start();
649
650 try {
651 $main = new self( RequestContext::getMain(), false );
652 $main->handleException( $e );
653 $main->logRequest( 0, $e );
654 } catch ( Exception $e2 ) { // @todo Remove this block when HHVM is no longer supported
655 // Nope, even that didn't work. Punt.
656 throw $e;
657 } catch ( Throwable $e2 ) {
658 // Nope, even that didn't work. Punt.
659 throw $e;
660 }
661
662 // Reset cache headers
663 $main->sendCacheHeaders( true );
664
665 ob_end_flush();
666 }
667
682 protected function handleCORS() {
683 $originParam = $this->getParameter( 'origin' ); // defaults to null
684 if ( $originParam === null ) {
685 // No origin parameter, nothing to do
686 return true;
687 }
688
689 $request = $this->getRequest();
690 $response = $request->response();
691
692 $matchedOrigin = false;
693 $allowTiming = false;
694 $varyOrigin = true;
695
696 if ( $originParam === '*' ) {
697 // Request for anonymous CORS
698 // Technically we should check for the presence of an Origin header
699 // and not process it as CORS if it's not set, but that would
700 // require us to vary on Origin for all 'origin=*' requests which
701 // we don't want to do.
702 $matchedOrigin = true;
703 $allowOrigin = '*';
704 $allowCredentials = 'false';
705 $varyOrigin = false; // No need to vary
706 } else {
707 // Non-anonymous CORS, check we allow the domain
708
709 // Origin: header is a space-separated list of origins, check all of them
710 $originHeader = $request->getHeader( 'Origin' );
711 if ( $originHeader === false ) {
712 $origins = [];
713 } else {
714 $originHeader = trim( $originHeader );
715 $origins = preg_split( '/\s+/', $originHeader );
716 }
717
718 if ( !in_array( $originParam, $origins ) ) {
719 // origin parameter set but incorrect
720 // Send a 403 response
721 $response->statusHeader( 403 );
722 $response->header( 'Cache-Control: no-cache' );
723 echo "'origin' parameter does not match Origin header\n";
724
725 return false;
726 }
727
728 $config = $this->getConfig();
729 $matchedOrigin = count( $origins ) === 1 && self::matchOrigin(
730 $originParam,
731 $config->get( 'CrossSiteAJAXdomains' ),
732 $config->get( 'CrossSiteAJAXdomainExceptions' )
733 );
734
735 $allowOrigin = $originHeader;
736 $allowCredentials = 'true';
737 $allowTiming = $originHeader;
738 }
739
740 if ( $matchedOrigin ) {
741 $requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
742 $preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
743 if ( $preflight ) {
744 // This is a CORS preflight request
745 if ( $requestedMethod !== 'POST' && $requestedMethod !== 'GET' ) {
746 // If method is not a case-sensitive match, do not set any additional headers and terminate.
747 $response->header( 'MediaWiki-CORS-Rejection: Unsupported method requested in preflight' );
748 return true;
749 }
750 // We allow the actual request to send the following headers
751 $requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
752 if ( $requestedHeaders !== false ) {
753 if ( !self::matchRequestedHeaders( $requestedHeaders ) ) {
754 $response->header( 'MediaWiki-CORS-Rejection: Unsupported header requested in preflight' );
755 return true;
756 }
757 $response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
758 }
759
760 // We only allow the actual request to be GET or POST
761 $response->header( 'Access-Control-Allow-Methods: POST, GET' );
762 } elseif ( $request->getMethod() !== 'POST' && $request->getMethod() !== 'GET' ) {
763 // Unsupported non-preflight method, don't handle it as CORS
764 $response->header(
765 'MediaWiki-CORS-Rejection: Unsupported method for simple request or actual request'
766 );
767 return true;
768 }
769
770 $response->header( "Access-Control-Allow-Origin: $allowOrigin" );
771 $response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
772 // https://www.w3.org/TR/resource-timing/#timing-allow-origin
773 if ( $allowTiming !== false ) {
774 $response->header( "Timing-Allow-Origin: $allowTiming" );
775 }
776
777 if ( !$preflight ) {
778 $response->header(
779 'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag, '
780 . 'MediaWiki-Login-Suppressed'
781 );
782 }
783 } else {
784 $response->header( 'MediaWiki-CORS-Rejection: Origin mismatch' );
785 }
786
787 if ( $varyOrigin ) {
788 $this->getOutput()->addVaryHeader( 'Origin' );
789 }
790
791 return true;
792 }
793
802 protected static function matchOrigin( $value, $rules, $exceptions ) {
803 foreach ( $rules as $rule ) {
804 if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
805 // Rule matches, check exceptions
806 foreach ( $exceptions as $exc ) {
807 if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
808 return false;
809 }
810 }
811
812 return true;
813 }
814 }
815
816 return false;
817 }
818
826 protected static function matchRequestedHeaders( $requestedHeaders ) {
827 if ( trim( $requestedHeaders ) === '' ) {
828 return true;
829 }
830 $requestedHeaders = explode( ',', $requestedHeaders );
831 $allowedAuthorHeaders = array_flip( [
832 /* simple headers (see spec) */
833 'accept',
834 'accept-language',
835 'content-language',
836 'content-type',
837 /* non-authorable headers in XHR, which are however requested by some UAs */
838 'accept-encoding',
839 'dnt',
840 'origin',
841 /* MediaWiki whitelist */
842 'user-agent',
843 'api-user-agent',
844 ] );
845 foreach ( $requestedHeaders as $rHeader ) {
846 $rHeader = strtolower( trim( $rHeader ) );
847 if ( !isset( $allowedAuthorHeaders[$rHeader] ) ) {
848 LoggerFactory::getInstance( 'api-warning' )->warning(
849 'CORS preflight failed on requested header: {header}', [
850 'header' => $rHeader
851 ]
852 );
853 return false;
854 }
855 }
856 return true;
857 }
858
867 protected static function wildcardToRegex( $wildcard ) {
868 $wildcard = preg_quote( $wildcard, '/' );
869 $wildcard = str_replace(
870 [ '\*', '\?' ],
871 [ '.*?', '.' ],
872 $wildcard
873 );
874
875 return "/^https?:\/\/$wildcard$/";
876 }
877
883 protected function sendCacheHeaders( $isError ) {
884 $response = $this->getRequest()->response();
885 $out = $this->getOutput();
886
887 $out->addVaryHeader( 'Treat-as-Untrusted' );
888
889 $config = $this->getConfig();
890
891 if ( $config->get( 'VaryOnXFP' ) ) {
892 $out->addVaryHeader( 'X-Forwarded-Proto' );
893 }
894
895 if ( !$isError && $this->mModule &&
896 ( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
897 ) {
898 $etag = $this->mModule->getConditionalRequestData( 'etag' );
899 if ( $etag !== null ) {
900 $response->header( "ETag: $etag" );
901 }
902 $lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
903 if ( $lastMod !== null ) {
904 $response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
905 }
906 }
907
908 // The logic should be:
909 // $this->mCacheControl['max-age'] is set?
910 // Use it, the module knows better than our guess.
911 // !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
912 // Use 0 because we can guess caching is probably the wrong thing to do.
913 // Use $this->getParameter( 'maxage' ), which already defaults to 0.
914 $maxage = 0;
915 if ( isset( $this->mCacheControl['max-age'] ) ) {
916 $maxage = $this->mCacheControl['max-age'];
917 } elseif ( ( $this->mModule && !$this->mModule->isWriteMode() ) ||
918 $this->mCacheMode !== 'private'
919 ) {
920 $maxage = $this->getParameter( 'maxage' );
921 }
922 $privateCache = 'private, must-revalidate, max-age=' . $maxage;
923
924 if ( $this->mCacheMode == 'private' ) {
925 $response->header( "Cache-Control: $privateCache" );
926 return;
927 }
928
929 if ( $this->mCacheMode == 'anon-public-user-private' ) {
930 $out->addVaryHeader( 'Cookie' );
931 $response->header( $out->getVaryHeader() );
932 if ( MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
933 // Logged in or otherwise has session (e.g. anonymous users who have edited)
934 // Mark request private
935 $response->header( "Cache-Control: $privateCache" );
936
937 return;
938 } // else anonymous, send public headers below
939 }
940
941 // Send public headers
942 $response->header( $out->getVaryHeader() );
943
944 // If nobody called setCacheMaxAge(), use the (s)maxage parameters
945 if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
946 $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
947 }
948 if ( !isset( $this->mCacheControl['max-age'] ) ) {
949 $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
950 }
951
952 if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
953 // Public cache not requested
954 // Sending a Vary header in this case is harmless, and protects us
955 // against conditional calls of setCacheMaxAge().
956 $response->header( "Cache-Control: $privateCache" );
957
958 return;
959 }
960
961 $this->mCacheControl['public'] = true;
962
963 // Send an Expires header
964 $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
965 $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
966 $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
967
968 // Construct the Cache-Control header
969 $ccHeader = '';
970 $separator = '';
971 foreach ( $this->mCacheControl as $name => $value ) {
972 if ( is_bool( $value ) ) {
973 if ( $value ) {
974 $ccHeader .= $separator . $name;
975 $separator = ', ';
976 }
977 } else {
978 $ccHeader .= $separator . "$name=$value";
979 $separator = ', ';
980 }
981 }
982
983 $response->header( "Cache-Control: $ccHeader" );
984 }
985
989 private function createErrorPrinter() {
990 if ( !isset( $this->mPrinter ) ) {
991 $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
992 if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
993 $value = self::API_DEFAULT_FORMAT;
994 }
995 $this->mPrinter = $this->createPrinterByName( $value );
996 }
997
998 // Printer may not be able to handle errors. This is particularly
999 // likely if the module returns something for getCustomPrinter().
1000 if ( !$this->mPrinter->canPrintErrors() ) {
1001 $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
1002 }
1003 }
1004
1020 protected function errorMessagesFromException( $e, $type = 'error' ) {
1021 $messages = [];
1022 if ( $e instanceof ApiUsageException ) {
1023 foreach ( $e->getStatusValue()->getErrorsByType( $type ) as $error ) {
1024 $messages[] = ApiMessage::create( $error );
1025 }
1026 } elseif ( $type !== 'error' ) {
1027 // None of the rest have any messages for non-error types
1028 } else {
1029 // Something is seriously wrong
1030 $config = $this->getConfig();
1031 // TODO: Avoid embedding arbitrary class names in the error code.
1032 $class = preg_replace( '#^Wikimedia\\\Rdbms\\\#', '', get_class( $e ) );
1033 $code = 'internal_api_error_' . $class;
1034 $data = [ 'errorclass' => get_class( $e ) ];
1035 if ( $config->get( 'ShowExceptionDetails' ) ) {
1036 if ( $e instanceof ILocalizedException ) {
1037 $msg = $e->getMessageObject();
1038 } elseif ( $e instanceof MessageSpecifier ) {
1039 $msg = Message::newFromSpecifier( $e );
1040 } else {
1041 $msg = wfEscapeWikiText( $e->getMessage() );
1042 }
1043 $params = [ 'apierror-exceptioncaught', WebRequest::getRequestId(), $msg ];
1044 } else {
1045 $params = [ 'apierror-exceptioncaughttype', WebRequest::getRequestId(), get_class( $e ) ];
1046 }
1047
1048 $messages[] = ApiMessage::create( $params, $code, $data );
1049 }
1050 return $messages;
1051 }
1052
1058 protected function substituteResultWithError( $e ) {
1059 $result = $this->getResult();
1060 $formatter = $this->getErrorFormatter();
1061 $config = $this->getConfig();
1062 $errorCodes = [];
1063
1064 // Remember existing warnings and errors across the reset
1065 $errors = $result->getResultData( [ 'errors' ] );
1066 $warnings = $result->getResultData( [ 'warnings' ] );
1067 $result->reset();
1068 if ( $warnings !== null ) {
1069 $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
1070 }
1071 if ( $errors !== null ) {
1072 $result->addValue( null, 'errors', $errors, ApiResult::NO_SIZE_CHECK );
1073
1074 // Collect the copied error codes for the return value
1075 foreach ( $errors as $error ) {
1076 if ( isset( $error['code'] ) ) {
1077 $errorCodes[$error['code']] = true;
1078 }
1079 }
1080 }
1081
1082 // Add errors from the exception
1083 $modulePath = $e instanceof ApiUsageException ? $e->getModulePath() : null;
1084 foreach ( $this->errorMessagesFromException( $e, 'error' ) as $msg ) {
1085 if ( ApiErrorFormatter::isValidApiCode( $msg->getApiCode() ) ) {
1086 $errorCodes[$msg->getApiCode()] = true;
1087 } else {
1088 LoggerFactory::getInstance( 'api-warning' )->error( 'Invalid API error code "{code}"', [
1089 'code' => $msg->getApiCode(),
1090 'exception' => $e,
1091 ] );
1092 $errorCodes['<invalid-code>'] = true;
1093 }
1094 $formatter->addError( $modulePath, $msg );
1095 }
1096 foreach ( $this->errorMessagesFromException( $e, 'warning' ) as $msg ) {
1097 $formatter->addWarning( $modulePath, $msg );
1098 }
1099
1100 // Add additional data. Path depends on whether we're in BC mode or not.
1101 // Data depends on the type of exception.
1102 if ( $formatter instanceof ApiErrorFormatter_BackCompat ) {
1103 $path = [ 'error' ];
1104 } else {
1105 $path = null;
1106 }
1107 if ( $e instanceof ApiUsageException ) {
1108 $link = wfExpandUrl( wfScript( 'api' ) );
1109 $result->addContentValue(
1110 $path,
1111 'docref',
1112 trim(
1113 $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text()
1114 . ' '
1115 . $this->msg( 'api-usage-mailinglist-ref' )->inLanguage( $formatter->getLanguage() )->text()
1116 )
1117 );
1118 } elseif ( $config->get( 'ShowExceptionDetails' ) ) {
1119 $result->addContentValue(
1120 $path,
1121 'trace',
1122 $this->msg( 'api-exception-trace',
1123 get_class( $e ),
1124 $e->getFile(),
1125 $e->getLine(),
1126 MWExceptionHandler::getRedactedTraceAsString( $e )
1127 )->inLanguage( $formatter->getLanguage() )->text()
1128 );
1129 }
1130
1131 // Add the id and such
1132 $this->addRequestedFields( [ 'servedby' ] );
1133
1134 return array_keys( $errorCodes );
1135 }
1136
1142 protected function addRequestedFields( $force = [] ) {
1143 $result = $this->getResult();
1144
1145 $requestid = $this->getParameter( 'requestid' );
1146 if ( $requestid !== null ) {
1147 $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
1148 }
1149
1150 if ( $this->getConfig()->get( 'ShowHostnames' ) && (
1151 in_array( 'servedby', $force, true ) || $this->getParameter( 'servedby' )
1152 ) ) {
1153 $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
1154 }
1155
1156 if ( $this->getParameter( 'curtimestamp' ) ) {
1157 $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601 ), ApiResult::NO_SIZE_CHECK );
1158 }
1159
1160 if ( $this->getParameter( 'responselanginfo' ) ) {
1161 $result->addValue( null, 'uselang', $this->getLanguage()->getCode(),
1162 ApiResult::NO_SIZE_CHECK );
1163 $result->addValue( null, 'errorlang', $this->getErrorFormatter()->getLanguage()->getCode(),
1164 ApiResult::NO_SIZE_CHECK );
1165 }
1166 }
1167
1172 protected function setupExecuteAction() {
1173 $this->addRequestedFields();
1174
1175 $params = $this->extractRequestParams();
1176 $this->mAction = $params['action'];
1177
1178 return $params;
1179 }
1180
1187 protected function setupModule() {
1188 // Instantiate the module requested by the user
1189 $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
1190 if ( $module === null ) {
1191 // Probably can't happen
1192 // @codeCoverageIgnoreStart
1193 $this->dieWithError(
1194 [ 'apierror-unknownaction', wfEscapeWikiText( $this->mAction ) ], 'unknown_action'
1195 );
1196 // @codeCoverageIgnoreEnd
1197 }
1198 $moduleParams = $module->extractRequestParams();
1199
1200 // Check token, if necessary
1201 if ( $module->needsToken() === true ) {
1202 throw new MWException(
1203 "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
1204 'See documentation for ApiBase::needsToken for details.'
1205 );
1206 }
1207 if ( $module->needsToken() ) {
1208 if ( !$module->mustBePosted() ) {
1209 throw new MWException(
1210 "Module '{$module->getModuleName()}' must require POST to use tokens."
1211 );
1212 }
1213
1214 if ( !isset( $moduleParams['token'] ) ) {
1215 // Probably can't happen
1216 // @codeCoverageIgnoreStart
1217 $module->dieWithError( [ 'apierror-missingparam', 'token' ] );
1218 // @codeCoverageIgnoreEnd
1219 }
1220
1221 $module->requirePostedParameters( [ 'token' ] );
1222
1223 if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
1224 $module->dieWithError( 'apierror-badtoken' );
1225 }
1226 }
1227
1228 return $module;
1229 }
1230
1234 private function getMaxLag() {
1235 $dbLag = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaxLag();
1236 $lagInfo = [
1237 'host' => $dbLag[0],
1238 'lag' => $dbLag[1],
1239 'type' => 'db'
1240 ];
1241
1242 $jobQueueLagFactor = $this->getConfig()->get( 'JobQueueIncludeInMaxLagFactor' );
1243 if ( $jobQueueLagFactor ) {
1244 // Turn total number of jobs into seconds by using the configured value
1245 $totalJobs = array_sum( JobQueueGroup::singleton()->getQueueSizes() );
1246 $jobQueueLag = $totalJobs / (float)$jobQueueLagFactor;
1247 if ( $jobQueueLag > $lagInfo['lag'] ) {
1248 $lagInfo = [
1249 'host' => wfHostname(), // XXX: Is there a better value that could be used?
1250 'lag' => $jobQueueLag,
1251 'type' => 'jobqueue',
1252 'jobs' => $totalJobs,
1253 ];
1254 }
1255 }
1256
1257 Hooks::runWithoutAbort( 'ApiMaxLagInfo', [ &$lagInfo ] );
1258
1259 return $lagInfo;
1260 }
1261
1268 protected function checkMaxLag( $module, $params ) {
1269 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
1270 $maxLag = $params['maxlag'];
1271 $lagInfo = $this->getMaxLag();
1272 if ( $lagInfo['lag'] > $maxLag ) {
1273 $response = $this->getRequest()->response();
1274
1275 $response->header( 'Retry-After: ' . max( (int)$maxLag, 5 ) );
1276 $response->header( 'X-Database-Lag: ' . (int)$lagInfo['lag'] );
1277
1278 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1279 $this->dieWithError(
1280 [ 'apierror-maxlag', $lagInfo['lag'], $lagInfo['host'] ],
1281 'maxlag',
1282 $lagInfo
1283 );
1284 }
1285
1286 $this->dieWithError( [ 'apierror-maxlag-generic', $lagInfo['lag'] ], 'maxlag', $lagInfo );
1287 }
1288 }
1289
1290 return true;
1291 }
1292
1314 protected function checkConditionalRequestHeaders( $module ) {
1315 if ( $this->mInternalMode ) {
1316 // No headers to check in internal mode
1317 return true;
1318 }
1319
1320 if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) {
1321 // Don't check POSTs
1322 return true;
1323 }
1324
1325 $return304 = false;
1326
1327 $ifNoneMatch = array_diff(
1328 $this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: [],
1329 [ '' ]
1330 );
1331 if ( $ifNoneMatch ) {
1332 if ( $ifNoneMatch === [ '*' ] ) {
1333 // API responses always "exist"
1334 $etag = '*';
1335 } else {
1336 $etag = $module->getConditionalRequestData( 'etag' );
1337 }
1338 }
1339 if ( $ifNoneMatch && $etag !== null ) {
1340 $test = substr( $etag, 0, 2 ) === 'W/' ? substr( $etag, 2 ) : $etag;
1341 $match = array_map( function ( $s ) {
1342 return substr( $s, 0, 2 ) === 'W/' ? substr( $s, 2 ) : $s;
1343 }, $ifNoneMatch );
1344 $return304 = in_array( $test, $match, true );
1345 } else {
1346 $value = trim( $this->getRequest()->getHeader( 'If-Modified-Since' ) );
1347
1348 // Some old browsers sends sizes after the date, like this:
1349 // Wed, 20 Aug 2003 06:51:19 GMT; length=5202
1350 // Ignore that.
1351 $i = strpos( $value, ';' );
1352 if ( $i !== false ) {
1353 $value = trim( substr( $value, 0, $i ) );
1354 }
1355
1356 if ( $value !== '' ) {
1357 try {
1358 $ts = new MWTimestamp( $value );
1359 if (
1360 // RFC 7231 IMF-fixdate
1361 $ts->getTimestamp( TS_RFC2822 ) === $value ||
1362 // RFC 850
1363 $ts->format( 'l, d-M-y H:i:s' ) . ' GMT' === $value ||
1364 // asctime (with and without space-padded day)
1365 $ts->format( 'D M j H:i:s Y' ) === $value ||
1366 $ts->format( 'D M j H:i:s Y' ) === $value
1367 ) {
1368 $config = $this->getConfig();
1369 $lastMod = $module->getConditionalRequestData( 'last-modified' );
1370 if ( $lastMod !== null ) {
1371 // Mix in some MediaWiki modification times
1372 $modifiedTimes = [
1373 'page' => $lastMod,
1374 'user' => $this->getUser()->getTouched(),
1375 'epoch' => $config->get( 'CacheEpoch' ),
1376 ];
1377
1378 if ( $config->get( 'UseCdn' ) ) {
1379 // T46570: the core page itself may not change, but resources might
1380 $modifiedTimes['sepoch'] = wfTimestamp(
1381 TS_MW, time() - $config->get( 'CdnMaxAge' )
1382 );
1383 }
1384 Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this->getOutput() ] );
1385 $lastMod = max( $modifiedTimes );
1386 $return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
1387 }
1388 }
1389 } catch ( TimestampException $e ) {
1390 // Invalid timestamp, ignore it
1391 }
1392 }
1393 }
1394
1395 if ( $return304 ) {
1396 $this->getRequest()->response()->statusHeader( 304 );
1397
1398 // Avoid outputting the compressed representation of a zero-length body
1399 Wikimedia\suppressWarnings();
1400 ini_set( 'zlib.output_compression', 0 );
1401 Wikimedia\restoreWarnings();
1403
1404 return false;
1405 }
1406
1407 return true;
1408 }
1409
1414 protected function checkExecutePermissions( $module ) {
1415 $user = $this->getUser();
1416 if ( $module->isReadMode() && !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) &&
1417 !$this->getPermissionManager()->userHasRight( $user, 'read' )
1418 ) {
1419 $this->dieWithError( 'apierror-readapidenied' );
1420 }
1421
1422 if ( $module->isWriteMode() ) {
1423 if ( !$this->mEnableWrite ) {
1424 $this->dieWithError( 'apierror-noapiwrite' );
1425 } elseif ( !$this->getPermissionManager()->userHasRight( $user, 'writeapi' ) ) {
1426 $this->dieWithError( 'apierror-writeapidenied' );
1427 } elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) {
1428 $this->dieWithError( 'apierror-promised-nonwrite-api' );
1429 }
1430
1431 $this->checkReadOnly( $module );
1432 }
1433
1434 // Allow extensions to stop execution for arbitrary reasons.
1435 $message = 'hookaborted';
1436 if ( !Hooks::run( 'ApiCheckCanExecute', [ $module, $user, &$message ] ) ) {
1437 $this->dieWithError( $message );
1438 }
1439 }
1440
1445 protected function checkReadOnly( $module ) {
1446 if ( wfReadOnly() ) {
1447 $this->dieReadOnly();
1448 }
1449
1450 if ( $module->isWriteMode()
1451 && $this->getUser()->isBot()
1452 && MediaWikiServices::getInstance()->getDBLoadBalancer()->getServerCount() > 1
1453 ) {
1454 $this->checkBotReadOnly();
1455 }
1456 }
1457
1461 private function checkBotReadOnly() {
1462 // Figure out how many servers have passed the lag threshold
1463 $numLagged = 0;
1464 $lagLimit = $this->getConfig()->get( 'APIMaxLagThreshold' );
1465 $laggedServers = [];
1466 $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
1467 foreach ( $loadBalancer->getLagTimes() as $serverIndex => $lag ) {
1468 if ( $lag > $lagLimit ) {
1469 ++$numLagged;
1470 $laggedServers[] = $loadBalancer->getServerName( $serverIndex ) . " ({$lag}s)";
1471 }
1472 }
1473
1474 // If a majority of replica DBs are too lagged then disallow writes
1475 $replicaCount = $loadBalancer->getServerCount() - 1;
1476 if ( $numLagged >= ceil( $replicaCount / 2 ) ) {
1477 $laggedServers = implode( ', ', $laggedServers );
1478 wfDebugLog(
1479 'api-readonly', // Deprecate this channel in favor of api-warning?
1480 "Api request failed as read only because the following DBs are lagged: $laggedServers"
1481 );
1482 LoggerFactory::getInstance( 'api-warning' )->warning(
1483 "Api request failed as read only because the following DBs are lagged: {laggeddbs}", [
1484 'laggeddbs' => $laggedServers,
1485 ]
1486 );
1487
1488 $this->dieWithError(
1489 'readonly_lag',
1490 'readonly',
1491 [ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ]
1492 );
1493 }
1494 }
1495
1500 protected function checkAsserts( $params ) {
1501 if ( isset( $params['assert'] ) ) {
1502 $user = $this->getUser();
1503 switch ( $params['assert'] ) {
1504 case 'user':
1505 if ( $user->isAnon() ) {
1506 $this->dieWithError( 'apierror-assertuserfailed' );
1507 }
1508 break;
1509 case 'bot':
1510 if ( !$this->getPermissionManager()->userHasRight( $user, 'bot' ) ) {
1511 $this->dieWithError( 'apierror-assertbotfailed' );
1512 }
1513 break;
1514 }
1515 }
1516 if ( isset( $params['assertuser'] ) ) {
1517 $assertUser = User::newFromName( $params['assertuser'], false );
1518 if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) {
1519 $this->dieWithError(
1520 [ 'apierror-assertnameduserfailed', wfEscapeWikiText( $params['assertuser'] ) ]
1521 );
1522 }
1523 }
1524 }
1525
1531 protected function setupExternalResponse( $module, $params ) {
1532 $validMethods = [ 'GET', 'HEAD', 'POST', 'OPTIONS' ];
1533 $request = $this->getRequest();
1534
1535 if ( !in_array( $request->getMethod(), $validMethods ) ) {
1536 $this->dieWithError( 'apierror-invalidmethod', null, null, 405 );
1537 }
1538
1539 if ( !$request->wasPosted() && $module->mustBePosted() ) {
1540 // Module requires POST. GET request might still be allowed
1541 // if $wgDebugApi is true, otherwise fail.
1542 $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] );
1543 }
1544
1545 if ( $request->wasPosted() && !$request->getHeader( 'Content-Type' ) ) {
1546 $this->addDeprecation(
1547 'apiwarn-deprecation-post-without-content-type', 'post-without-content-type'
1548 );
1549 }
1550
1551 // See if custom printer is used
1552 $this->mPrinter = $module->getCustomPrinter();
1553 if ( is_null( $this->mPrinter ) ) {
1554 // Create an appropriate printer
1555 $this->mPrinter = $this->createPrinterByName( $params['format'] );
1556 }
1557
1558 if ( $request->getProtocol() === 'http' &&
1559 (
1560 $this->getConfig()->get( 'ForceHTTPS' ) ||
1561 $request->getSession()->shouldForceHTTPS() ||
1562 ( $this->getUser()->isLoggedIn() &&
1563 $this->getUser()->requiresHTTPS() )
1564 )
1565 ) {
1566 $this->addDeprecation( 'apiwarn-deprecation-httpsexpected', 'https-expected' );
1567 }
1568 }
1569
1573 protected function executeAction() {
1574 $params = $this->setupExecuteAction();
1575
1576 // Check asserts early so e.g. errors in parsing a module's parameters due to being
1577 // logged out don't override the client's intended "am I logged in?" check.
1578 $this->checkAsserts( $params );
1579
1580 $module = $this->setupModule();
1581 $this->mModule = $module;
1582
1583 if ( !$this->mInternalMode ) {
1584 $this->setRequestExpectations( $module );
1585 }
1586
1587 $this->checkExecutePermissions( $module );
1588
1589 if ( !$this->checkMaxLag( $module, $params ) ) {
1590 return;
1591 }
1592
1593 if ( !$this->checkConditionalRequestHeaders( $module ) ) {
1594 return;
1595 }
1596
1597 if ( !$this->mInternalMode ) {
1598 $this->setupExternalResponse( $module, $params );
1599 }
1600
1601 $module->execute();
1602 Hooks::run( 'APIAfterExecute', [ &$module ] );
1603
1604 $this->reportUnusedParams();
1605
1606 if ( !$this->mInternalMode ) {
1607 MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
1608
1609 $this->printResult();
1610 }
1611 }
1612
1617 protected function setRequestExpectations( ApiBase $module ) {
1618 $limits = $this->getConfig()->get( 'TrxProfilerLimits' );
1619 $trxProfiler = Profiler::instance()->getTransactionProfiler();
1620 $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
1621 if ( $this->getRequest()->hasSafeMethod() ) {
1622 $trxProfiler->setExpectations( $limits['GET'], __METHOD__ );
1623 } elseif ( $this->getRequest()->wasPosted() && !$module->isWriteMode() ) {
1624 $trxProfiler->setExpectations( $limits['POST-nonwrite'], __METHOD__ );
1625 $this->getRequest()->markAsSafeRequest();
1626 } else {
1627 $trxProfiler->setExpectations( $limits['POST'], __METHOD__ );
1628 }
1629 }
1630
1636 protected function logRequest( $time, $e = null ) {
1637 $request = $this->getRequest();
1638
1639 $logCtx = [
1640 // https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/event-schemas/+/master/jsonschema/mediawiki/api/request
1641 '$schema' => '/mediawiki/api/request/0.0.1',
1642 'meta' => [
1643 'request_id' => WebRequest::getRequestId(),
1644 'id' => UIDGenerator::newUUIDv4(),
1645 'dt' => wfTimestamp( TS_ISO_8601 ),
1646 'domain' => $this->getConfig()->get( 'ServerName' ),
1647 // If using the EventBus extension (as intended) with this log channel,
1648 // this stream name will map to a Kafka topic.
1649 'stream' => 'mediawiki.api-request'
1650 ],
1651 'http' => [
1652 'method' => $request->getMethod(),
1653 'client_ip' => $request->getIP()
1654 ],
1655 'database' => WikiMap::getCurrentWikiDbDomain()->getId(),
1656 'backend_time_ms' => (int)round( $time * 1000 ),
1657 ];
1658
1659 // If set, these headers will be logged in http.request_headers.
1660 $httpRequestHeadersToLog = [ 'accept-language', 'referer', 'user-agent' ];
1661 foreach ( $httpRequestHeadersToLog as $header ) {
1662 if ( $request->getHeader( $header ) ) {
1663 // Set the header in http.request_headers
1664 $logCtx['http']['request_headers'][$header] = $request->getHeader( $header );
1665 }
1666 }
1667
1668 if ( $e ) {
1669 $logCtx['api_error_codes'] = [];
1670 foreach ( $this->errorMessagesFromException( $e ) as $msg ) {
1671 $logCtx['api_error_codes'][] = $msg->getApiCode();
1672 }
1673 }
1674
1675 // Construct space separated message for 'api' log channel
1676 $msg = "API {$request->getMethod()} " .
1677 wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
1678 " {$logCtx['http']['client_ip']} " .
1679 "T={$logCtx['backend_time_ms']}ms";
1680
1681 $sensitive = array_flip( $this->getSensitiveParams() );
1682 foreach ( $this->getParamsUsed() as $name ) {
1683 $value = $request->getVal( $name );
1684 if ( $value === null ) {
1685 continue;
1686 }
1687
1688 if ( isset( $sensitive[$name] ) ) {
1689 $value = '[redacted]';
1690 $encValue = '[redacted]';
1691 } elseif ( strlen( $value ) > 256 ) {
1692 $value = substr( $value, 0, 256 );
1693 $encValue = $this->encodeRequestLogValue( $value ) . '[...]';
1694 } else {
1695 $encValue = $this->encodeRequestLogValue( $value );
1696 }
1697
1698 $logCtx['params'][$name] = $value;
1699 $msg .= " {$name}={$encValue}";
1700 }
1701
1702 // Log an unstructured message to the api channel.
1703 wfDebugLog( 'api', $msg, 'private' );
1704
1705 // The api-request channel a structured data log channel.
1706 wfDebugLog( 'api-request', '', 'private', $logCtx );
1707 }
1708
1714 protected function encodeRequestLogValue( $s ) {
1715 static $table = [];
1716 if ( !$table ) {
1717 $chars = ';@$!*(),/:';
1718 $numChars = strlen( $chars );
1719 for ( $i = 0; $i < $numChars; $i++ ) {
1720 $table[rawurlencode( $chars[$i] )] = $chars[$i];
1721 }
1722 }
1723
1724 return strtr( rawurlencode( $s ), $table );
1725 }
1726
1731 protected function getParamsUsed() {
1732 return array_keys( $this->mParamsUsed );
1733 }
1734
1739 public function markParamsUsed( $params ) {
1740 $this->mParamsUsed += array_fill_keys( (array)$params, true );
1741 }
1742
1748 protected function getSensitiveParams() {
1749 return array_keys( $this->mParamsSensitive );
1750 }
1751
1757 public function markParamsSensitive( $params ) {
1758 $this->mParamsSensitive += array_fill_keys( (array)$params, true );
1759 }
1760
1767 public function getVal( $name, $default = null ) {
1768 $this->mParamsUsed[$name] = true;
1769
1770 $ret = $this->getRequest()->getVal( $name );
1771 if ( $ret === null ) {
1772 if ( $this->getRequest()->getArray( $name ) !== null ) {
1773 // See T12262 for why we don't just implode( '|', ... ) the
1774 // array.
1775 $this->addWarning( [ 'apiwarn-unsupportedarray', $name ] );
1776 }
1777 $ret = $default;
1778 }
1779 return $ret;
1780 }
1781
1788 public function getCheck( $name ) {
1789 return $this->getVal( $name, null ) !== null;
1790 }
1791
1799 public function getUpload( $name ) {
1800 $this->mParamsUsed[$name] = true;
1801
1802 return $this->getRequest()->getUpload( $name );
1803 }
1804
1809 protected function reportUnusedParams() {
1810 $paramsUsed = $this->getParamsUsed();
1811 $allParams = $this->getRequest()->getValueNames();
1812
1813 if ( !$this->mInternalMode ) {
1814 // Printer has not yet executed; don't warn that its parameters are unused
1815 $printerParams = $this->mPrinter->encodeParamName(
1816 array_keys( $this->mPrinter->getFinalParams() ?: [] )
1817 );
1818 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
1819 } else {
1820 $unusedParams = array_diff( $allParams, $paramsUsed );
1821 }
1822
1823 if ( count( $unusedParams ) ) {
1824 $this->addWarning( [
1825 'apierror-unrecognizedparams',
1826 Message::listParam( array_map( 'wfEscapeWikiText', $unusedParams ), 'comma' ),
1827 count( $unusedParams )
1828 ] );
1829 }
1830 }
1831
1837 protected function printResult( $httpCode = 0 ) {
1838 if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
1839 $this->addWarning( 'apiwarn-wgdebugapi' );
1840 }
1841
1842 $printer = $this->mPrinter;
1843 $printer->initPrinter( false );
1844 if ( $httpCode ) {
1845 $printer->setHttpStatus( $httpCode );
1846 }
1847 $printer->execute();
1848 $printer->closePrinter();
1849 }
1850
1854 public function isReadMode() {
1855 return false;
1856 }
1857
1863 public function getAllowedParams() {
1864 return [
1865 'action' => [
1866 ApiBase::PARAM_DFLT => 'help',
1867 ApiBase::PARAM_TYPE => 'submodule',
1868 ],
1869 'format' => [
1870 ApiBase::PARAM_DFLT => self::API_DEFAULT_FORMAT,
1871 ApiBase::PARAM_TYPE => 'submodule',
1872 ],
1873 'maxlag' => [
1874 ApiBase::PARAM_TYPE => 'integer'
1875 ],
1876 'smaxage' => [
1877 ApiBase::PARAM_TYPE => 'integer',
1879 ],
1880 'maxage' => [
1881 ApiBase::PARAM_TYPE => 'integer',
1883 ],
1884 'assert' => [
1885 ApiBase::PARAM_TYPE => [ 'user', 'bot' ]
1886 ],
1887 'assertuser' => [
1888 ApiBase::PARAM_TYPE => 'user',
1889 ],
1890 'requestid' => null,
1891 'servedby' => false,
1892 'curtimestamp' => false,
1893 'responselanginfo' => false,
1894 'origin' => null,
1895 'uselang' => [
1896 ApiBase::PARAM_DFLT => self::API_DEFAULT_USELANG,
1897 ],
1898 'errorformat' => [
1899 ApiBase::PARAM_TYPE => [ 'plaintext', 'wikitext', 'html', 'raw', 'none', 'bc' ],
1900 ApiBase::PARAM_DFLT => 'bc',
1901 ],
1902 'errorlang' => [
1903 ApiBase::PARAM_DFLT => 'uselang',
1904 ],
1905 'errorsuselocal' => [
1906 ApiBase::PARAM_DFLT => false,
1907 ],
1908 ];
1909 }
1910
1912 protected function getExamplesMessages() {
1913 return [
1914 'action=help'
1915 => 'apihelp-help-example-main',
1916 'action=help&recursivesubmodules=1'
1917 => 'apihelp-help-example-recursive',
1918 ];
1919 }
1920
1925 public function modifyHelp( array &$help, array $options, array &$tocData ) {
1926 // Wish PHP had an "array_insert_before". Instead, we have to manually
1927 // reindex the array to get 'permissions' in the right place.
1928 $oldHelp = $help;
1929 $help = [];
1930 foreach ( $oldHelp as $k => $v ) {
1931 if ( $k === 'submodules' ) {
1932 $help['permissions'] = '';
1933 }
1934 $help[$k] = $v;
1935 }
1936 $help['datatypes'] = '';
1937 $help['templatedparams'] = '';
1938 $help['credits'] = '';
1939
1940 // Fill 'permissions'
1941 $help['permissions'] .= Html::openElement( 'div',
1942 [ 'class' => 'apihelp-block apihelp-permissions' ] );
1943 $m = $this->msg( 'api-help-permissions' );
1944 if ( !$m->isDisabled() ) {
1945 $help['permissions'] .= Html::rawElement( 'div', [ 'class' => 'apihelp-block-head' ],
1946 $m->numParams( count( self::$mRights ) )->parse()
1947 );
1948 }
1949 $help['permissions'] .= Html::openElement( 'dl' );
1950 foreach ( self::$mRights as $right => $rightMsg ) {
1951 $help['permissions'] .= Html::element( 'dt', null, $right );
1952
1953 $rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
1954 $help['permissions'] .= Html::rawElement( 'dd', null, $rightMsg );
1955
1956 $groups = array_map( function ( $group ) {
1957 return $group == '*' ? 'all' : $group;
1958 }, $this->getPermissionManager()->getGroupsWithPermission( $right ) );
1959
1960 $help['permissions'] .= Html::rawElement( 'dd', null,
1961 $this->msg( 'api-help-permissions-granted-to' )
1962 ->numParams( count( $groups ) )
1963 ->params( Message::listParam( $groups ) )
1964 ->parse()
1965 );
1966 }
1967 $help['permissions'] .= Html::closeElement( 'dl' );
1968 $help['permissions'] .= Html::closeElement( 'div' );
1969
1970 // Fill 'datatypes', 'templatedparams', and 'credits', if applicable
1971 if ( empty( $options['nolead'] ) ) {
1972 $level = $options['headerlevel'];
1973 $tocnumber = &$options['tocnumber'];
1974
1975 $header = $this->msg( 'api-help-datatypes-header' )->parse();
1976
1977 $id = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_PRIMARY );
1978 $idFallback = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_FALLBACK );
1979 $headline = Linker::makeHeadline( min( 6, $level ),
1980 ' class="apihelp-header">',
1981 $id,
1982 $header,
1983 '',
1984 $idFallback
1985 );
1986 // Ensure we have a sane anchor
1987 if ( $id !== 'main/datatypes' && $idFallback !== 'main/datatypes' ) {
1988 $headline = '<div id="main/datatypes"></div>' . $headline;
1989 }
1990 $help['datatypes'] .= $headline;
1991 $help['datatypes'] .= $this->msg( 'api-help-datatypes' )->parseAsBlock();
1992 if ( !isset( $tocData['main/datatypes'] ) ) {
1993 $tocnumber[$level]++;
1994 $tocData['main/datatypes'] = [
1995 'toclevel' => count( $tocnumber ),
1996 'level' => $level,
1997 'anchor' => 'main/datatypes',
1998 'line' => $header,
1999 'number' => implode( '.', $tocnumber ),
2000 'index' => false,
2001 ];
2002 }
2003
2004 $header = $this->msg( 'api-help-templatedparams-header' )->parse();
2005
2006 $id = Sanitizer::escapeIdForAttribute( 'main/templatedparams', Sanitizer::ID_PRIMARY );
2007 $idFallback = Sanitizer::escapeIdForAttribute( 'main/templatedparams', Sanitizer::ID_FALLBACK );
2008 $headline = Linker::makeHeadline( min( 6, $level ),
2009 ' class="apihelp-header">',
2010 $id,
2011 $header,
2012 '',
2013 $idFallback
2014 );
2015 // Ensure we have a sane anchor
2016 if ( $id !== 'main/templatedparams' && $idFallback !== 'main/templatedparams' ) {
2017 $headline = '<div id="main/templatedparams"></div>' . $headline;
2018 }
2019 $help['templatedparams'] .= $headline;
2020 $help['templatedparams'] .= $this->msg( 'api-help-templatedparams' )->parseAsBlock();
2021 if ( !isset( $tocData['main/templatedparams'] ) ) {
2022 $tocnumber[$level]++;
2023 $tocData['main/templatedparams'] = [
2024 'toclevel' => count( $tocnumber ),
2025 'level' => $level,
2026 'anchor' => 'main/templatedparams',
2027 'line' => $header,
2028 'number' => implode( '.', $tocnumber ),
2029 'index' => false,
2030 ];
2031 }
2032
2033 $header = $this->msg( 'api-credits-header' )->parse();
2034 $id = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_PRIMARY );
2035 $idFallback = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_FALLBACK );
2036 $headline = Linker::makeHeadline( min( 6, $level ),
2037 ' class="apihelp-header">',
2038 $id,
2039 $header,
2040 '',
2041 $idFallback
2042 );
2043 // Ensure we have a sane anchor
2044 if ( $id !== 'main/credits' && $idFallback !== 'main/credits' ) {
2045 $headline = '<div id="main/credits"></div>' . $headline;
2046 }
2047 $help['credits'] .= $headline;
2048 $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
2049 if ( !isset( $tocData['main/credits'] ) ) {
2050 $tocnumber[$level]++;
2051 $tocData['main/credits'] = [
2052 'toclevel' => count( $tocnumber ),
2053 'level' => $level,
2054 'anchor' => 'main/credits',
2055 'line' => $header,
2056 'number' => implode( '.', $tocnumber ),
2057 'index' => false,
2058 ];
2059 }
2060 }
2061 }
2062
2063 private $mCanApiHighLimits = null;
2064
2069 public function canApiHighLimits() {
2070 if ( !isset( $this->mCanApiHighLimits ) ) {
2071 $this->mCanApiHighLimits = $this->getPermissionManager()
2072 ->userHasRight( $this->getUser(), 'apihighlimits' );
2073 }
2074
2075 return $this->mCanApiHighLimits;
2076 }
2077
2082 public function getModuleManager() {
2083 return $this->mModuleMgr;
2084 }
2085
2094 public function getUserAgent() {
2095 return trim(
2096 $this->getRequest()->getHeader( 'Api-user-agent' ) . ' ' .
2097 $this->getRequest()->getHeader( 'User-agent' )
2098 );
2099 }
2100}
2101
getPermissionManager()
getUser()
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
wfReadOnly()
Check whether the wiki is in read-only mode.
wfHostname()
Get host name of the current machine, for use in error reporting.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfClearOutputBuffers()
More legible than passing a 'false' parameter to wfResetOutputBuffers():
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
getContext()
$wgLang
Definition Setup.php:880
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:42
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:876
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition ApiBase.php:2014
dieWithErrorOrDebug( $msg, $code=null, $data=null, $httpCode=null)
Will only set a warning instead of failing if the global $wgDebugAPI is set to true.
Definition ApiBase.php:2191
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below.
Definition ApiBase.php:94
isWriteMode()
Indicates whether this module requires write mode.
Definition ApiBase.php:427
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
Definition ApiBase.php:55
dieReadOnly()
Helper function for readonly errors.
Definition ApiBase.php:2114
addDeprecation( $msg, $feature, $data=[])
Add a deprecation warning for this module.
Definition ApiBase.php:1947
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition ApiBase.php:265
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:761
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition ApiBase.php:1933
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:261
This manages continuation state.
Format errors and warnings in the old style, for backwards compatibility.
Formats errors and warnings for the API, and add them to the associated ApiResult.
static isValidApiCode( $code)
Test whether a code is a valid API error code.
This is the abstract base class for API formatters.
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:41
static handleApiBeforeMainException( $e)
Handle an exception from the ApiBeforeMain hook.
Definition ApiMain.php:647
getExamplesMessages()
Returns usage examples for this module.Return value has query strings as keys, with values being eith...
Definition ApiMain.php:1912
isReadMode()
Definition ApiMain.php:1854
setRequestExpectations(ApiBase $module)
Set database connection, query, and write expectations given this module request.
Definition ApiMain.php:1617
getAllowedParams()
See ApiBase for description.
Definition ApiMain.php:1863
getSensitiveParams()
Get the request parameters that should be considered sensitive.
Definition ApiMain.php:1748
getPrinter()
Get the result formatter object.
Definition ApiMain.php:397
logRequest( $time, $e=null)
Log the preceding request.
Definition ApiMain.php:1636
array $mCacheControl
Definition ApiMain.php:157
static $Modules
List of available modules: action name => module class.
Definition ApiMain.php:55
sendCacheHeaders( $isError)
Send caching headers.
Definition ApiMain.php:883
encodeRequestLogValue( $s)
Encode a value in a format suitable for a space-separated log line.
Definition ApiMain.php:1714
ApiFormatBase $mPrinter
Definition ApiMain.php:144
getMaxLag()
Definition ApiMain.php:1234
markParamsUsed( $params)
Mark parameters as used.
Definition ApiMain.php:1739
executeActionWithErrorHandling()
Execute an action, and in case of an error, erase whatever partial results have been accumulated,...
Definition ApiMain.php:516
$mParamsSensitive
Definition ApiMain.php:159
createPrinterByName( $format)
Create an instance of an output formatter by its name.
Definition ApiMain.php:490
setCacheMaxAge( $maxage)
Set how long the response should be cached.
Definition ApiMain.php:406
static $mRights
List of user roles that are specifically relevant to the API.
Definition ApiMain.php:130
getResult()
Get the ApiResult object associated with current request.
Definition ApiMain.php:314
createErrorPrinter()
Create the printer for error output.
Definition ApiMain.php:989
executeAction()
Execute the actual module, without any error handling.
Definition ApiMain.php:1573
getErrorFormatter()
Get the ApiErrorFormatter object associated with current request.
Definition ApiMain.php:357
checkMaxLag( $module, $params)
Check the max lag if necessary.
Definition ApiMain.php:1268
checkAsserts( $params)
Check asserts of the user's rights.
Definition ApiMain.php:1500
$mErrorFormatter
Definition ApiMain.php:146
setupExternalResponse( $module, $params)
Check POST for external response and setup result printer.
Definition ApiMain.php:1531
static matchRequestedHeaders( $requestedHeaders)
Attempt to validate the value of Access-Control-Request-Headers against a list of headers that we all...
Definition ApiMain.php:826
bool null $lacksSameOriginSecurity
Cached return value from self::lacksSameOriginSecurity()
Definition ApiMain.php:162
setCacheControl( $directives)
Set directives (key/value pairs) for the Cache-Control header.
Definition ApiMain.php:479
static wildcardToRegex( $wildcard)
Helper function to convert wildcard string into a regex '*' => '.
Definition ApiMain.php:867
setContinuationManager(ApiContinuationManager $manager=null)
Set the continuation manager.
Definition ApiMain.php:373
setCacheMode( $mode)
Set the type of caching headers which will be sent.
Definition ApiMain.php:438
setupModule()
Set up the module for response.
Definition ApiMain.php:1187
markParamsSensitive( $params)
Mark parameters as sensitive.
Definition ApiMain.php:1757
ApiBase $mModule
Definition ApiMain.php:153
setupExecuteAction()
Set up for the execution.
Definition ApiMain.php:1172
checkConditionalRequestHeaders( $module)
Check selected RFC 7232 precondition headers.
Definition ApiMain.php:1314
checkBotReadOnly()
Check whether we are readonly for bots.
Definition ApiMain.php:1461
handleException( $e)
Handle an exception as an API response.
Definition ApiMain.php:573
getUserAgent()
Fetches the user agent used for this request.
Definition ApiMain.php:2094
const API_DEFAULT_USELANG
When no uselang parameter is given, this language will be used.
Definition ApiMain.php:50
static matchOrigin( $value, $rules, $exceptions)
Attempt to match an Origin header against a set of rules and a set of exceptions.
Definition ApiMain.php:802
getModule()
Get the API module object.
Definition ApiMain.php:388
__construct( $context=null, $enableWrite=false)
Constructs an instance of ApiMain that utilizes the module and format specified by $request.
Definition ApiMain.php:172
isInternalMode()
Return true if the API was started by other PHP code using FauxRequest.
Definition ApiMain.php:305
addRequestedFields( $force=[])
Add requested fields to the result.
Definition ApiMain.php:1142
modifyHelp(array &$help, array $options, array &$tocData)
Called from ApiHelp before the pieces are joined together and returned.This exists mainly for ApiMain...
Definition ApiMain.php:1925
checkReadOnly( $module)
Check if the DB is read-only for this user.
Definition ApiMain.php:1445
getCheck( $name)
Get a boolean request value, and register the fact that the parameter was used, for logging.
Definition ApiMain.php:1788
$mInternalMode
Definition ApiMain.php:151
getContinuationManager()
Get the continuation manager.
Definition ApiMain.php:365
printResult( $httpCode=0)
Print results using the current printer.
Definition ApiMain.php:1837
reportUnusedParams()
Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,...
Definition ApiMain.php:1809
lacksSameOriginSecurity()
Get the security flag for the current request.
Definition ApiMain.php:322
getVal( $name, $default=null)
Get a request value, and register the fact that it was used, for logging.
Definition ApiMain.php:1767
getParamsUsed()
Get the request parameters used in the course of the preceding execute() request.
Definition ApiMain.php:1731
getModuleManager()
Overrides to return this instance's module manager.
Definition ApiMain.php:2082
ApiContinuationManager null $mContinuationManager
Definition ApiMain.php:148
substituteResultWithError( $e)
Replace the result data with the information about an exception.
Definition ApiMain.php:1058
const API_DEFAULT_FORMAT
When no format parameter is given, this format will be used.
Definition ApiMain.php:45
checkExecutePermissions( $module)
Check for sufficient permissions to execute.
Definition ApiMain.php:1414
getUpload( $name)
Get a request upload, and register the fact that it was used, for logging.
Definition ApiMain.php:1799
canApiHighLimits()
Check whether the current user is allowed to use high limits.
Definition ApiMain.php:2069
static $Formats
List of available formats: format name => format class.
Definition ApiMain.php:113
$mCanApiHighLimits
Definition ApiMain.php:2063
errorMessagesFromException( $e, $type='error')
Create an error message for the given exception.
Definition ApiMain.php:1020
handleCORS()
Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
Definition ApiMain.php:682
execute()
Execute api request.
Definition ApiMain.php:504
$mEnableWrite
Definition ApiMain.php:150
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
This class holds a list of modules and handles instantiation.
This class represents the result of the API operations.
Definition ApiResult.php:35
Exception used to abort API execution with an error.
getModulePath()
Fetch the responsible module name.
getStatusValue()
Fetch the error status.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
setContext(IContextSource $context)
An IContextSource implementation which will inherit context from another source but allow individual ...
WebRequest clone which takes values from a provided array.
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition Linker.php:1754
MediaWiki exception.
Library for creating and parsing MW-style timestamps.
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static listParam(array $list, $type='text')
Definition Message.php:1115
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition Message.php:427
static newUUIDv4( $flags=0)
Return an RFC4122 compliant v4 UUID.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Interface for MediaWiki-localized exceptions.
$context
Definition load.php:45
if(PHP_SAPI !=='cli' &&PHP_SAPI !=='phpdbg' $chars)
$help
Definition mcc.php:32
This class serves as a utility class for this extension.
$header