MediaWiki REL1_33
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';
156 private $mCacheControl = [];
157 private $mParamsUsed = [];
158 private $mParamsSensitive = [];
159
162
170 public function __construct( $context = null, $enableWrite = false ) {
171 if ( $context === null ) {
172 $context = RequestContext::getMain();
173 } elseif ( $context instanceof WebRequest ) {
174 // BC for pre-1.19
176 $context = RequestContext::getMain();
177 }
178 // We set a derivative context so we can change stuff later
179 $this->setContext( new DerivativeContext( $context ) );
180
181 if ( isset( $request ) ) {
182 $this->getContext()->setRequest( $request );
183 } else {
184 $request = $this->getRequest();
185 }
186
187 $this->mInternalMode = ( $request instanceof FauxRequest );
188
189 // Special handling for the main module: $parent === $this
190 parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
191
192 $config = $this->getConfig();
193
194 if ( !$this->mInternalMode ) {
195 // Log if a request with a non-whitelisted Origin header is seen
196 // with session cookies.
197 $originHeader = $request->getHeader( 'Origin' );
198 if ( $originHeader === false ) {
199 $origins = [];
200 } else {
201 $originHeader = trim( $originHeader );
202 $origins = preg_split( '/\s+/', $originHeader );
203 }
204 $sessionCookies = array_intersect(
205 array_keys( $_COOKIE ),
206 MediaWiki\Session\SessionManager::singleton()->getVaryCookies()
207 );
208 if ( $origins && $sessionCookies && (
209 count( $origins ) !== 1 || !self::matchOrigin(
210 $origins[0],
211 $config->get( 'CrossSiteAJAXdomains' ),
212 $config->get( 'CrossSiteAJAXdomainExceptions' )
213 )
214 ) ) {
215 LoggerFactory::getInstance( 'cors' )->warning(
216 'Non-whitelisted CORS request with session cookies', [
217 'origin' => $originHeader,
218 'cookies' => $sessionCookies,
219 'ip' => $request->getIP(),
220 'userAgent' => $this->getUserAgent(),
221 'wiki' => WikiMap::getCurrentWikiDbDomain()->getId(),
222 ]
223 );
224 }
225
226 // If we're in a mode that breaks the same-origin policy, strip
227 // user credentials for security.
228 if ( $this->lacksSameOriginSecurity() ) {
229 global $wgUser;
230 wfDebug( "API: stripping user credentials when the same-origin policy is not applied\n" );
231 $wgUser = new User();
232 $this->getContext()->setUser( $wgUser );
233 $request->response()->header( 'MediaWiki-Login-Suppressed: true' );
234 }
235 }
236
237 $this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
238
239 // Setup uselang. This doesn't use $this->getParameter()
240 // because we're not ready to handle errors yet.
241 $uselang = $request->getVal( 'uselang', self::API_DEFAULT_USELANG );
242 if ( $uselang === 'user' ) {
243 // Assume the parent context is going to return the user language
244 // for uselang=user (see T85635).
245 } else {
246 if ( $uselang === 'content' ) {
247 $uselang = MediaWikiServices::getInstance()->getContentLanguage()->getCode();
248 }
249 $code = RequestContext::sanitizeLangCode( $uselang );
250 $this->getContext()->setLanguage( $code );
251 if ( !$this->mInternalMode ) {
252 global $wgLang;
253 $wgLang = $this->getContext()->getLanguage();
254 RequestContext::getMain()->setLanguage( $wgLang );
255 }
256 }
257
258 // Set up the error formatter. This doesn't use $this->getParameter()
259 // because we're not ready to handle errors yet.
260 $errorFormat = $request->getVal( 'errorformat', 'bc' );
261 $errorLangCode = $request->getVal( 'errorlang', 'uselang' );
262 $errorsUseDB = $request->getCheck( 'errorsuselocal' );
263 if ( in_array( $errorFormat, [ 'plaintext', 'wikitext', 'html', 'raw', 'none' ], true ) ) {
264 if ( $errorLangCode === 'uselang' ) {
265 $errorLang = $this->getLanguage();
266 } elseif ( $errorLangCode === 'content' ) {
267 $errorLang = MediaWikiServices::getInstance()->getContentLanguage();
268 } else {
269 $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode );
270 $errorLang = Language::factory( $errorLangCode );
271 }
272 $this->mErrorFormatter = new ApiErrorFormatter(
273 $this->mResult, $errorLang, $errorFormat, $errorsUseDB
274 );
275 } else {
276 $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
277 }
278 $this->mResult->setErrorFormatter( $this->getErrorFormatter() );
279
280 $this->mModuleMgr = new ApiModuleManager( $this );
281 $this->mModuleMgr->addModules( self::$Modules, 'action' );
282 $this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
283 $this->mModuleMgr->addModules( self::$Formats, 'format' );
284 $this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' );
285
286 Hooks::run( 'ApiMain::moduleManager', [ $this->mModuleMgr ] );
287
288 $this->mContinuationManager = null;
289 $this->mEnableWrite = $enableWrite;
290
291 $this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling()
292 $this->mCommit = false;
293 }
294
299 public function isInternalMode() {
301 }
302
308 public function getResult() {
309 return $this->mResult;
310 }
311
316 public function lacksSameOriginSecurity() {
317 if ( $this->lacksSameOriginSecurity !== null ) {
319 }
320
321 $request = $this->getRequest();
322
323 // JSONP mode
324 if ( $request->getCheck( 'callback' ) ) {
325 $this->lacksSameOriginSecurity = true;
326 return true;
327 }
328
329 // Anonymous CORS
330 if ( $request->getVal( 'origin' ) === '*' ) {
331 $this->lacksSameOriginSecurity = true;
332 return true;
333 }
334
335 // Header to be used from XMLHTTPRequest when the request might
336 // otherwise be used for XSS.
337 if ( $request->getHeader( 'Treat-as-Untrusted' ) !== false ) {
338 $this->lacksSameOriginSecurity = true;
339 return true;
340 }
341
342 // Allow extensions to override.
343 $this->lacksSameOriginSecurity = !Hooks::run( 'RequestHasSameOriginSecurity', [ $request ] );
345 }
346
351 public function getErrorFormatter() {
353 }
354
359 public function getContinuationManager() {
361 }
362
367 public function setContinuationManager( ApiContinuationManager $manager = null ) {
368 if ( $manager !== null && $this->mContinuationManager !== null ) {
369 throw new UnexpectedValueException(
370 __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
371 ' when a manager is already set from ' . $this->mContinuationManager->getSource()
372 );
373 }
374 $this->mContinuationManager = $manager;
375 }
376
382 public function getModule() {
383 return $this->mModule;
384 }
385
391 public function getPrinter() {
392 return $this->mPrinter;
393 }
394
400 public function setCacheMaxAge( $maxage ) {
401 $this->setCacheControl( [
402 'max-age' => $maxage,
403 's-maxage' => $maxage
404 ] );
405 }
406
432 public function setCacheMode( $mode ) {
433 if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
434 wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" );
435
436 // Ignore for forwards-compatibility
437 return;
438 }
439
440 if ( !User::isEveryoneAllowed( 'read' ) ) {
441 // Private wiki, only private headers
442 if ( $mode !== 'private' ) {
443 wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
444
445 return;
446 }
447 }
448
449 if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
450 // User language is used for i18n, so we don't want to publicly
451 // cache. Anons are ok, because if they have non-default language
452 // then there's an appropriate Vary header set by whatever set
453 // their non-default language.
454 wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
455 "'anon-public-user-private' due to uselang=user\n" );
456 $mode = 'anon-public-user-private';
457 }
458
459 wfDebug( __METHOD__ . ": setting cache mode $mode\n" );
460 $this->mCacheMode = $mode;
461 }
462
473 public function setCacheControl( $directives ) {
474 $this->mCacheControl = $directives + $this->mCacheControl;
475 }
476
484 public function createPrinterByName( $format ) {
485 $printer = $this->mModuleMgr->getModule( $format, 'format', /* $ignoreCache */ true );
486 if ( $printer === null ) {
487 $this->dieWithError(
488 [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
489 );
490 }
491
492 return $printer;
493 }
494
498 public function execute() {
499 if ( $this->mInternalMode ) {
500 $this->executeAction();
501 } else {
503 }
504 }
505
510 protected function executeActionWithErrorHandling() {
511 // Verify the CORS header before executing the action
512 if ( !$this->handleCORS() ) {
513 // handleCORS() has sent a 403, abort
514 return;
515 }
516
517 // Exit here if the request method was OPTIONS
518 // (assume there will be a followup GET or POST)
519 if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
520 return;
521 }
522
523 // In case an error occurs during data output,
524 // clear the output buffer and print just the error information
525 $obLevel = ob_get_level();
526 ob_start();
527
528 $t = microtime( true );
529 $isError = false;
530 try {
531 $this->executeAction();
532 $runTime = microtime( true ) - $t;
533 $this->logRequest( $runTime );
534 MediaWikiServices::getInstance()->getStatsdDataFactory()->timing(
535 'api.' . $this->mModule->getModuleName() . '.executeTiming', 1000 * $runTime
536 );
537 } catch ( Exception $e ) { // @todo Remove this block when HHVM is no longer supported
538 $this->handleException( $e );
539 $this->logRequest( microtime( true ) - $t, $e );
540 $isError = true;
541 } catch ( Throwable $e ) {
542 $this->handleException( $e );
543 $this->logRequest( microtime( true ) - $t, $e );
544 $isError = true;
545 }
546
547 // Commit DBs and send any related cookies and headers
548 MediaWiki::preOutputCommit( $this->getContext() );
549
550 // Send cache headers after any code which might generate an error, to
551 // avoid sending public cache headers for errors.
552 $this->sendCacheHeaders( $isError );
553
554 // Executing the action might have already messed with the output
555 // buffers.
556 while ( ob_get_level() > $obLevel ) {
557 ob_end_flush();
558 }
559 }
560
567 protected function handleException( $e ) {
568 // T65145: Rollback any open database transactions
569 if ( !$e instanceof ApiUsageException ) {
570 // ApiUsageExceptions are intentional, so don't rollback if that's the case
571 MWExceptionHandler::rollbackMasterChangesAndLog( $e );
572 }
573
574 // Allow extra cleanup and logging
575 Hooks::run( 'ApiMain::onException', [ $this, $e ] );
576
577 // Handle any kind of exception by outputting properly formatted error message.
578 // If this fails, an unhandled exception should be thrown so that global error
579 // handler will process and log it.
580
581 $errCodes = $this->substituteResultWithError( $e );
582
583 // Error results should not be cached
584 $this->setCacheMode( 'private' );
585
586 $response = $this->getRequest()->response();
587 $headerStr = 'MediaWiki-API-Error: ' . implode( ', ', $errCodes );
588 $response->header( $headerStr );
589
590 // Reset and print just the error message
591 ob_clean();
592
593 // Printer may not be initialized if the extractRequestParams() fails for the main module
594 $this->createErrorPrinter();
595
596 $failed = false;
597 try {
598 $this->printResult( $e->getCode() );
599 } catch ( ApiUsageException $ex ) {
600 // The error printer itself is failing. Try suppressing its request
601 // parameters and redo.
602 $failed = true;
603 $this->addWarning( 'apiwarn-errorprinterfailed' );
604 foreach ( $ex->getStatusValue()->getErrors() as $error ) {
605 try {
606 $this->mPrinter->addWarning( $error );
607 } catch ( Exception $ex2 ) { // @todo Remove this block when HHVM is no longer supported
608 // WTF?
609 $this->addWarning( $error );
610 } catch ( Throwable $ex2 ) {
611 // WTF?
612 $this->addWarning( $error );
613 }
614 }
615 }
616 if ( $failed ) {
617 $this->mPrinter = null;
618 $this->createErrorPrinter();
619 $this->mPrinter->forceDefaultParams();
620 if ( $e->getCode() ) {
621 $response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200
622 }
623 $this->printResult( $e->getCode() );
624 }
625 }
626
637 public static function handleApiBeforeMainException( $e ) {
638 ob_start();
639
640 try {
641 $main = new self( RequestContext::getMain(), false );
642 $main->handleException( $e );
643 $main->logRequest( 0, $e );
644 } catch ( Exception $e2 ) { // @todo Remove this block when HHVM is no longer supported
645 // Nope, even that didn't work. Punt.
646 throw $e;
647 } catch ( Throwable $e2 ) {
648 // Nope, even that didn't work. Punt.
649 throw $e;
650 }
651
652 // Reset cache headers
653 $main->sendCacheHeaders( true );
654
655 ob_end_flush();
656 }
657
672 protected function handleCORS() {
673 $originParam = $this->getParameter( 'origin' ); // defaults to null
674 if ( $originParam === null ) {
675 // No origin parameter, nothing to do
676 return true;
677 }
678
679 $request = $this->getRequest();
680 $response = $request->response();
681
682 $matchedOrigin = false;
683 $allowTiming = false;
684 $varyOrigin = true;
685
686 if ( $originParam === '*' ) {
687 // Request for anonymous CORS
688 // Technically we should check for the presence of an Origin header
689 // and not process it as CORS if it's not set, but that would
690 // require us to vary on Origin for all 'origin=*' requests which
691 // we don't want to do.
692 $matchedOrigin = true;
693 $allowOrigin = '*';
694 $allowCredentials = 'false';
695 $varyOrigin = false; // No need to vary
696 } else {
697 // Non-anonymous CORS, check we allow the domain
698
699 // Origin: header is a space-separated list of origins, check all of them
700 $originHeader = $request->getHeader( 'Origin' );
701 if ( $originHeader === false ) {
702 $origins = [];
703 } else {
704 $originHeader = trim( $originHeader );
705 $origins = preg_split( '/\s+/', $originHeader );
706 }
707
708 if ( !in_array( $originParam, $origins ) ) {
709 // origin parameter set but incorrect
710 // Send a 403 response
711 $response->statusHeader( 403 );
712 $response->header( 'Cache-Control: no-cache' );
713 echo "'origin' parameter does not match Origin header\n";
714
715 return false;
716 }
717
718 $config = $this->getConfig();
719 $matchedOrigin = count( $origins ) === 1 && self::matchOrigin(
720 $originParam,
721 $config->get( 'CrossSiteAJAXdomains' ),
722 $config->get( 'CrossSiteAJAXdomainExceptions' )
723 );
724
725 $allowOrigin = $originHeader;
726 $allowCredentials = 'true';
727 $allowTiming = $originHeader;
728 }
729
730 if ( $matchedOrigin ) {
731 $requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
732 $preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
733 if ( $preflight ) {
734 // This is a CORS preflight request
735 if ( $requestedMethod !== 'POST' && $requestedMethod !== 'GET' ) {
736 // If method is not a case-sensitive match, do not set any additional headers and terminate.
737 $response->header( 'MediaWiki-CORS-Rejection: Unsupported method requested in preflight' );
738 return true;
739 }
740 // We allow the actual request to send the following headers
741 $requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
742 if ( $requestedHeaders !== false ) {
743 if ( !self::matchRequestedHeaders( $requestedHeaders ) ) {
744 $response->header( 'MediaWiki-CORS-Rejection: Unsupported header requested in preflight' );
745 return true;
746 }
747 $response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
748 }
749
750 // We only allow the actual request to be GET or POST
751 $response->header( 'Access-Control-Allow-Methods: POST, GET' );
752 } elseif ( $request->getMethod() !== 'POST' && $request->getMethod() !== 'GET' ) {
753 // Unsupported non-preflight method, don't handle it as CORS
754 $response->header(
755 'MediaWiki-CORS-Rejection: Unsupported method for simple request or actual request'
756 );
757 return true;
758 }
759
760 $response->header( "Access-Control-Allow-Origin: $allowOrigin" );
761 $response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
762 // https://www.w3.org/TR/resource-timing/#timing-allow-origin
763 if ( $allowTiming !== false ) {
764 $response->header( "Timing-Allow-Origin: $allowTiming" );
765 }
766
767 if ( !$preflight ) {
768 $response->header(
769 'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag, '
770 . 'MediaWiki-Login-Suppressed'
771 );
772 }
773 } else {
774 $response->header( 'MediaWiki-CORS-Rejection: Origin mismatch' );
775 }
776
777 if ( $varyOrigin ) {
778 $this->getOutput()->addVaryHeader( 'Origin' );
779 }
780
781 return true;
782 }
783
792 protected static function matchOrigin( $value, $rules, $exceptions ) {
793 foreach ( $rules as $rule ) {
794 if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
795 // Rule matches, check exceptions
796 foreach ( $exceptions as $exc ) {
797 if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
798 return false;
799 }
800 }
801
802 return true;
803 }
804 }
805
806 return false;
807 }
808
816 protected static function matchRequestedHeaders( $requestedHeaders ) {
817 if ( trim( $requestedHeaders ) === '' ) {
818 return true;
819 }
820 $requestedHeaders = explode( ',', $requestedHeaders );
821 $allowedAuthorHeaders = array_flip( [
822 /* simple headers (see spec) */
823 'accept',
824 'accept-language',
825 'content-language',
826 'content-type',
827 /* non-authorable headers in XHR, which are however requested by some UAs */
828 'accept-encoding',
829 'dnt',
830 'origin',
831 /* MediaWiki whitelist */
832 'user-agent',
833 'api-user-agent',
834 ] );
835 foreach ( $requestedHeaders as $rHeader ) {
836 $rHeader = strtolower( trim( $rHeader ) );
837 if ( !isset( $allowedAuthorHeaders[$rHeader] ) ) {
838 LoggerFactory::getInstance( 'api-warning' )->warning(
839 'CORS preflight failed on requested header: {header}', [
840 'header' => $rHeader
841 ]
842 );
843 return false;
844 }
845 }
846 return true;
847 }
848
857 protected static function wildcardToRegex( $wildcard ) {
858 $wildcard = preg_quote( $wildcard, '/' );
859 $wildcard = str_replace(
860 [ '\*', '\?' ],
861 [ '.*?', '.' ],
862 $wildcard
863 );
864
865 return "/^https?:\/\/$wildcard$/";
866 }
867
873 protected function sendCacheHeaders( $isError ) {
874 $response = $this->getRequest()->response();
875 $out = $this->getOutput();
876
877 $out->addVaryHeader( 'Treat-as-Untrusted' );
878
879 $config = $this->getConfig();
880
881 if ( $config->get( 'VaryOnXFP' ) ) {
882 $out->addVaryHeader( 'X-Forwarded-Proto' );
883 }
884
885 if ( !$isError && $this->mModule &&
886 ( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
887 ) {
888 $etag = $this->mModule->getConditionalRequestData( 'etag' );
889 if ( $etag !== null ) {
890 $response->header( "ETag: $etag" );
891 }
892 $lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
893 if ( $lastMod !== null ) {
894 $response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
895 }
896 }
897
898 // The logic should be:
899 // $this->mCacheControl['max-age'] is set?
900 // Use it, the module knows better than our guess.
901 // !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
902 // Use 0 because we can guess caching is probably the wrong thing to do.
903 // Use $this->getParameter( 'maxage' ), which already defaults to 0.
904 $maxage = 0;
905 if ( isset( $this->mCacheControl['max-age'] ) ) {
906 $maxage = $this->mCacheControl['max-age'];
907 } elseif ( ( $this->mModule && !$this->mModule->isWriteMode() ) ||
908 $this->mCacheMode !== 'private'
909 ) {
910 $maxage = $this->getParameter( 'maxage' );
911 }
912 $privateCache = 'private, must-revalidate, max-age=' . $maxage;
913
914 if ( $this->mCacheMode == 'private' ) {
915 $response->header( "Cache-Control: $privateCache" );
916 return;
917 }
918
919 $useKeyHeader = $config->get( 'UseKeyHeader' );
920 if ( $this->mCacheMode == 'anon-public-user-private' ) {
921 $out->addVaryHeader( 'Cookie' );
922 $response->header( $out->getVaryHeader() );
923 if ( $useKeyHeader ) {
924 $response->header( $out->getKeyHeader() );
925 if ( $out->haveCacheVaryCookies() ) {
926 // Logged in, mark this request private
927 $response->header( "Cache-Control: $privateCache" );
928 return;
929 }
930 // Logged out, send normal public headers below
931 } elseif ( MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
932 // Logged in or otherwise has session (e.g. anonymous users who have edited)
933 // Mark request private
934 $response->header( "Cache-Control: $privateCache" );
935
936 return;
937 } // else no Key and anonymous, send public headers below
938 }
939
940 // Send public headers
941 $response->header( $out->getVaryHeader() );
942 if ( $useKeyHeader ) {
943 $response->header( $out->getKeyHeader() );
944 }
945
946 // If nobody called setCacheMaxAge(), use the (s)maxage parameters
947 if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
948 $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
949 }
950 if ( !isset( $this->mCacheControl['max-age'] ) ) {
951 $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
952 }
953
954 if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
955 // Public cache not requested
956 // Sending a Vary header in this case is harmless, and protects us
957 // against conditional calls of setCacheMaxAge().
958 $response->header( "Cache-Control: $privateCache" );
959
960 return;
961 }
962
963 $this->mCacheControl['public'] = true;
964
965 // Send an Expires header
966 $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
967 $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
968 $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
969
970 // Construct the Cache-Control header
971 $ccHeader = '';
972 $separator = '';
973 foreach ( $this->mCacheControl as $name => $value ) {
974 if ( is_bool( $value ) ) {
975 if ( $value ) {
976 $ccHeader .= $separator . $name;
977 $separator = ', ';
978 }
979 } else {
980 $ccHeader .= $separator . "$name=$value";
981 $separator = ', ';
982 }
983 }
984
985 $response->header( "Cache-Control: $ccHeader" );
986 }
987
991 private function createErrorPrinter() {
992 if ( !isset( $this->mPrinter ) ) {
993 $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
994 if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
996 }
997 $this->mPrinter = $this->createPrinterByName( $value );
998 }
999
1000 // Printer may not be able to handle errors. This is particularly
1001 // likely if the module returns something for getCustomPrinter().
1002 if ( !$this->mPrinter->canPrintErrors() ) {
1003 $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
1004 }
1005 }
1006
1022 protected function errorMessagesFromException( $e, $type = 'error' ) {
1023 $messages = [];
1024 if ( $e instanceof ApiUsageException ) {
1025 foreach ( $e->getStatusValue()->getErrorsByType( $type ) as $error ) {
1026 $messages[] = ApiMessage::create( $error );
1027 }
1028 } elseif ( $type !== 'error' ) {
1029 // None of the rest have any messages for non-error types
1030 } else {
1031 // Something is seriously wrong
1032 $config = $this->getConfig();
1033 // TODO: Avoid embedding arbitrary class names in the error code.
1034 $class = preg_replace( '#^Wikimedia\\\Rdbms\\\#', '', get_class( $e ) );
1035 $code = 'internal_api_error_' . $class;
1036 $data = [ 'errorclass' => get_class( $e ) ];
1037 if ( $config->get( 'ShowExceptionDetails' ) ) {
1038 if ( $e instanceof ILocalizedException ) {
1039 $msg = $e->getMessageObject();
1040 } elseif ( $e instanceof MessageSpecifier ) {
1041 $msg = Message::newFromSpecifier( $e );
1042 } else {
1043 $msg = wfEscapeWikiText( $e->getMessage() );
1044 }
1045 $params = [ 'apierror-exceptioncaught', WebRequest::getRequestId(), $msg ];
1046 } else {
1047 $params = [ 'apierror-exceptioncaughttype', WebRequest::getRequestId(), get_class( $e ) ];
1048 }
1049
1051 }
1052 return $messages;
1053 }
1054
1060 protected function substituteResultWithError( $e ) {
1061 $result = $this->getResult();
1062 $formatter = $this->getErrorFormatter();
1063 $config = $this->getConfig();
1064 $errorCodes = [];
1065
1066 // Remember existing warnings and errors across the reset
1067 $errors = $result->getResultData( [ 'errors' ] );
1068 $warnings = $result->getResultData( [ 'warnings' ] );
1069 $result->reset();
1070 if ( $warnings !== null ) {
1071 $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
1072 }
1073 if ( $errors !== null ) {
1074 $result->addValue( null, 'errors', $errors, ApiResult::NO_SIZE_CHECK );
1075
1076 // Collect the copied error codes for the return value
1077 foreach ( $errors as $error ) {
1078 if ( isset( $error['code'] ) ) {
1079 $errorCodes[$error['code']] = true;
1080 }
1081 }
1082 }
1083
1084 // Add errors from the exception
1085 $modulePath = $e instanceof ApiUsageException ? $e->getModulePath() : null;
1086 foreach ( $this->errorMessagesFromException( $e, 'error' ) as $msg ) {
1087 if ( ApiErrorFormatter::isValidApiCode( $msg->getApiCode() ) ) {
1088 $errorCodes[$msg->getApiCode()] = true;
1089 } else {
1090 LoggerFactory::getInstance( 'api-warning' )->error( 'Invalid API error code "{code}"', [
1091 'code' => $msg->getApiCode(),
1092 'exception' => $e,
1093 ] );
1094 $errorCodes['<invalid-code>'] = true;
1095 }
1096 $formatter->addError( $modulePath, $msg );
1097 }
1098 foreach ( $this->errorMessagesFromException( $e, 'warning' ) as $msg ) {
1099 $formatter->addWarning( $modulePath, $msg );
1100 }
1101
1102 // Add additional data. Path depends on whether we're in BC mode or not.
1103 // Data depends on the type of exception.
1104 if ( $formatter instanceof ApiErrorFormatter_BackCompat ) {
1105 $path = [ 'error' ];
1106 } else {
1107 $path = null;
1108 }
1109 if ( $e instanceof ApiUsageException ) {
1110 $link = wfExpandUrl( wfScript( 'api' ) );
1111 $result->addContentValue(
1112 $path,
1113 'docref',
1114 trim(
1115 $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text()
1116 . ' '
1117 . $this->msg( 'api-usage-mailinglist-ref' )->inLanguage( $formatter->getLanguage() )->text()
1118 )
1119 );
1120 } elseif ( $config->get( 'ShowExceptionDetails' ) ) {
1121 $result->addContentValue(
1122 $path,
1123 'trace',
1124 $this->msg( 'api-exception-trace',
1125 get_class( $e ),
1126 $e->getFile(),
1127 $e->getLine(),
1128 MWExceptionHandler::getRedactedTraceAsString( $e )
1129 )->inLanguage( $formatter->getLanguage() )->text()
1130 );
1131 }
1132
1133 // Add the id and such
1134 $this->addRequestedFields( [ 'servedby' ] );
1135
1136 return array_keys( $errorCodes );
1137 }
1138
1144 protected function addRequestedFields( $force = [] ) {
1145 $result = $this->getResult();
1146
1147 $requestid = $this->getParameter( 'requestid' );
1148 if ( $requestid !== null ) {
1149 $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
1150 }
1151
1152 if ( $this->getConfig()->get( 'ShowHostnames' ) && (
1153 in_array( 'servedby', $force, true ) || $this->getParameter( 'servedby' )
1154 ) ) {
1155 $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
1156 }
1157
1158 if ( $this->getParameter( 'curtimestamp' ) ) {
1159 $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601, time() ),
1161 }
1162
1163 if ( $this->getParameter( 'responselanginfo' ) ) {
1164 $result->addValue( null, 'uselang', $this->getLanguage()->getCode(),
1166 $result->addValue( null, 'errorlang', $this->getErrorFormatter()->getLanguage()->getCode(),
1168 }
1169 }
1170
1175 protected function setupExecuteAction() {
1176 $this->addRequestedFields();
1177
1178 $params = $this->extractRequestParams();
1179 $this->mAction = $params['action'];
1180
1181 return $params;
1182 }
1183
1190 protected function setupModule() {
1191 // Instantiate the module requested by the user
1192 $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
1193 if ( $module === null ) {
1194 // Probably can't happen
1195 // @codeCoverageIgnoreStart
1196 $this->dieWithError(
1197 [ 'apierror-unknownaction', wfEscapeWikiText( $this->mAction ) ], 'unknown_action'
1198 );
1199 // @codeCoverageIgnoreEnd
1200 }
1201 $moduleParams = $module->extractRequestParams();
1202
1203 // Check token, if necessary
1204 if ( $module->needsToken() === true ) {
1205 throw new MWException(
1206 "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
1207 'See documentation for ApiBase::needsToken for details.'
1208 );
1209 }
1210 if ( $module->needsToken() ) {
1211 if ( !$module->mustBePosted() ) {
1212 throw new MWException(
1213 "Module '{$module->getModuleName()}' must require POST to use tokens."
1214 );
1215 }
1216
1217 if ( !isset( $moduleParams['token'] ) ) {
1218 // Probably can't happen
1219 // @codeCoverageIgnoreStart
1220 $module->dieWithError( [ 'apierror-missingparam', 'token' ] );
1221 // @codeCoverageIgnoreEnd
1222 }
1223
1224 $module->requirePostedParameters( [ 'token' ] );
1225
1226 if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
1227 $module->dieWithError( 'apierror-badtoken' );
1228 }
1229 }
1230
1231 return $module;
1232 }
1233
1237 private function getMaxLag() {
1238 $dbLag = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaxLag();
1239 $lagInfo = [
1240 'host' => $dbLag[0],
1241 'lag' => $dbLag[1],
1242 'type' => 'db'
1243 ];
1244
1245 $jobQueueLagFactor = $this->getConfig()->get( 'JobQueueIncludeInMaxLagFactor' );
1246 if ( $jobQueueLagFactor ) {
1247 // Turn total number of jobs into seconds by using the configured value
1248 $totalJobs = array_sum( JobQueueGroup::singleton()->getQueueSizes() );
1249 $jobQueueLag = $totalJobs / (float)$jobQueueLagFactor;
1250 if ( $jobQueueLag > $lagInfo['lag'] ) {
1251 $lagInfo = [
1252 'host' => wfHostname(), // XXX: Is there a better value that could be used?
1253 'lag' => $jobQueueLag,
1254 'type' => 'jobqueue',
1255 'jobs' => $totalJobs,
1256 ];
1257 }
1258 }
1259
1260 Hooks::runWithoutAbort( 'ApiMaxLagInfo', [ &$lagInfo ] );
1261
1262 return $lagInfo;
1263 }
1264
1271 protected function checkMaxLag( $module, $params ) {
1272 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
1273 $maxLag = $params['maxlag'];
1274 $lagInfo = $this->getMaxLag();
1275 if ( $lagInfo['lag'] > $maxLag ) {
1276 $response = $this->getRequest()->response();
1277
1278 $response->header( 'Retry-After: ' . max( (int)$maxLag, 5 ) );
1279 $response->header( 'X-Database-Lag: ' . (int)$lagInfo['lag'] );
1280
1281 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1282 $this->dieWithError(
1283 [ 'apierror-maxlag', $lagInfo['lag'], $lagInfo['host'] ],
1284 'maxlag',
1285 $lagInfo
1286 );
1287 }
1288
1289 $this->dieWithError( [ 'apierror-maxlag-generic', $lagInfo['lag'] ], 'maxlag', $lagInfo );
1290 }
1291 }
1292
1293 return true;
1294 }
1295
1317 protected function checkConditionalRequestHeaders( $module ) {
1318 if ( $this->mInternalMode ) {
1319 // No headers to check in internal mode
1320 return true;
1321 }
1322
1323 if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) {
1324 // Don't check POSTs
1325 return true;
1326 }
1327
1328 $return304 = false;
1329
1330 $ifNoneMatch = array_diff(
1331 $this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: [],
1332 [ '' ]
1333 );
1334 if ( $ifNoneMatch ) {
1335 if ( $ifNoneMatch === [ '*' ] ) {
1336 // API responses always "exist"
1337 $etag = '*';
1338 } else {
1339 $etag = $module->getConditionalRequestData( 'etag' );
1340 }
1341 }
1342 if ( $ifNoneMatch && $etag !== null ) {
1343 $test = substr( $etag, 0, 2 ) === 'W/' ? substr( $etag, 2 ) : $etag;
1344 $match = array_map( function ( $s ) {
1345 return substr( $s, 0, 2 ) === 'W/' ? substr( $s, 2 ) : $s;
1346 }, $ifNoneMatch );
1347 $return304 = in_array( $test, $match, true );
1348 } else {
1349 $value = trim( $this->getRequest()->getHeader( 'If-Modified-Since' ) );
1350
1351 // Some old browsers sends sizes after the date, like this:
1352 // Wed, 20 Aug 2003 06:51:19 GMT; length=5202
1353 // Ignore that.
1354 $i = strpos( $value, ';' );
1355 if ( $i !== false ) {
1356 $value = trim( substr( $value, 0, $i ) );
1357 }
1358
1359 if ( $value !== '' ) {
1360 try {
1361 $ts = new MWTimestamp( $value );
1362 if (
1363 // RFC 7231 IMF-fixdate
1364 $ts->getTimestamp( TS_RFC2822 ) === $value ||
1365 // RFC 850
1366 $ts->format( 'l, d-M-y H:i:s' ) . ' GMT' === $value ||
1367 // asctime (with and without space-padded day)
1368 $ts->format( 'D M j H:i:s Y' ) === $value ||
1369 $ts->format( 'D M j H:i:s Y' ) === $value
1370 ) {
1371 $lastMod = $module->getConditionalRequestData( 'last-modified' );
1372 if ( $lastMod !== null ) {
1373 // Mix in some MediaWiki modification times
1374 $modifiedTimes = [
1375 'page' => $lastMod,
1376 'user' => $this->getUser()->getTouched(),
1377 'epoch' => $this->getConfig()->get( 'CacheEpoch' ),
1378 ];
1379 if ( $this->getConfig()->get( 'UseSquid' ) ) {
1380 // T46570: the core page itself may not change, but resources might
1381 $modifiedTimes['sepoch'] = wfTimestamp(
1382 TS_MW, time() - $this->getConfig()->get( 'SquidMaxage' )
1383 );
1384 }
1385 Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this->getOutput() ] );
1386 $lastMod = max( $modifiedTimes );
1387 $return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
1388 }
1389 }
1390 } catch ( TimestampException $e ) {
1391 // Invalid timestamp, ignore it
1392 }
1393 }
1394 }
1395
1396 if ( $return304 ) {
1397 $this->getRequest()->response()->statusHeader( 304 );
1398
1399 // Avoid outputting the compressed representation of a zero-length body
1400 Wikimedia\suppressWarnings();
1401 ini_set( 'zlib.output_compression', 0 );
1402 Wikimedia\restoreWarnings();
1404
1405 return false;
1406 }
1407
1408 return true;
1409 }
1410
1415 protected function checkExecutePermissions( $module ) {
1416 $user = $this->getUser();
1417 if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) &&
1418 !$user->isAllowed( 'read' )
1419 ) {
1420 $this->dieWithError( 'apierror-readapidenied' );
1421 }
1422
1423 if ( $module->isWriteMode() ) {
1424 if ( !$this->mEnableWrite ) {
1425 $this->dieWithError( 'apierror-noapiwrite' );
1426 } elseif ( !$user->isAllowed( 'writeapi' ) ) {
1427 $this->dieWithError( 'apierror-writeapidenied' );
1428 } elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) {
1429 $this->dieWithError( 'apierror-promised-nonwrite-api' );
1430 }
1431
1432 $this->checkReadOnly( $module );
1433 }
1434
1435 // Allow extensions to stop execution for arbitrary reasons.
1436 $message = 'hookaborted';
1437 if ( !Hooks::run( 'ApiCheckCanExecute', [ $module, $user, &$message ] ) ) {
1438 $this->dieWithError( $message );
1439 }
1440 }
1441
1446 protected function checkReadOnly( $module ) {
1447 if ( wfReadOnly() ) {
1448 $this->dieReadOnly();
1449 }
1450
1451 if ( $module->isWriteMode()
1452 && $this->getUser()->isBot()
1453 && MediaWikiServices::getInstance()->getDBLoadBalancer()->getServerCount() > 1
1454 ) {
1455 $this->checkBotReadOnly();
1456 }
1457 }
1458
1462 private function checkBotReadOnly() {
1463 // Figure out how many servers have passed the lag threshold
1464 $numLagged = 0;
1465 $lagLimit = $this->getConfig()->get( 'APIMaxLagThreshold' );
1466 $laggedServers = [];
1467 $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
1468 foreach ( $loadBalancer->getLagTimes() as $serverIndex => $lag ) {
1469 if ( $lag > $lagLimit ) {
1470 ++$numLagged;
1471 $laggedServers[] = $loadBalancer->getServerName( $serverIndex ) . " ({$lag}s)";
1472 }
1473 }
1474
1475 // If a majority of replica DBs are too lagged then disallow writes
1476 $replicaCount = $loadBalancer->getServerCount() - 1;
1477 if ( $numLagged >= ceil( $replicaCount / 2 ) ) {
1478 $laggedServers = implode( ', ', $laggedServers );
1479 wfDebugLog(
1480 'api-readonly', // Deprecate this channel in favor of api-warning?
1481 "Api request failed as read only because the following DBs are lagged: $laggedServers"
1482 );
1483 LoggerFactory::getInstance( 'api-warning' )->warning(
1484 "Api request failed as read only because the following DBs are lagged: {laggeddbs}", [
1485 'laggeddbs' => $laggedServers,
1486 ]
1487 );
1488
1489 $this->dieWithError(
1490 'readonly_lag',
1491 'readonly',
1492 [ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ]
1493 );
1494 }
1495 }
1496
1501 protected function checkAsserts( $params ) {
1502 if ( isset( $params['assert'] ) ) {
1503 $user = $this->getUser();
1504 switch ( $params['assert'] ) {
1505 case 'user':
1506 if ( $user->isAnon() ) {
1507 $this->dieWithError( 'apierror-assertuserfailed' );
1508 }
1509 break;
1510 case 'bot':
1511 if ( !$user->isAllowed( 'bot' ) ) {
1512 $this->dieWithError( 'apierror-assertbotfailed' );
1513 }
1514 break;
1515 }
1516 }
1517 if ( isset( $params['assertuser'] ) ) {
1518 $assertUser = User::newFromName( $params['assertuser'], false );
1519 if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) {
1520 $this->dieWithError(
1521 [ 'apierror-assertnameduserfailed', wfEscapeWikiText( $params['assertuser'] ) ]
1522 );
1523 }
1524 }
1525 }
1526
1532 protected function setupExternalResponse( $module, $params ) {
1533 $validMethods = [ 'GET', 'HEAD', 'POST', 'OPTIONS' ];
1534 $request = $this->getRequest();
1535
1536 if ( !in_array( $request->getMethod(), $validMethods ) ) {
1537 $this->dieWithError( 'apierror-invalidmethod', null, null, 405 );
1538 }
1539
1540 if ( !$request->wasPosted() && $module->mustBePosted() ) {
1541 // Module requires POST. GET request might still be allowed
1542 // if $wgDebugApi is true, otherwise fail.
1543 $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] );
1544 }
1545
1546 // See if custom printer is used
1547 $this->mPrinter = $module->getCustomPrinter();
1548 if ( is_null( $this->mPrinter ) ) {
1549 // Create an appropriate printer
1550 $this->mPrinter = $this->createPrinterByName( $params['format'] );
1551 }
1552
1553 if ( $request->getProtocol() === 'http' && (
1554 $request->getSession()->shouldForceHTTPS() ||
1555 ( $this->getUser()->isLoggedIn() &&
1556 $this->getUser()->requiresHTTPS() )
1557 ) ) {
1558 $this->addDeprecation( 'apiwarn-deprecation-httpsexpected', 'https-expected' );
1559 }
1560 }
1561
1565 protected function executeAction() {
1566 $params = $this->setupExecuteAction();
1567
1568 // Check asserts early so e.g. errors in parsing a module's parameters due to being
1569 // logged out don't override the client's intended "am I logged in?" check.
1570 $this->checkAsserts( $params );
1571
1572 $module = $this->setupModule();
1573 $this->mModule = $module;
1574
1575 if ( !$this->mInternalMode ) {
1576 $this->setRequestExpectations( $module );
1577 }
1578
1579 $this->checkExecutePermissions( $module );
1580
1581 if ( !$this->checkMaxLag( $module, $params ) ) {
1582 return;
1583 }
1584
1585 if ( !$this->checkConditionalRequestHeaders( $module ) ) {
1586 return;
1587 }
1588
1589 if ( !$this->mInternalMode ) {
1590 $this->setupExternalResponse( $module, $params );
1591 }
1592
1593 $module->execute();
1594 Hooks::run( 'APIAfterExecute', [ &$module ] );
1595
1596 $this->reportUnusedParams();
1597
1598 if ( !$this->mInternalMode ) {
1599 MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
1600
1601 $this->printResult();
1602 }
1603 }
1604
1609 protected function setRequestExpectations( ApiBase $module ) {
1610 $limits = $this->getConfig()->get( 'TrxProfilerLimits' );
1611 $trxProfiler = Profiler::instance()->getTransactionProfiler();
1612 $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
1613 if ( $this->getRequest()->hasSafeMethod() ) {
1614 $trxProfiler->setExpectations( $limits['GET'], __METHOD__ );
1615 } elseif ( $this->getRequest()->wasPosted() && !$module->isWriteMode() ) {
1616 $trxProfiler->setExpectations( $limits['POST-nonwrite'], __METHOD__ );
1617 $this->getRequest()->markAsSafeRequest();
1618 } else {
1619 $trxProfiler->setExpectations( $limits['POST'], __METHOD__ );
1620 }
1621 }
1622
1628 protected function logRequest( $time, $e = null ) {
1629 $request = $this->getRequest();
1630 $legacyLogCtx = [
1631 'ts' => time(),
1632 'ip' => $request->getIP(),
1633 'userAgent' => $this->getUserAgent(),
1634 'wiki' => WikiMap::getCurrentWikiDbDomain()->getId(),
1635 'timeSpentBackend' => (int)round( $time * 1000 ),
1636 'hadError' => $e !== null,
1637 'errorCodes' => [],
1638 'params' => [],
1639 ];
1640
1641 $logCtx = [
1642 '$schema' => '/mediawiki/api/request/0.0.1',
1643 'meta' => [
1644 'request_id' => WebRequest::getRequestId(),
1645 'id' => UIDGenerator::newUUIDv1(),
1646 'dt' => wfTimestamp( TS_ISO_8601 ),
1647 'domain' => $this->getConfig()->get( 'ServerName' ),
1648 'stream' => 'mediawiki.api-request'
1649 ],
1650 'http' => [
1651 'method' => $request->getMethod(),
1652 'client_ip' => $request->getIP()
1653 ],
1654 'database' => WikiMap::getCurrentWikiDbDomain()->getId(),
1655 'backend_time_ms' => (int)round( $time * 1000 ),
1656 ];
1657
1658 // If set, these headers will be logged in http.request_headers.
1659 $httpRequestHeadersToLog = [ 'accept-language', 'referer', 'user-agent' ];
1660 foreach ( $httpRequestHeadersToLog as $header ) {
1661 if ( $request->getHeader( $header ) ) {
1662 // Set the header in http.request_headers
1663 $logCtx['http']['request_headers'][$header] = $request->getHeader( $header );
1664 }
1665 }
1666
1667 if ( $e ) {
1668 $logCtx['api_error_codes'] = [];
1669 foreach ( $this->errorMessagesFromException( $e ) as $msg ) {
1670 $legacyLogCtx['errorCodes'][] = $msg->getApiCode();
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 " {$legacyLogCtx['ip']} " .
1679 "T={$legacyLogCtx['timeSpentBackend']}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 $legacyLogCtx['params'][$name] = $value;
1699 $logCtx['params'][$name] = $value;
1700 $msg .= " {$name}={$encValue}";
1701 }
1702
1703 wfDebugLog( 'api', $msg, 'private' );
1704 // ApiAction channel is for structured data consumers.
1705 // The ApiAction was using logging channel is deprecated and is replaced
1706 // by the api-request channel.
1707 wfDebugLog( 'ApiAction', '', 'private', $legacyLogCtx );
1708 wfDebugLog( 'api-request', '', 'private', $logCtx );
1709 }
1710
1716 protected function encodeRequestLogValue( $s ) {
1717 static $table;
1718 if ( !$table ) {
1719 $chars = ';@$!*(),/:';
1720 $numChars = strlen( $chars );
1721 for ( $i = 0; $i < $numChars; $i++ ) {
1722 $table[rawurlencode( $chars[$i] )] = $chars[$i];
1723 }
1724 }
1725
1726 return strtr( rawurlencode( $s ), $table );
1727 }
1728
1733 protected function getParamsUsed() {
1734 return array_keys( $this->mParamsUsed );
1735 }
1736
1741 public function markParamsUsed( $params ) {
1742 $this->mParamsUsed += array_fill_keys( (array)$params, true );
1743 }
1744
1750 protected function getSensitiveParams() {
1751 return array_keys( $this->mParamsSensitive );
1752 }
1753
1759 public function markParamsSensitive( $params ) {
1760 $this->mParamsSensitive += array_fill_keys( (array)$params, true );
1761 }
1762
1769 public function getVal( $name, $default = null ) {
1770 $this->mParamsUsed[$name] = true;
1771
1772 $ret = $this->getRequest()->getVal( $name );
1773 if ( $ret === null ) {
1774 if ( $this->getRequest()->getArray( $name ) !== null ) {
1775 // See T12262 for why we don't just implode( '|', ... ) the
1776 // array.
1777 $this->addWarning( [ 'apiwarn-unsupportedarray', $name ] );
1778 }
1779 $ret = $default;
1780 }
1781 return $ret;
1782 }
1783
1790 public function getCheck( $name ) {
1791 return $this->getVal( $name, null ) !== null;
1792 }
1793
1801 public function getUpload( $name ) {
1802 $this->mParamsUsed[$name] = true;
1803
1804 return $this->getRequest()->getUpload( $name );
1805 }
1806
1811 protected function reportUnusedParams() {
1812 $paramsUsed = $this->getParamsUsed();
1813 $allParams = $this->getRequest()->getValueNames();
1814
1815 if ( !$this->mInternalMode ) {
1816 // Printer has not yet executed; don't warn that its parameters are unused
1817 $printerParams = $this->mPrinter->encodeParamName(
1818 array_keys( $this->mPrinter->getFinalParams() ?: [] )
1819 );
1820 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
1821 } else {
1822 $unusedParams = array_diff( $allParams, $paramsUsed );
1823 }
1824
1825 if ( count( $unusedParams ) ) {
1826 $this->addWarning( [
1827 'apierror-unrecognizedparams',
1828 Message::listParam( array_map( 'wfEscapeWikiText', $unusedParams ), 'comma' ),
1829 count( $unusedParams )
1830 ] );
1831 }
1832 }
1833
1839 protected function printResult( $httpCode = 0 ) {
1840 if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
1841 $this->addWarning( 'apiwarn-wgDebugAPI' );
1842 }
1843
1844 $printer = $this->mPrinter;
1845 $printer->initPrinter( false );
1846 if ( $httpCode ) {
1847 $printer->setHttpStatus( $httpCode );
1848 }
1849 $printer->execute();
1850 $printer->closePrinter();
1851 }
1852
1856 public function isReadMode() {
1857 return false;
1858 }
1859
1865 public function getAllowedParams() {
1866 return [
1867 'action' => [
1868 ApiBase::PARAM_DFLT => 'help',
1869 ApiBase::PARAM_TYPE => 'submodule',
1870 ],
1871 'format' => [
1873 ApiBase::PARAM_TYPE => 'submodule',
1874 ],
1875 'maxlag' => [
1876 ApiBase::PARAM_TYPE => 'integer'
1877 ],
1878 'smaxage' => [
1879 ApiBase::PARAM_TYPE => 'integer',
1881 ],
1882 'maxage' => [
1883 ApiBase::PARAM_TYPE => 'integer',
1885 ],
1886 'assert' => [
1887 ApiBase::PARAM_TYPE => [ 'user', 'bot' ]
1888 ],
1889 'assertuser' => [
1890 ApiBase::PARAM_TYPE => 'user',
1891 ],
1892 'requestid' => null,
1893 'servedby' => false,
1894 'curtimestamp' => false,
1895 'responselanginfo' => false,
1896 'origin' => null,
1897 'uselang' => [
1899 ],
1900 'errorformat' => [
1901 ApiBase::PARAM_TYPE => [ 'plaintext', 'wikitext', 'html', 'raw', 'none', 'bc' ],
1902 ApiBase::PARAM_DFLT => 'bc',
1903 ],
1904 'errorlang' => [
1905 ApiBase::PARAM_DFLT => 'uselang',
1906 ],
1907 'errorsuselocal' => [
1908 ApiBase::PARAM_DFLT => false,
1909 ],
1910 ];
1911 }
1912
1914 protected function getExamplesMessages() {
1915 return [
1916 'action=help'
1917 => 'apihelp-help-example-main',
1918 'action=help&recursivesubmodules=1'
1919 => 'apihelp-help-example-recursive',
1920 ];
1921 }
1922
1923 public function modifyHelp( array &$help, array $options, array &$tocData ) {
1924 // Wish PHP had an "array_insert_before". Instead, we have to manually
1925 // reindex the array to get 'permissions' in the right place.
1926 $oldHelp = $help;
1927 $help = [];
1928 foreach ( $oldHelp as $k => $v ) {
1929 if ( $k === 'submodules' ) {
1930 $help['permissions'] = '';
1931 }
1932 $help[$k] = $v;
1933 }
1934 $help['datatypes'] = '';
1935 $help['templatedparams'] = '';
1936 $help['credits'] = '';
1937
1938 // Fill 'permissions'
1939 $help['permissions'] .= Html::openElement( 'div',
1940 [ 'class' => 'apihelp-block apihelp-permissions' ] );
1941 $m = $this->msg( 'api-help-permissions' );
1942 if ( !$m->isDisabled() ) {
1943 $help['permissions'] .= Html::rawElement( 'div', [ 'class' => 'apihelp-block-head' ],
1944 $m->numParams( count( self::$mRights ) )->parse()
1945 );
1946 }
1947 $help['permissions'] .= Html::openElement( 'dl' );
1948 foreach ( self::$mRights as $right => $rightMsg ) {
1949 $help['permissions'] .= Html::element( 'dt', null, $right );
1950
1951 $rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
1952 $help['permissions'] .= Html::rawElement( 'dd', null, $rightMsg );
1953
1954 $groups = array_map( function ( $group ) {
1955 return $group == '*' ? 'all' : $group;
1956 }, User::getGroupsWithPermission( $right ) );
1957
1958 $help['permissions'] .= Html::rawElement( 'dd', null,
1959 $this->msg( 'api-help-permissions-granted-to' )
1960 ->numParams( count( $groups ) )
1961 ->params( Message::listParam( $groups ) )
1962 ->parse()
1963 );
1964 }
1965 $help['permissions'] .= Html::closeElement( 'dl' );
1966 $help['permissions'] .= Html::closeElement( 'div' );
1967
1968 // Fill 'datatypes', 'templatedparams', and 'credits', if applicable
1969 if ( empty( $options['nolead'] ) ) {
1970 $level = $options['headerlevel'];
1971 $tocnumber = &$options['tocnumber'];
1972
1973 $header = $this->msg( 'api-help-datatypes-header' )->parse();
1974
1975 $id = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_PRIMARY );
1976 $idFallback = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_FALLBACK );
1977 $headline = Linker::makeHeadline( min( 6, $level ),
1978 ' class="apihelp-header">',
1979 $id,
1980 $header,
1981 '',
1982 $idFallback
1983 );
1984 // Ensure we have a sane anchor
1985 if ( $id !== 'main/datatypes' && $idFallback !== 'main/datatypes' ) {
1986 $headline = '<div id="main/datatypes"></div>' . $headline;
1987 }
1988 $help['datatypes'] .= $headline;
1989 $help['datatypes'] .= $this->msg( 'api-help-datatypes' )->parseAsBlock();
1990 if ( !isset( $tocData['main/datatypes'] ) ) {
1991 $tocnumber[$level]++;
1992 $tocData['main/datatypes'] = [
1993 'toclevel' => count( $tocnumber ),
1994 'level' => $level,
1995 'anchor' => 'main/datatypes',
1996 'line' => $header,
1997 'number' => implode( '.', $tocnumber ),
1998 'index' => false,
1999 ];
2000 }
2001
2002 $header = $this->msg( 'api-help-templatedparams-header' )->parse();
2003
2004 $id = Sanitizer::escapeIdForAttribute( 'main/templatedparams', Sanitizer::ID_PRIMARY );
2005 $idFallback = Sanitizer::escapeIdForAttribute( 'main/templatedparams', Sanitizer::ID_FALLBACK );
2006 $headline = Linker::makeHeadline( min( 6, $level ),
2007 ' class="apihelp-header">',
2008 $id,
2009 $header,
2010 '',
2011 $idFallback
2012 );
2013 // Ensure we have a sane anchor
2014 if ( $id !== 'main/templatedparams' && $idFallback !== 'main/templatedparams' ) {
2015 $headline = '<div id="main/templatedparams"></div>' . $headline;
2016 }
2017 $help['templatedparams'] .= $headline;
2018 $help['templatedparams'] .= $this->msg( 'api-help-templatedparams' )->parseAsBlock();
2019 if ( !isset( $tocData['main/templatedparams'] ) ) {
2020 $tocnumber[$level]++;
2021 $tocData['main/templatedparams'] = [
2022 'toclevel' => count( $tocnumber ),
2023 'level' => $level,
2024 'anchor' => 'main/templatedparams',
2025 'line' => $header,
2026 'number' => implode( '.', $tocnumber ),
2027 'index' => false,
2028 ];
2029 }
2030
2031 $header = $this->msg( 'api-credits-header' )->parse();
2032 $id = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_PRIMARY );
2033 $idFallback = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_FALLBACK );
2034 $headline = Linker::makeHeadline( min( 6, $level ),
2035 ' class="apihelp-header">',
2036 $id,
2037 $header,
2038 '',
2039 $idFallback
2040 );
2041 // Ensure we have a sane anchor
2042 if ( $id !== 'main/credits' && $idFallback !== 'main/credits' ) {
2043 $headline = '<div id="main/credits"></div>' . $headline;
2044 }
2045 $help['credits'] .= $headline;
2046 $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
2047 if ( !isset( $tocData['main/credits'] ) ) {
2048 $tocnumber[$level]++;
2049 $tocData['main/credits'] = [
2050 'toclevel' => count( $tocnumber ),
2051 'level' => $level,
2052 'anchor' => 'main/credits',
2053 'line' => $header,
2054 'number' => implode( '.', $tocnumber ),
2055 'index' => false,
2056 ];
2057 }
2058 }
2059 }
2060
2061 private $mCanApiHighLimits = null;
2062
2067 public function canApiHighLimits() {
2068 if ( !isset( $this->mCanApiHighLimits ) ) {
2069 $this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' );
2070 }
2071
2073 }
2074
2079 public function getModuleManager() {
2080 return $this->mModuleMgr;
2081 }
2082
2091 public function getUserAgent() {
2092 return trim(
2093 $this->getRequest()->getHeader( 'Api-user-agent' ) . ' ' .
2094 $this->getRequest()->getHeader( 'User-agent' )
2095 );
2096 }
2097}
2098
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
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()
Fetch server name for use in error reporting etc.
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,...
$messages
$wgLang
Definition Setup.php:875
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:37
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:858
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition ApiBase.php:1990
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:2159
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below.
Definition ApiBase.php:87
isWriteMode()
Indicates whether this module requires write mode.
Definition ApiBase.php:419
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
Definition ApiBase.php:48
dieReadOnly()
Helper function for readonly errors.
Definition ApiBase.php:2089
addDeprecation( $msg, $feature, $data=[])
Add a deprecation warning for this module.
Definition ApiBase.php:1923
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition ApiBase.php:258
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:743
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition ApiBase.php:1909
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:254
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.
initPrinter( $unused=false)
Initialize the printer function and prepare the output headers.
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:637
getExamplesMessages()
@inheritDoc
Definition ApiMain.php:1914
isReadMode()
Definition ApiMain.php:1856
setRequestExpectations(ApiBase $module)
Set database connection, query, and write expectations given this module request.
Definition ApiMain.php:1609
getAllowedParams()
See ApiBase for description.
Definition ApiMain.php:1865
getSensitiveParams()
Get the request parameters that should be considered sensitive.
Definition ApiMain.php:1750
getPrinter()
Get the result formatter object.
Definition ApiMain.php:391
logRequest( $time, $e=null)
Log the preceding request.
Definition ApiMain.php:1628
static $Modules
List of available modules: action name => module class.
Definition ApiMain.php:55
sendCacheHeaders( $isError)
Send caching headers.
Definition ApiMain.php:873
encodeRequestLogValue( $s)
Encode a value in a format suitable for a space-separated log line.
Definition ApiMain.php:1716
ApiFormatBase $mPrinter
Definition ApiMain.php:144
getMaxLag()
Definition ApiMain.php:1237
markParamsUsed( $params)
Mark parameters as used.
Definition ApiMain.php:1741
executeActionWithErrorHandling()
Execute an action, and in case of an error, erase whatever partial results have been accumulated,...
Definition ApiMain.php:510
$mParamsSensitive
Definition ApiMain.php:158
createPrinterByName( $format)
Create an instance of an output formatter by its name.
Definition ApiMain.php:484
setCacheMaxAge( $maxage)
Set how long the response should be cached.
Definition ApiMain.php:400
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:308
createErrorPrinter()
Create the printer for error output.
Definition ApiMain.php:991
executeAction()
Execute the actual module, without any error handling.
Definition ApiMain.php:1565
getErrorFormatter()
Get the ApiErrorFormatter object associated with current request.
Definition ApiMain.php:351
checkMaxLag( $module, $params)
Check the max lag if necessary.
Definition ApiMain.php:1271
checkAsserts( $params)
Check asserts of the user's rights.
Definition ApiMain.php:1501
$mErrorFormatter
Definition ApiMain.php:146
setupExternalResponse( $module, $params)
Check POST for external response and setup result printer.
Definition ApiMain.php:1532
static matchRequestedHeaders( $requestedHeaders)
Attempt to validate the value of Access-Control-Request-Headers against a list of headers that we all...
Definition ApiMain.php:816
bool null $lacksSameOriginSecurity
Cached return value from self::lacksSameOriginSecurity()
Definition ApiMain.php:161
setCacheControl( $directives)
Set directives (key/value pairs) for the Cache-Control header.
Definition ApiMain.php:473
static wildcardToRegex( $wildcard)
Helper function to convert wildcard string into a regex '*' => '.
Definition ApiMain.php:857
setContinuationManager(ApiContinuationManager $manager=null)
Set the continuation manager.
Definition ApiMain.php:367
setCacheMode( $mode)
Set the type of caching headers which will be sent.
Definition ApiMain.php:432
setupModule()
Set up the module for response.
Definition ApiMain.php:1190
markParamsSensitive( $params)
Mark parameters as sensitive.
Definition ApiMain.php:1759
ApiBase $mModule
Definition ApiMain.php:153
setupExecuteAction()
Set up for the execution.
Definition ApiMain.php:1175
checkConditionalRequestHeaders( $module)
Check selected RFC 7232 precondition headers.
Definition ApiMain.php:1317
checkBotReadOnly()
Check whether we are readonly for bots.
Definition ApiMain.php:1462
handleException( $e)
Handle an exception as an API response.
Definition ApiMain.php:567
$mSquidMaxage
Definition ApiMain.php:151
getUserAgent()
Fetches the user agent used for this request.
Definition ApiMain.php:2091
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:792
getModule()
Get the API module object.
Definition ApiMain.php:382
__construct( $context=null, $enableWrite=false)
Constructs an instance of ApiMain that utilizes the module and format specified by $request.
Definition ApiMain.php:170
isInternalMode()
Return true if the API was started by other PHP code using FauxRequest.
Definition ApiMain.php:299
addRequestedFields( $force=[])
Add requested fields to the result.
Definition ApiMain.php:1144
modifyHelp(array &$help, array $options, array &$tocData)
Called from ApiHelp before the pieces are joined together and returned.
Definition ApiMain.php:1923
$mCacheControl
Definition ApiMain.php:156
checkReadOnly( $module)
Check if the DB is read-only for this user.
Definition ApiMain.php:1446
getCheck( $name)
Get a boolean request value, and register the fact that the parameter was used, for logging.
Definition ApiMain.php:1790
$mInternalMode
Definition ApiMain.php:151
getContinuationManager()
Get the continuation manager.
Definition ApiMain.php:359
printResult( $httpCode=0)
Print results using the current printer.
Definition ApiMain.php:1839
reportUnusedParams()
Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,...
Definition ApiMain.php:1811
lacksSameOriginSecurity()
Get the security flag for the current request.
Definition ApiMain.php:316
getVal( $name, $default=null)
Get a request value, and register the fact that it was used, for logging.
Definition ApiMain.php:1769
getParamsUsed()
Get the request parameters used in the course of the preceding execute() request.
Definition ApiMain.php:1733
getModuleManager()
Overrides to return this instance's module manager.
Definition ApiMain.php:2079
ApiContinuationManager null $mContinuationManager
Definition ApiMain.php:148
substituteResultWithError( $e)
Replace the result data with the information about an exception.
Definition ApiMain.php:1060
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:1415
getUpload( $name)
Get a request upload, and register the fact that it was used, for logging.
Definition ApiMain.php:1801
canApiHighLimits()
Check whether the current user is allowed to use high limits.
Definition ApiMain.php:2067
static $Formats
List of available formats: format name => format class.
Definition ApiMain.php:113
$mCanApiHighLimits
Definition ApiMain.php:2061
errorMessagesFromException( $e, $type='error')
Create an error message for the given exception.
Definition ApiMain.php:1022
handleCORS()
Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
Definition ApiMain.php:672
execute()
Execute api request.
Definition ApiMain.php:498
$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
const NO_SIZE_CHECK
For addValue() and similar functions, do not check size while adding a value Don't use this unless yo...
Definition ApiResult.php:58
Exception used to abort API execution with an error.
getModulePath()
Fetch the responsible module name.
getStatusValue()
Fetch the error status.
msg( $key)
Get a Message object with context set Parameters are the same as wfMessage()
IContextSource $context
getContext()
Get the base IContextSource object.
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:1693
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 newUUIDv1()
Return an RFC4122 compliant v1 UUID.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:48
static isEveryoneAllowed( $right)
Check if all users may be assumed to have the given permission.
Definition User.php:5082
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:585
static getGroupsWithPermission( $role)
Get all the groups who have a given permission.
Definition User.php:5039
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1802
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition hooks.txt:2843
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImgAuthModifyHeaders':Executed just before a file is streamed to a user via img_auth.php, allowing headers to be modified beforehand. $title:LinkTarget object & $headers:HTTP headers(name=> value, names are case insensitive). Two headers get special handling:If-Modified-Since(value must be a valid HTTP date) and Range(must be of the form "bytes=(\d*-\d*)") will be honored when streaming the file. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition hooks.txt:1991
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition hooks.txt:855
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:1999
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition hooks.txt:856
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2003
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3069
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:271
this hook is for auditing only $response
Definition hooks.txt:780
return true to allow those checks to and false if checking is done & $user
Definition hooks.txt:1510
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
returning false will NOT prevent logging $e
Definition hooks.txt:2175
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
Interface for MediaWiki-localized exceptions.
$help
Definition mcc.php:32
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
A helper class for throttling authentication attempts.
$params
$header