MediaWiki REL1_31
ApiMain.php
Go to the documentation of this file.
1<?php
26use Wikimedia\Timestamp\TimestampException;
29
43class ApiMain extends ApiBase {
47 const API_DEFAULT_FORMAT = 'jsonfm';
48
52 const API_DEFAULT_USELANG = 'user';
53
57 private static $Modules = [
58 'login' => ApiLogin::class,
59 'clientlogin' => ApiClientLogin::class,
60 'logout' => ApiLogout::class,
61 'createaccount' => ApiAMCreateAccount::class,
62 'linkaccount' => ApiLinkAccount::class,
63 'unlinkaccount' => ApiRemoveAuthenticationData::class,
64 'changeauthenticationdata' => ApiChangeAuthenticationData::class,
65 'removeauthenticationdata' => ApiRemoveAuthenticationData::class,
66 'resetpassword' => ApiResetPassword::class,
67 'query' => ApiQuery::class,
68 'expandtemplates' => ApiExpandTemplates::class,
69 'parse' => ApiParse::class,
70 'stashedit' => ApiStashEdit::class,
71 'opensearch' => ApiOpenSearch::class,
72 'feedcontributions' => ApiFeedContributions::class,
73 'feedrecentchanges' => ApiFeedRecentChanges::class,
74 'feedwatchlist' => ApiFeedWatchlist::class,
75 'help' => ApiHelp::class,
76 'paraminfo' => ApiParamInfo::class,
77 'rsd' => ApiRsd::class,
78 'compare' => ApiComparePages::class,
79 'tokens' => ApiTokens::class,
80 'checktoken' => ApiCheckToken::class,
81 'cspreport' => ApiCSPReport::class,
82 'validatepassword' => ApiValidatePassword::class,
83
84 // Write modules
85 'purge' => ApiPurge::class,
86 'setnotificationtimestamp' => ApiSetNotificationTimestamp::class,
87 'rollback' => ApiRollback::class,
88 'delete' => ApiDelete::class,
89 'undelete' => ApiUndelete::class,
90 'protect' => ApiProtect::class,
91 'block' => ApiBlock::class,
92 'unblock' => ApiUnblock::class,
93 'move' => ApiMove::class,
94 'edit' => ApiEditPage::class,
95 'upload' => ApiUpload::class,
96 'filerevert' => ApiFileRevert::class,
97 'emailuser' => ApiEmailUser::class,
98 'watch' => ApiWatch::class,
99 'patrol' => ApiPatrol::class,
100 'import' => ApiImport::class,
101 'clearhasmsg' => ApiClearHasMsg::class,
102 'userrights' => ApiUserrights::class,
103 'options' => ApiOptions::class,
104 'imagerotate' => ApiImageRotate::class,
105 'revisiondelete' => ApiRevisionDelete::class,
106 'managetags' => ApiManageTags::class,
107 'tag' => ApiTag::class,
108 'mergehistory' => ApiMergeHistory::class,
109 'setpagelanguage' => ApiSetPageLanguage::class,
110 ];
111
115 private static $Formats = [
116 'json' => ApiFormatJson::class,
117 'jsonfm' => ApiFormatJson::class,
118 'php' => ApiFormatPhp::class,
119 'phpfm' => ApiFormatPhp::class,
120 'xml' => ApiFormatXml::class,
121 'xmlfm' => ApiFormatXml::class,
122 'rawfm' => ApiFormatJson::class,
123 'none' => ApiFormatNone::class,
124 ];
125
132 private static $mRights = [
133 'writeapi' => [
134 'msg' => 'right-writeapi',
135 'params' => []
136 ],
137 'apihighlimits' => [
138 'msg' => 'api-help-right-apihighlimits',
140 ]
141 ];
142
146 private $mPrinter;
147
151 private $mAction;
155 private $mModule;
156
157 private $mCacheMode = 'private';
158 private $mCacheControl = [];
159 private $mParamsUsed = [];
160 private $mParamsSensitive = [];
161
164
172 public function __construct( $context = null, $enableWrite = false ) {
173 if ( $context === null ) {
175 } elseif ( $context instanceof WebRequest ) {
176 // BC for pre-1.19
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' => wfWikiID(),
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 $uselang = $request->getVal( 'uselang', self::API_DEFAULT_USELANG );
244 if ( $uselang === 'user' ) {
245 // Assume the parent context is going to return the user language
246 // for uselang=user (see T85635).
247 } else {
248 if ( $uselang === 'content' ) {
249 global $wgContLang;
250 $uselang = $wgContLang->getCode();
251 }
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 $errorFormat = $request->getVal( 'errorformat', 'bc' );
264 $errorLangCode = $request->getVal( 'errorlang', 'uselang' );
265 $errorsUseDB = $request->getCheck( 'errorsuselocal' );
266 if ( in_array( $errorFormat, [ 'plaintext', 'wikitext', 'html', 'raw', 'none' ], true ) ) {
267 if ( $errorLangCode === 'uselang' ) {
268 $errorLang = $this->getLanguage();
269 } elseif ( $errorLangCode === 'content' ) {
270 global $wgContLang;
271 $errorLang = $wgContLang;
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( $this );
285 $this->mModuleMgr->addModules( self::$Modules, 'action' );
286 $this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
287 $this->mModuleMgr->addModules( self::$Formats, 'format' );
288 $this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' );
289
290 Hooks::run( 'ApiMain::moduleManager', [ $this->mModuleMgr ] );
291
292 $this->mContinuationManager = null;
293 $this->mEnableWrite = $enableWrite;
294
295 $this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling()
296 $this->mCommit = false;
297 }
298
303 public function isInternalMode() {
305 }
306
312 public function getResult() {
313 return $this->mResult;
314 }
315
320 public function lacksSameOriginSecurity() {
321 if ( $this->lacksSameOriginSecurity !== null ) {
323 }
324
325 $request = $this->getRequest();
326
327 // JSONP mode
328 if ( $request->getVal( 'callback' ) !== null ) {
329 $this->lacksSameOriginSecurity = true;
330 return true;
331 }
332
333 // Anonymous CORS
334 if ( $request->getVal( 'origin' ) === '*' ) {
335 $this->lacksSameOriginSecurity = true;
336 return true;
337 }
338
339 // Header to be used from XMLHTTPRequest when the request might
340 // otherwise be used for XSS.
341 if ( $request->getHeader( 'Treat-as-Untrusted' ) !== false ) {
342 $this->lacksSameOriginSecurity = true;
343 return true;
344 }
345
346 // Allow extensions to override.
347 $this->lacksSameOriginSecurity = !Hooks::run( 'RequestHasSameOriginSecurity', [ $request ] );
349 }
350
355 public function getErrorFormatter() {
357 }
358
363 public function getContinuationManager() {
365 }
366
371 public function setContinuationManager( ApiContinuationManager $manager = null ) {
372 if ( $manager !== null && $this->mContinuationManager !== null ) {
373 throw new UnexpectedValueException(
374 __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
375 ' when a manager is already set from ' . $this->mContinuationManager->getSource()
376 );
377 }
378 $this->mContinuationManager = $manager;
379 }
380
386 public function getModule() {
387 return $this->mModule;
388 }
389
395 public function getPrinter() {
396 return $this->mPrinter;
397 }
398
404 public function setCacheMaxAge( $maxage ) {
405 $this->setCacheControl( [
406 'max-age' => $maxage,
407 's-maxage' => $maxage
408 ] );
409 }
410
436 public function setCacheMode( $mode ) {
437 if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
438 wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" );
439
440 // Ignore for forwards-compatibility
441 return;
442 }
443
444 if ( !User::isEveryoneAllowed( 'read' ) ) {
445 // Private wiki, only private headers
446 if ( $mode !== 'private' ) {
447 wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
448
449 return;
450 }
451 }
452
453 if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
454 // User language is used for i18n, so we don't want to publicly
455 // cache. Anons are ok, because if they have non-default language
456 // then there's an appropriate Vary header set by whatever set
457 // their non-default language.
458 wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
459 "'anon-public-user-private' due to uselang=user\n" );
460 $mode = 'anon-public-user-private';
461 }
462
463 wfDebug( __METHOD__ . ": setting cache mode $mode\n" );
464 $this->mCacheMode = $mode;
465 }
466
477 public function setCacheControl( $directives ) {
478 $this->mCacheControl = $directives + $this->mCacheControl;
479 }
480
488 public function createPrinterByName( $format ) {
489 $printer = $this->mModuleMgr->getModule( $format, 'format' );
490 if ( $printer === null ) {
491 $this->dieWithError(
492 [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
493 );
494 }
495
496 return $printer;
497 }
498
502 public function execute() {
503 if ( $this->mInternalMode ) {
504 $this->executeAction();
505 } else {
507 }
508 }
509
514 protected function executeActionWithErrorHandling() {
515 // Verify the CORS header before executing the action
516 if ( !$this->handleCORS() ) {
517 // handleCORS() has sent a 403, abort
518 return;
519 }
520
521 // Exit here if the request method was OPTIONS
522 // (assume there will be a followup GET or POST)
523 if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
524 return;
525 }
526
527 // In case an error occurs during data output,
528 // clear the output buffer and print just the error information
529 $obLevel = ob_get_level();
530 ob_start();
531
532 $t = microtime( true );
533 $isError = false;
534 try {
535 $this->executeAction();
536 $runTime = microtime( true ) - $t;
537 $this->logRequest( $runTime );
538 if ( $this->mModule->isWriteMode() && $this->getRequest()->wasPosted() ) {
539 MediaWikiServices::getInstance()->getStatsdDataFactory()->timing(
540 'api.' . $this->mModule->getModuleName() . '.executeTiming', 1000 * $runTime
541 );
542 }
543 } catch ( Exception $e ) {
544 $this->handleException( $e );
545 $this->logRequest( microtime( true ) - $t, $e );
546 $isError = true;
547 }
548
549 // Commit DBs and send any related cookies and headers
550 MediaWiki::preOutputCommit( $this->getContext() );
551
552 // Send cache headers after any code which might generate an error, to
553 // avoid sending public cache headers for errors.
554 $this->sendCacheHeaders( $isError );
555
556 // Executing the action might have already messed with the output
557 // buffers.
558 while ( ob_get_level() > $obLevel ) {
559 ob_end_flush();
560 }
561 }
562
569 protected function handleException( Exception $e ) {
570 // T65145: Rollback any open database transactions
571 if ( !( $e instanceof ApiUsageException || $e instanceof UsageException ) ) {
572 // UsageExceptions are intentional, so don't rollback if that's the case
573 MWExceptionHandler::rollbackMasterChangesAndLog( $e );
574 }
575
576 // Allow extra cleanup and logging
577 Hooks::run( 'ApiMain::onException', [ $this, $e ] );
578
579 // Handle any kind of exception by outputting properly formatted error message.
580 // If this fails, an unhandled exception should be thrown so that global error
581 // handler will process and log it.
582
583 $errCodes = $this->substituteResultWithError( $e );
584
585 // Error results should not be cached
586 $this->setCacheMode( 'private' );
587
588 $response = $this->getRequest()->response();
589 $headerStr = 'MediaWiki-API-Error: ' . implode( ', ', $errCodes );
590 $response->header( $headerStr );
591
592 // Reset and print just the error message
593 ob_clean();
594
595 // Printer may not be initialized if the extractRequestParams() fails for the main module
596 $this->createErrorPrinter();
597
598 $failed = false;
599 try {
600 $this->printResult( $e->getCode() );
601 } catch ( ApiUsageException $ex ) {
602 // The error printer itself is failing. Try suppressing its request
603 // parameters and redo.
604 $failed = true;
605 $this->addWarning( 'apiwarn-errorprinterfailed' );
606 foreach ( $ex->getStatusValue()->getErrors() as $error ) {
607 try {
608 $this->mPrinter->addWarning( $error );
609 } catch ( Exception $ex2 ) {
610 // WTF?
611 $this->addWarning( $error );
612 }
613 }
614 } catch ( UsageException $ex ) {
615 // The error printer itself is failing. Try suppressing its request
616 // parameters and redo.
617 $failed = true;
618 $this->addWarning(
619 [ 'apiwarn-errorprinterfailed-ex', $ex->getMessage() ], 'errorprinterfailed'
620 );
621 }
622 if ( $failed ) {
623 $this->mPrinter = null;
624 $this->createErrorPrinter();
625 $this->mPrinter->forceDefaultParams();
626 if ( $e->getCode() ) {
627 $response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200
628 }
629 $this->printResult( $e->getCode() );
630 }
631 }
632
643 public static function handleApiBeforeMainException( Exception $e ) {
644 ob_start();
645
646 try {
647 $main = new self( RequestContext::getMain(), false );
648 $main->handleException( $e );
649 $main->logRequest( 0, $e );
650 } catch ( Exception $e2 ) {
651 // Nope, even that didn't work. Punt.
652 throw $e;
653 }
654
655 // Reset cache headers
656 $main->sendCacheHeaders( true );
657
658 ob_end_flush();
659 }
660
675 protected function handleCORS() {
676 $originParam = $this->getParameter( 'origin' ); // defaults to null
677 if ( $originParam === null ) {
678 // No origin parameter, nothing to do
679 return true;
680 }
681
682 $request = $this->getRequest();
683 $response = $request->response();
684
685 $matchedOrigin = false;
686 $allowTiming = false;
687 $varyOrigin = true;
688
689 if ( $originParam === '*' ) {
690 // Request for anonymous CORS
691 // Technically we should check for the presence of an Origin header
692 // and not process it as CORS if it's not set, but that would
693 // require us to vary on Origin for all 'origin=*' requests which
694 // we don't want to do.
695 $matchedOrigin = true;
696 $allowOrigin = '*';
697 $allowCredentials = 'false';
698 $varyOrigin = false; // No need to vary
699 } else {
700 // Non-anonymous CORS, check we allow the domain
701
702 // Origin: header is a space-separated list of origins, check all of them
703 $originHeader = $request->getHeader( 'Origin' );
704 if ( $originHeader === false ) {
705 $origins = [];
706 } else {
707 $originHeader = trim( $originHeader );
708 $origins = preg_split( '/\s+/', $originHeader );
709 }
710
711 if ( !in_array( $originParam, $origins ) ) {
712 // origin parameter set but incorrect
713 // Send a 403 response
714 $response->statusHeader( 403 );
715 $response->header( 'Cache-Control: no-cache' );
716 echo "'origin' parameter does not match Origin header\n";
717
718 return false;
719 }
720
721 $config = $this->getConfig();
722 $matchedOrigin = count( $origins ) === 1 && self::matchOrigin(
723 $originParam,
724 $config->get( 'CrossSiteAJAXdomains' ),
725 $config->get( 'CrossSiteAJAXdomainExceptions' )
726 );
727
728 $allowOrigin = $originHeader;
729 $allowCredentials = 'true';
730 $allowTiming = $originHeader;
731 }
732
733 if ( $matchedOrigin ) {
734 $requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
735 $preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
736 if ( $preflight ) {
737 // This is a CORS preflight request
738 if ( $requestedMethod !== 'POST' && $requestedMethod !== 'GET' ) {
739 // If method is not a case-sensitive match, do not set any additional headers and terminate.
740 $response->header( 'MediaWiki-CORS-Rejection: Unsupported method requested in preflight' );
741 return true;
742 }
743 // We allow the actual request to send the following headers
744 $requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
745 if ( $requestedHeaders !== false ) {
746 if ( !self::matchRequestedHeaders( $requestedHeaders ) ) {
747 $response->header( 'MediaWiki-CORS-Rejection: Unsupported header requested in preflight' );
748 return true;
749 }
750 $response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
751 }
752
753 // We only allow the actual request to be GET or POST
754 $response->header( 'Access-Control-Allow-Methods: POST, GET' );
755 } elseif ( $request->getMethod() !== 'POST' && $request->getMethod() !== 'GET' ) {
756 // Unsupported non-preflight method, don't handle it as CORS
757 $response->header(
758 'MediaWiki-CORS-Rejection: Unsupported method for simple request or actual request'
759 );
760 return true;
761 }
762
763 $response->header( "Access-Control-Allow-Origin: $allowOrigin" );
764 $response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
765 // https://www.w3.org/TR/resource-timing/#timing-allow-origin
766 if ( $allowTiming !== false ) {
767 $response->header( "Timing-Allow-Origin: $allowTiming" );
768 }
769
770 if ( !$preflight ) {
771 $response->header(
772 'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag, '
773 . 'MediaWiki-Login-Suppressed'
774 );
775 }
776 } else {
777 $response->header( 'MediaWiki-CORS-Rejection: Origin mismatch' );
778 }
779
780 if ( $varyOrigin ) {
781 $this->getOutput()->addVaryHeader( 'Origin' );
782 }
783
784 return true;
785 }
786
795 protected static function matchOrigin( $value, $rules, $exceptions ) {
796 foreach ( $rules as $rule ) {
797 if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
798 // Rule matches, check exceptions
799 foreach ( $exceptions as $exc ) {
800 if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
801 return false;
802 }
803 }
804
805 return true;
806 }
807 }
808
809 return false;
810 }
811
819 protected static function matchRequestedHeaders( $requestedHeaders ) {
820 if ( trim( $requestedHeaders ) === '' ) {
821 return true;
822 }
823 $requestedHeaders = explode( ',', $requestedHeaders );
824 $allowedAuthorHeaders = array_flip( [
825 /* simple headers (see spec) */
826 'accept',
827 'accept-language',
828 'content-language',
829 'content-type',
830 /* non-authorable headers in XHR, which are however requested by some UAs */
831 'accept-encoding',
832 'dnt',
833 'origin',
834 /* MediaWiki whitelist */
835 'api-user-agent',
836 ] );
837 foreach ( $requestedHeaders as $rHeader ) {
838 $rHeader = strtolower( trim( $rHeader ) );
839 if ( !isset( $allowedAuthorHeaders[$rHeader] ) ) {
840 wfDebugLog( 'api', 'CORS preflight failed on requested header: ' . $rHeader );
841 return false;
842 }
843 }
844 return true;
845 }
846
855 protected static function wildcardToRegex( $wildcard ) {
856 $wildcard = preg_quote( $wildcard, '/' );
857 $wildcard = str_replace(
858 [ '\*', '\?' ],
859 [ '.*?', '.' ],
860 $wildcard
861 );
862
863 return "/^https?:\/\/$wildcard$/";
864 }
865
871 protected function sendCacheHeaders( $isError ) {
872 $response = $this->getRequest()->response();
873 $out = $this->getOutput();
874
875 $out->addVaryHeader( 'Treat-as-Untrusted' );
876
877 $config = $this->getConfig();
878
879 if ( $config->get( 'VaryOnXFP' ) ) {
880 $out->addVaryHeader( 'X-Forwarded-Proto' );
881 }
882
883 if ( !$isError && $this->mModule &&
884 ( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
885 ) {
886 $etag = $this->mModule->getConditionalRequestData( 'etag' );
887 if ( $etag !== null ) {
888 $response->header( "ETag: $etag" );
889 }
890 $lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
891 if ( $lastMod !== null ) {
892 $response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
893 }
894 }
895
896 // The logic should be:
897 // $this->mCacheControl['max-age'] is set?
898 // Use it, the module knows better than our guess.
899 // !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
900 // Use 0 because we can guess caching is probably the wrong thing to do.
901 // Use $this->getParameter( 'maxage' ), which already defaults to 0.
902 $maxage = 0;
903 if ( isset( $this->mCacheControl['max-age'] ) ) {
904 $maxage = $this->mCacheControl['max-age'];
905 } elseif ( ( $this->mModule && !$this->mModule->isWriteMode() ) ||
906 $this->mCacheMode !== 'private'
907 ) {
908 $maxage = $this->getParameter( 'maxage' );
909 }
910 $privateCache = 'private, must-revalidate, max-age=' . $maxage;
911
912 if ( $this->mCacheMode == 'private' ) {
913 $response->header( "Cache-Control: $privateCache" );
914 return;
915 }
916
917 $useKeyHeader = $config->get( 'UseKeyHeader' );
918 if ( $this->mCacheMode == 'anon-public-user-private' ) {
919 $out->addVaryHeader( 'Cookie' );
920 $response->header( $out->getVaryHeader() );
921 if ( $useKeyHeader ) {
922 $response->header( $out->getKeyHeader() );
923 if ( $out->haveCacheVaryCookies() ) {
924 // Logged in, mark this request private
925 $response->header( "Cache-Control: $privateCache" );
926 return;
927 }
928 // Logged out, send normal public headers below
929 } elseif ( MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
930 // Logged in or otherwise has session (e.g. anonymous users who have edited)
931 // Mark request private
932 $response->header( "Cache-Control: $privateCache" );
933
934 return;
935 } // else no Key and anonymous, send public headers below
936 }
937
938 // Send public headers
939 $response->header( $out->getVaryHeader() );
940 if ( $useKeyHeader ) {
941 $response->header( $out->getKeyHeader() );
942 }
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' ) ) {
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
1023 protected function errorMessagesFromException( $e, $type = 'error' ) {
1024 $messages = [];
1025 if ( $e instanceof ApiUsageException ) {
1026 foreach ( $e->getStatusValue()->getErrorsByType( $type ) as $error ) {
1027 $messages[] = ApiMessage::create( $error );
1028 }
1029 } elseif ( $type !== 'error' ) {
1030 // None of the rest have any messages for non-error types
1031 } elseif ( $e instanceof UsageException ) {
1032 // User entered incorrect parameters - generate error response
1033 $data = Wikimedia\quietCall( [ $e, 'getMessageArray' ] );
1034 $code = $data['code'];
1035 $info = $data['info'];
1036 unset( $data['code'], $data['info'] );
1037 $messages[] = new ApiRawMessage( [ '$1', $info ], $code, $data );
1038 } else {
1039 // Something is seriously wrong
1040 $config = $this->getConfig();
1041 $class = preg_replace( '#^Wikimedia\\\Rdbms\\\#', '', get_class( $e ) );
1042 $code = 'internal_api_error_' . $class;
1043 if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) {
1044 $params = [ 'apierror-databaseerror', WebRequest::getRequestId() ];
1045 } else {
1046 if ( $e instanceof ILocalizedException ) {
1047 $msg = $e->getMessageObject();
1048 } elseif ( $e instanceof MessageSpecifier ) {
1049 $msg = Message::newFromSpecifier( $e );
1050 } else {
1051 $msg = wfEscapeWikiText( $e->getMessage() );
1052 }
1053 $params = [ 'apierror-exceptioncaught', WebRequest::getRequestId(), $msg ];
1054 }
1056 }
1057 return $messages;
1058 }
1059
1065 protected function substituteResultWithError( $e ) {
1066 $result = $this->getResult();
1067 $formatter = $this->getErrorFormatter();
1068 $config = $this->getConfig();
1069 $errorCodes = [];
1070
1071 // Remember existing warnings and errors across the reset
1072 $errors = $result->getResultData( [ 'errors' ] );
1073 $warnings = $result->getResultData( [ 'warnings' ] );
1074 $result->reset();
1075 if ( $warnings !== null ) {
1076 $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
1077 }
1078 if ( $errors !== null ) {
1079 $result->addValue( null, 'errors', $errors, ApiResult::NO_SIZE_CHECK );
1080
1081 // Collect the copied error codes for the return value
1082 foreach ( $errors as $error ) {
1083 if ( isset( $error['code'] ) ) {
1084 $errorCodes[$error['code']] = true;
1085 }
1086 }
1087 }
1088
1089 // Add errors from the exception
1090 $modulePath = $e instanceof ApiUsageException ? $e->getModulePath() : null;
1091 foreach ( $this->errorMessagesFromException( $e, 'error' ) as $msg ) {
1092 $errorCodes[$msg->getApiCode()] = true;
1093 $formatter->addError( $modulePath, $msg );
1094 }
1095 foreach ( $this->errorMessagesFromException( $e, 'warning' ) as $msg ) {
1096 $formatter->addWarning( $modulePath, $msg );
1097 }
1098
1099 // Add additional data. Path depends on whether we're in BC mode or not.
1100 // Data depends on the type of exception.
1101 if ( $formatter instanceof ApiErrorFormatter_BackCompat ) {
1102 $path = [ 'error' ];
1103 } else {
1104 $path = null;
1105 }
1106 if ( $e instanceof ApiUsageException || $e instanceof UsageException ) {
1107 $link = wfExpandUrl( wfScript( 'api' ) );
1108 $result->addContentValue(
1109 $path,
1110 'docref',
1111 trim(
1112 $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text()
1113 . ' '
1114 . $this->msg( 'api-usage-mailinglist-ref' )->inLanguage( $formatter->getLanguage() )->text()
1115 )
1116 );
1117 } else {
1118 if ( $config->get( 'ShowExceptionDetails' ) &&
1119 ( !$e instanceof DBError || $config->get( 'ShowDBErrorBacktrace' ) )
1120 ) {
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
1134 // Add the id and such
1135 $this->addRequestedFields( [ 'servedby' ] );
1136
1137 return array_keys( $errorCodes );
1138 }
1139
1145 protected function addRequestedFields( $force = [] ) {
1146 $result = $this->getResult();
1147
1148 $requestid = $this->getParameter( 'requestid' );
1149 if ( $requestid !== null ) {
1150 $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
1151 }
1152
1153 if ( $this->getConfig()->get( 'ShowHostnames' ) && (
1154 in_array( 'servedby', $force, true ) || $this->getParameter( 'servedby' )
1155 ) ) {
1156 $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
1157 }
1158
1159 if ( $this->getParameter( 'curtimestamp' ) ) {
1160 $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601, time() ),
1162 }
1163
1164 if ( $this->getParameter( 'responselanginfo' ) ) {
1165 $result->addValue( null, 'uselang', $this->getLanguage()->getCode(),
1167 $result->addValue( null, 'errorlang', $this->getErrorFormatter()->getLanguage()->getCode(),
1169 }
1170 }
1171
1176 protected function setupExecuteAction() {
1177 $this->addRequestedFields();
1178
1179 $params = $this->extractRequestParams();
1180 $this->mAction = $params['action'];
1181
1182 return $params;
1183 }
1184
1191 protected function setupModule() {
1192 // Instantiate the module requested by the user
1193 $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
1194 if ( $module === null ) {
1195 // Probably can't happen
1196 // @codeCoverageIgnoreStart
1197 $this->dieWithError(
1198 [ 'apierror-unknownaction', wfEscapeWikiText( $this->mAction ) ], 'unknown_action'
1199 );
1200 // @codeCoverageIgnoreEnd
1201 }
1202 $moduleParams = $module->extractRequestParams();
1203
1204 // Check token, if necessary
1205 if ( $module->needsToken() === true ) {
1206 throw new MWException(
1207 "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
1208 'See documentation for ApiBase::needsToken for details.'
1209 );
1210 }
1211 if ( $module->needsToken() ) {
1212 if ( !$module->mustBePosted() ) {
1213 throw new MWException(
1214 "Module '{$module->getModuleName()}' must require POST to use tokens."
1215 );
1216 }
1217
1218 if ( !isset( $moduleParams['token'] ) ) {
1219 // Probably can't happen
1220 // @codeCoverageIgnoreStart
1221 $module->dieWithError( [ 'apierror-missingparam', 'token' ] );
1222 // @codeCoverageIgnoreEnd
1223 }
1224
1225 $module->requirePostedParameters( [ 'token' ] );
1226
1227 if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
1228 $module->dieWithError( 'apierror-badtoken' );
1229 }
1230 }
1231
1232 return $module;
1233 }
1234
1238 private function getMaxLag() {
1239 $dbLag = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaxLag();
1240 $lagInfo = [
1241 'host' => $dbLag[0],
1242 'lag' => $dbLag[1],
1243 'type' => 'db'
1244 ];
1245
1246 $jobQueueLagFactor = $this->getConfig()->get( 'JobQueueIncludeInMaxLagFactor' );
1247 if ( $jobQueueLagFactor ) {
1248 // Turn total number of jobs into seconds by using the configured value
1249 $totalJobs = array_sum( JobQueueGroup::singleton()->getQueueSizes() );
1250 $jobQueueLag = $totalJobs / (float)$jobQueueLagFactor;
1251 if ( $jobQueueLag > $lagInfo['lag'] ) {
1252 $lagInfo = [
1253 'host' => wfHostname(), // XXX: Is there a better value that could be used?
1254 'lag' => $jobQueueLag,
1255 'type' => 'jobqueue',
1256 'jobs' => $totalJobs,
1257 ];
1258 }
1259 }
1260
1261 return $lagInfo;
1262 }
1263
1270 protected function checkMaxLag( $module, $params ) {
1271 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
1272 $maxLag = $params['maxlag'];
1273 $lagInfo = $this->getMaxLag();
1274 if ( $lagInfo['lag'] > $maxLag ) {
1275 $response = $this->getRequest()->response();
1276
1277 $response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
1278 $response->header( 'X-Database-Lag: ' . intval( $lagInfo['lag'] ) );
1279
1280 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1281 $this->dieWithError(
1282 [ 'apierror-maxlag', $lagInfo['lag'], $lagInfo['host'] ],
1283 'maxlag',
1284 $lagInfo
1285 );
1286 }
1287
1288 $this->dieWithError( [ 'apierror-maxlag-generic', $lagInfo['lag'] ], 'maxlag', $lagInfo );
1289 }
1290 }
1291
1292 return true;
1293 }
1294
1316 protected function checkConditionalRequestHeaders( $module ) {
1317 if ( $this->mInternalMode ) {
1318 // No headers to check in internal mode
1319 return true;
1320 }
1321
1322 if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) {
1323 // Don't check POSTs
1324 return true;
1325 }
1326
1327 $return304 = false;
1328
1329 $ifNoneMatch = array_diff(
1330 $this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: [],
1331 [ '' ]
1332 );
1333 if ( $ifNoneMatch ) {
1334 if ( $ifNoneMatch === [ '*' ] ) {
1335 // API responses always "exist"
1336 $etag = '*';
1337 } else {
1338 $etag = $module->getConditionalRequestData( 'etag' );
1339 }
1340 }
1341 if ( $ifNoneMatch && $etag !== null ) {
1342 $test = substr( $etag, 0, 2 ) === 'W/' ? substr( $etag, 2 ) : $etag;
1343 $match = array_map( function ( $s ) {
1344 return substr( $s, 0, 2 ) === 'W/' ? substr( $s, 2 ) : $s;
1345 }, $ifNoneMatch );
1346 $return304 = in_array( $test, $match, true );
1347 } else {
1348 $value = trim( $this->getRequest()->getHeader( 'If-Modified-Since' ) );
1349
1350 // Some old browsers sends sizes after the date, like this:
1351 // Wed, 20 Aug 2003 06:51:19 GMT; length=5202
1352 // Ignore that.
1353 $i = strpos( $value, ';' );
1354 if ( $i !== false ) {
1355 $value = trim( substr( $value, 0, $i ) );
1356 }
1357
1358 if ( $value !== '' ) {
1359 try {
1360 $ts = new MWTimestamp( $value );
1361 if (
1362 // RFC 7231 IMF-fixdate
1363 $ts->getTimestamp( TS_RFC2822 ) === $value ||
1364 // RFC 850
1365 $ts->format( 'l, d-M-y H:i:s' ) . ' GMT' === $value ||
1366 // asctime (with and without space-padded day)
1367 $ts->format( 'D M j H:i:s Y' ) === $value ||
1368 $ts->format( 'D M j H:i:s Y' ) === $value
1369 ) {
1370 $lastMod = $module->getConditionalRequestData( 'last-modified' );
1371 if ( $lastMod !== null ) {
1372 // Mix in some MediaWiki modification times
1373 $modifiedTimes = [
1374 'page' => $lastMod,
1375 'user' => $this->getUser()->getTouched(),
1376 'epoch' => $this->getConfig()->get( 'CacheEpoch' ),
1377 ];
1378 if ( $this->getConfig()->get( 'UseSquid' ) ) {
1379 // T46570: the core page itself may not change, but resources might
1380 $modifiedTimes['sepoch'] = wfTimestamp(
1381 TS_MW, time() - $this->getConfig()->get( 'SquidMaxage' )
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() && !User::isEveryoneAllowed( 'read' ) &&
1417 !$user->isAllowed( 'read' )
1418 ) {
1419 $this->dieWithError( 'apierror-readapidenied' );
1420 }
1421
1422 if ( $module->isWriteMode() ) {
1423 if ( !$this->mEnableWrite ) {
1424 $this->dieWithError( 'apierror-noapiwrite' );
1425 } elseif ( !$user->isAllowed( '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',
1480 "Api request failed as read only because the following DBs are lagged: $laggedServers"
1481 );
1482
1483 $this->dieWithError(
1484 'readonly_lag',
1485 'readonly',
1486 [ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ]
1487 );
1488 }
1489 }
1490
1495 protected function checkAsserts( $params ) {
1496 if ( isset( $params['assert'] ) ) {
1497 $user = $this->getUser();
1498 switch ( $params['assert'] ) {
1499 case 'user':
1500 if ( $user->isAnon() ) {
1501 $this->dieWithError( 'apierror-assertuserfailed' );
1502 }
1503 break;
1504 case 'bot':
1505 if ( !$user->isAllowed( 'bot' ) ) {
1506 $this->dieWithError( 'apierror-assertbotfailed' );
1507 }
1508 break;
1509 }
1510 }
1511 if ( isset( $params['assertuser'] ) ) {
1512 $assertUser = User::newFromName( $params['assertuser'], false );
1513 if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) {
1514 $this->dieWithError(
1515 [ 'apierror-assertnameduserfailed', wfEscapeWikiText( $params['assertuser'] ) ]
1516 );
1517 }
1518 }
1519 }
1520
1526 protected function setupExternalResponse( $module, $params ) {
1527 $request = $this->getRequest();
1528 if ( !$request->wasPosted() && $module->mustBePosted() ) {
1529 // Module requires POST. GET request might still be allowed
1530 // if $wgDebugApi is true, otherwise fail.
1531 $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] );
1532 }
1533
1534 // See if custom printer is used
1535 $this->mPrinter = $module->getCustomPrinter();
1536 if ( is_null( $this->mPrinter ) ) {
1537 // Create an appropriate printer
1538 $this->mPrinter = $this->createPrinterByName( $params['format'] );
1539 }
1540
1541 if ( $request->getProtocol() === 'http' &&
1542 (
1543 $this->getConfig()->get( 'ForceHTTPS' ) ||
1544 $request->getSession()->shouldForceHTTPS() ||
1545 ( $this->getUser()->isLoggedIn() &&
1546 $this->getUser()->requiresHTTPS() )
1547 )
1548 ) {
1549 $this->addDeprecation( 'apiwarn-deprecation-httpsexpected', 'https-expected' );
1550 }
1551 }
1552
1556 protected function executeAction() {
1557 $params = $this->setupExecuteAction();
1558 $module = $this->setupModule();
1559 $this->mModule = $module;
1560
1561 if ( !$this->mInternalMode ) {
1562 $this->setRequestExpectations( $module );
1563 }
1564
1565 $this->checkExecutePermissions( $module );
1566
1567 if ( !$this->checkMaxLag( $module, $params ) ) {
1568 return;
1569 }
1570
1571 if ( !$this->checkConditionalRequestHeaders( $module ) ) {
1572 return;
1573 }
1574
1575 if ( !$this->mInternalMode ) {
1576 $this->setupExternalResponse( $module, $params );
1577 }
1578
1579 $this->checkAsserts( $params );
1580
1581 // Execute
1582 $module->execute();
1583 Hooks::run( 'APIAfterExecute', [ &$module ] );
1584
1585 $this->reportUnusedParams();
1586
1587 if ( !$this->mInternalMode ) {
1588 // append Debug information
1589 MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
1590
1591 // Print result data
1592 $this->printResult();
1593 }
1594 }
1595
1600 protected function setRequestExpectations( ApiBase $module ) {
1601 $limits = $this->getConfig()->get( 'TrxProfilerLimits' );
1602 $trxProfiler = Profiler::instance()->getTransactionProfiler();
1603 $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
1604 if ( $this->getRequest()->hasSafeMethod() ) {
1605 $trxProfiler->setExpectations( $limits['GET'], __METHOD__ );
1606 } elseif ( $this->getRequest()->wasPosted() && !$module->isWriteMode() ) {
1607 $trxProfiler->setExpectations( $limits['POST-nonwrite'], __METHOD__ );
1608 $this->getRequest()->markAsSafeRequest();
1609 } else {
1610 $trxProfiler->setExpectations( $limits['POST'], __METHOD__ );
1611 }
1612 }
1613
1619 protected function logRequest( $time, $e = null ) {
1620 $request = $this->getRequest();
1621 $logCtx = [
1622 'ts' => time(),
1623 'ip' => $request->getIP(),
1624 'userAgent' => $this->getUserAgent(),
1625 'wiki' => wfWikiID(),
1626 'timeSpentBackend' => (int)round( $time * 1000 ),
1627 'hadError' => $e !== null,
1628 'errorCodes' => [],
1629 'params' => [],
1630 ];
1631
1632 if ( $e ) {
1633 foreach ( $this->errorMessagesFromException( $e ) as $msg ) {
1634 $logCtx['errorCodes'][] = $msg->getApiCode();
1635 }
1636 }
1637
1638 // Construct space separated message for 'api' log channel
1639 $msg = "API {$request->getMethod()} " .
1640 wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
1641 " {$logCtx['ip']} " .
1642 "T={$logCtx['timeSpentBackend']}ms";
1643
1644 $sensitive = array_flip( $this->getSensitiveParams() );
1645 foreach ( $this->getParamsUsed() as $name ) {
1646 $value = $request->getVal( $name );
1647 if ( $value === null ) {
1648 continue;
1649 }
1650
1651 if ( isset( $sensitive[$name] ) ) {
1652 $value = '[redacted]';
1653 $encValue = '[redacted]';
1654 } elseif ( strlen( $value ) > 256 ) {
1655 $value = substr( $value, 0, 256 );
1656 $encValue = $this->encodeRequestLogValue( $value ) . '[...]';
1657 } else {
1658 $encValue = $this->encodeRequestLogValue( $value );
1659 }
1660
1661 $logCtx['params'][$name] = $value;
1662 $msg .= " {$name}={$encValue}";
1663 }
1664
1665 wfDebugLog( 'api', $msg, 'private' );
1666 // ApiAction channel is for structured data consumers
1667 wfDebugLog( 'ApiAction', '', 'private', $logCtx );
1668 }
1669
1675 protected function encodeRequestLogValue( $s ) {
1676 static $table;
1677 if ( !$table ) {
1678 $chars = ';@$!*(),/:';
1679 $numChars = strlen( $chars );
1680 for ( $i = 0; $i < $numChars; $i++ ) {
1681 $table[rawurlencode( $chars[$i] )] = $chars[$i];
1682 }
1683 }
1684
1685 return strtr( rawurlencode( $s ), $table );
1686 }
1687
1692 protected function getParamsUsed() {
1693 return array_keys( $this->mParamsUsed );
1694 }
1695
1700 public function markParamsUsed( $params ) {
1701 $this->mParamsUsed += array_fill_keys( (array)$params, true );
1702 }
1703
1709 protected function getSensitiveParams() {
1710 return array_keys( $this->mParamsSensitive );
1711 }
1712
1718 public function markParamsSensitive( $params ) {
1719 $this->mParamsSensitive += array_fill_keys( (array)$params, true );
1720 }
1721
1728 public function getVal( $name, $default = null ) {
1729 $this->mParamsUsed[$name] = true;
1730
1731 $ret = $this->getRequest()->getVal( $name );
1732 if ( $ret === null ) {
1733 if ( $this->getRequest()->getArray( $name ) !== null ) {
1734 // See T12262 for why we don't just implode( '|', ... ) the
1735 // array.
1736 $this->addWarning( [ 'apiwarn-unsupportedarray', $name ] );
1737 }
1738 $ret = $default;
1739 }
1740 return $ret;
1741 }
1742
1749 public function getCheck( $name ) {
1750 return $this->getVal( $name, null ) !== null;
1751 }
1752
1760 public function getUpload( $name ) {
1761 $this->mParamsUsed[$name] = true;
1762
1763 return $this->getRequest()->getUpload( $name );
1764 }
1765
1770 protected function reportUnusedParams() {
1771 $paramsUsed = $this->getParamsUsed();
1772 $allParams = $this->getRequest()->getValueNames();
1773
1774 if ( !$this->mInternalMode ) {
1775 // Printer has not yet executed; don't warn that its parameters are unused
1776 $printerParams = $this->mPrinter->encodeParamName(
1777 array_keys( $this->mPrinter->getFinalParams() ?: [] )
1778 );
1779 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
1780 } else {
1781 $unusedParams = array_diff( $allParams, $paramsUsed );
1782 }
1783
1784 if ( count( $unusedParams ) ) {
1785 $this->addWarning( [
1786 'apierror-unrecognizedparams',
1787 Message::listParam( array_map( 'wfEscapeWikiText', $unusedParams ), 'comma' ),
1788 count( $unusedParams )
1789 ] );
1790 }
1791 }
1792
1798 protected function printResult( $httpCode = 0 ) {
1799 if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
1800 $this->addWarning( 'apiwarn-wgDebugAPI' );
1801 }
1802
1803 $printer = $this->mPrinter;
1804 $printer->initPrinter( false );
1805 if ( $httpCode ) {
1806 $printer->setHttpStatus( $httpCode );
1807 }
1808 $printer->execute();
1809 $printer->closePrinter();
1810 }
1811
1815 public function isReadMode() {
1816 return false;
1817 }
1818
1824 public function getAllowedParams() {
1825 return [
1826 'action' => [
1827 ApiBase::PARAM_DFLT => 'help',
1828 ApiBase::PARAM_TYPE => 'submodule',
1829 ],
1830 'format' => [
1832 ApiBase::PARAM_TYPE => 'submodule',
1833 ],
1834 'maxlag' => [
1835 ApiBase::PARAM_TYPE => 'integer'
1836 ],
1837 'smaxage' => [
1838 ApiBase::PARAM_TYPE => 'integer',
1840 ],
1841 'maxage' => [
1842 ApiBase::PARAM_TYPE => 'integer',
1844 ],
1845 'assert' => [
1846 ApiBase::PARAM_TYPE => [ 'user', 'bot' ]
1847 ],
1848 'assertuser' => [
1849 ApiBase::PARAM_TYPE => 'user',
1850 ],
1851 'requestid' => null,
1852 'servedby' => false,
1853 'curtimestamp' => false,
1854 'responselanginfo' => false,
1855 'origin' => null,
1856 'uselang' => [
1858 ],
1859 'errorformat' => [
1860 ApiBase::PARAM_TYPE => [ 'plaintext', 'wikitext', 'html', 'raw', 'none', 'bc' ],
1861 ApiBase::PARAM_DFLT => 'bc',
1862 ],
1863 'errorlang' => [
1864 ApiBase::PARAM_DFLT => 'uselang',
1865 ],
1866 'errorsuselocal' => [
1867 ApiBase::PARAM_DFLT => false,
1868 ],
1869 ];
1870 }
1871
1873 protected function getExamplesMessages() {
1874 return [
1875 'action=help'
1876 => 'apihelp-help-example-main',
1877 'action=help&recursivesubmodules=1'
1878 => 'apihelp-help-example-recursive',
1879 ];
1880 }
1881
1882 public function modifyHelp( array &$help, array $options, array &$tocData ) {
1883 // Wish PHP had an "array_insert_before". Instead, we have to manually
1884 // reindex the array to get 'permissions' in the right place.
1885 $oldHelp = $help;
1886 $help = [];
1887 foreach ( $oldHelp as $k => $v ) {
1888 if ( $k === 'submodules' ) {
1889 $help['permissions'] = '';
1890 }
1891 $help[$k] = $v;
1892 }
1893 $help['datatypes'] = '';
1894 $help['credits'] = '';
1895
1896 // Fill 'permissions'
1897 $help['permissions'] .= Html::openElement( 'div',
1898 [ 'class' => 'apihelp-block apihelp-permissions' ] );
1899 $m = $this->msg( 'api-help-permissions' );
1900 if ( !$m->isDisabled() ) {
1901 $help['permissions'] .= Html::rawElement( 'div', [ 'class' => 'apihelp-block-head' ],
1902 $m->numParams( count( self::$mRights ) )->parse()
1903 );
1904 }
1905 $help['permissions'] .= Html::openElement( 'dl' );
1906 foreach ( self::$mRights as $right => $rightMsg ) {
1907 $help['permissions'] .= Html::element( 'dt', null, $right );
1908
1909 $rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
1910 $help['permissions'] .= Html::rawElement( 'dd', null, $rightMsg );
1911
1912 $groups = array_map( function ( $group ) {
1913 return $group == '*' ? 'all' : $group;
1914 }, User::getGroupsWithPermission( $right ) );
1915
1916 $help['permissions'] .= Html::rawElement( 'dd', null,
1917 $this->msg( 'api-help-permissions-granted-to' )
1918 ->numParams( count( $groups ) )
1919 ->params( Message::listParam( $groups ) )
1920 ->parse()
1921 );
1922 }
1923 $help['permissions'] .= Html::closeElement( 'dl' );
1924 $help['permissions'] .= Html::closeElement( 'div' );
1925
1926 // Fill 'datatypes' and 'credits', if applicable
1927 if ( empty( $options['nolead'] ) ) {
1928 $level = $options['headerlevel'];
1929 $tocnumber = &$options['tocnumber'];
1930
1931 $header = $this->msg( 'api-help-datatypes-header' )->parse();
1932
1933 $id = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_PRIMARY );
1934 $idFallback = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_FALLBACK );
1935 $headline = Linker::makeHeadline( min( 6, $level ),
1936 ' class="apihelp-header">',
1937 $id,
1938 $header,
1939 '',
1940 $idFallback
1941 );
1942 // Ensure we have a sane anchor
1943 if ( $id !== 'main/datatypes' && $idFallback !== 'main/datatypes' ) {
1944 $headline = '<div id="main/datatypes"></div>' . $headline;
1945 }
1946 $help['datatypes'] .= $headline;
1947 $help['datatypes'] .= $this->msg( 'api-help-datatypes' )->parseAsBlock();
1948 if ( !isset( $tocData['main/datatypes'] ) ) {
1949 $tocnumber[$level]++;
1950 $tocData['main/datatypes'] = [
1951 'toclevel' => count( $tocnumber ),
1952 'level' => $level,
1953 'anchor' => 'main/datatypes',
1954 'line' => $header,
1955 'number' => implode( '.', $tocnumber ),
1956 'index' => false,
1957 ];
1958 }
1959
1960 $header = $this->msg( 'api-credits-header' )->parse();
1961 $id = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_PRIMARY );
1962 $idFallback = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_FALLBACK );
1963 $headline = Linker::makeHeadline( min( 6, $level ),
1964 ' class="apihelp-header">',
1965 $id,
1966 $header,
1967 '',
1968 $idFallback
1969 );
1970 // Ensure we have a sane anchor
1971 if ( $id !== 'main/credits' && $idFallback !== 'main/credits' ) {
1972 $headline = '<div id="main/credits"></div>' . $headline;
1973 }
1974 $help['credits'] .= $headline;
1975 $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
1976 if ( !isset( $tocData['main/credits'] ) ) {
1977 $tocnumber[$level]++;
1978 $tocData['main/credits'] = [
1979 'toclevel' => count( $tocnumber ),
1980 'level' => $level,
1981 'anchor' => 'main/credits',
1982 'line' => $header,
1983 'number' => implode( '.', $tocnumber ),
1984 'index' => false,
1985 ];
1986 }
1987 }
1988 }
1989
1990 private $mCanApiHighLimits = null;
1991
1996 public function canApiHighLimits() {
1997 if ( !isset( $this->mCanApiHighLimits ) ) {
1998 $this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' );
1999 }
2000
2002 }
2003
2008 public function getModuleManager() {
2009 return $this->mModuleMgr;
2010 }
2011
2020 public function getUserAgent() {
2021 return trim(
2022 $this->getRequest()->getHeader( 'Api-user-agent' ) . ' ' .
2023 $this->getRequest()->getHeader( 'User-agent' )
2024 );
2025 }
2026}
2027
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,...
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
$messages
$wgUser
Definition Setup.php:902
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:773
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition ApiBase.php:1895
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:2049
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:428
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:1990
extractRequestParams( $parseLimit=true)
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:749
addDeprecation( $msg, $feature, $data=[])
Add a deprecation warning for this module.
Definition ApiBase.php:1833
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition ApiBase.php:240
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition ApiBase.php:1819
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:236
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.
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:43
getExamplesMessages()
@inheritDoc
Definition ApiMain.php:1873
isReadMode()
Definition ApiMain.php:1815
setRequestExpectations(ApiBase $module)
Set database connection, query, and write expectations given this module request.
Definition ApiMain.php:1600
getAllowedParams()
See ApiBase for description.
Definition ApiMain.php:1824
getSensitiveParams()
Get the request parameters that should be considered sensitive.
Definition ApiMain.php:1709
getPrinter()
Get the result formatter object.
Definition ApiMain.php:395
logRequest( $time, $e=null)
Log the preceding request.
Definition ApiMain.php:1619
static $Modules
List of available modules: action name => module class.
Definition ApiMain.php:57
sendCacheHeaders( $isError)
Send caching headers.
Definition ApiMain.php:871
encodeRequestLogValue( $s)
Encode a value in a format suitable for a space-separated log line.
Definition ApiMain.php:1675
ApiFormatBase $mPrinter
Definition ApiMain.php:146
getMaxLag()
Definition ApiMain.php:1238
markParamsUsed( $params)
Mark parameters as used.
Definition ApiMain.php:1700
executeActionWithErrorHandling()
Execute an action, and in case of an error, erase whatever partial results have been accumulated,...
Definition ApiMain.php:514
$mParamsSensitive
Definition ApiMain.php:160
createPrinterByName( $format)
Create an instance of an output formatter by its name.
Definition ApiMain.php:488
setCacheMaxAge( $maxage)
Set how long the response should be cached.
Definition ApiMain.php:404
static $mRights
List of user roles that are specifically relevant to the API.
Definition ApiMain.php:132
getResult()
Get the ApiResult object associated with current request.
Definition ApiMain.php:312
createErrorPrinter()
Create the printer for error output.
Definition ApiMain.php:989
executeAction()
Execute the actual module, without any error handling.
Definition ApiMain.php:1556
getErrorFormatter()
Get the ApiErrorFormatter object associated with current request.
Definition ApiMain.php:355
checkMaxLag( $module, $params)
Check the max lag if necessary.
Definition ApiMain.php:1270
checkAsserts( $params)
Check asserts of the user's rights.
Definition ApiMain.php:1495
$mErrorFormatter
Definition ApiMain.php:148
setupExternalResponse( $module, $params)
Check POST for external response and setup result printer.
Definition ApiMain.php:1526
static matchRequestedHeaders( $requestedHeaders)
Attempt to validate the value of Access-Control-Request-Headers against a list of headers that we all...
Definition ApiMain.php:819
bool null $lacksSameOriginSecurity
Cached return value from self::lacksSameOriginSecurity()
Definition ApiMain.php:163
setCacheControl( $directives)
Set directives (key/value pairs) for the Cache-Control header.
Definition ApiMain.php:477
static wildcardToRegex( $wildcard)
Helper function to convert wildcard string into a regex '*' => '.
Definition ApiMain.php:855
setContinuationManager(ApiContinuationManager $manager=null)
Set the continuation manager.
Definition ApiMain.php:371
setCacheMode( $mode)
Set the type of caching headers which will be sent.
Definition ApiMain.php:436
setupModule()
Set up the module for response.
Definition ApiMain.php:1191
markParamsSensitive( $params)
Mark parameters as sensitive.
Definition ApiMain.php:1718
ApiBase $mModule
Definition ApiMain.php:155
setupExecuteAction()
Set up for the execution.
Definition ApiMain.php:1176
checkConditionalRequestHeaders( $module)
Check selected RFC 7232 precondition headers.
Definition ApiMain.php:1316
checkBotReadOnly()
Check whether we are readonly for bots.
Definition ApiMain.php:1461
static handleApiBeforeMainException(Exception $e)
Handle an exception from the ApiBeforeMain hook.
Definition ApiMain.php:643
$mSquidMaxage
Definition ApiMain.php:153
getUserAgent()
Fetches the user agent used for this request.
Definition ApiMain.php:2020
const API_DEFAULT_USELANG
When no uselang parameter is given, this language will be used.
Definition ApiMain.php:52
static matchOrigin( $value, $rules, $exceptions)
Attempt to match an Origin header against a set of rules and a set of exceptions.
Definition ApiMain.php:795
getModule()
Get the API module object.
Definition ApiMain.php:386
__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:303
addRequestedFields( $force=[])
Add requested fields to the result.
Definition ApiMain.php:1145
modifyHelp(array &$help, array $options, array &$tocData)
Called from ApiHelp before the pieces are joined together and returned.
Definition ApiMain.php:1882
handleException(Exception $e)
Handle an exception as an API response.
Definition ApiMain.php:569
$mCacheControl
Definition ApiMain.php:158
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:1749
$mInternalMode
Definition ApiMain.php:153
getContinuationManager()
Get the continuation manager.
Definition ApiMain.php:363
printResult( $httpCode=0)
Print results using the current printer.
Definition ApiMain.php:1798
reportUnusedParams()
Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,...
Definition ApiMain.php:1770
lacksSameOriginSecurity()
Get the security flag for the current request.
Definition ApiMain.php:320
getVal( $name, $default=null)
Get a request value, and register the fact that it was used, for logging.
Definition ApiMain.php:1728
getParamsUsed()
Get the request parameters used in the course of the preceding execute() request.
Definition ApiMain.php:1692
getModuleManager()
Overrides to return this instance's module manager.
Definition ApiMain.php:2008
ApiContinuationManager null $mContinuationManager
Definition ApiMain.php:150
substituteResultWithError( $e)
Replace the result data with the information about an exception.
Definition ApiMain.php:1065
const API_DEFAULT_FORMAT
When no format parameter is given, this format will be used.
Definition ApiMain.php:47
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:1760
canApiHighLimits()
Check whether the current user is allowed to use high limits.
Definition ApiMain.php:1996
static $Formats
List of available formats: format name => format class.
Definition ApiMain.php:115
$mCanApiHighLimits
Definition ApiMain.php:1990
errorMessagesFromException( $e, $type='error')
Create an error message for the given exception.
Definition ApiMain.php:1023
handleCORS()
Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
Definition ApiMain.php:675
execute()
Execute api request.
Definition ApiMain.php:502
$mEnableWrite
Definition ApiMain.php:152
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
This class holds a list of modules and handles instantiation.
Extension of RawMessage implementing IApiMessage.
This class represents the result of the API operations.
Definition ApiResult.php:33
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:56
Exception used to abort API execution with an error.
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 singleton( $domain=false)
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition Linker.php:1642
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 sanitizeLangCode( $code)
Accepts a language code and ensures it's sane.
static getMain()
Get the RequestContext object associated with the main request.
This exception will be thrown when dieUsage is called to stop module execution.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:53
static isEveryoneAllowed( $right)
Check if all users may be assumed to have the given permission.
Definition User.php:5025
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:591
static getGroupsWithPermission( $role)
Get all the groups who have a given permission.
Definition User.php:4982
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Database error base class.
Definition DBError.php:30
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition design.txt:57
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as $wgLang
Definition design.txt:56
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:2806
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1795
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:2001
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub 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:865
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:2005
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub 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:864
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3021
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
this hook is for auditing only $response
Definition hooks.txt:783
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:2176
Interface for MediaWiki-localized exceptions.
$help
Definition mcc.php:32
A helper class for throttling authentication attempts.
$params
$header