MediaWiki  master
ApiMain.php
Go to the documentation of this file.
1 <?php
27 
41 class 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;
150  private $mEnableWrite;
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 ) {
173  } elseif ( $context instanceof WebRequest ) {
174  // BC for pre-1.19
175  $request = $context;
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  // Optimisation: Avoid slow getVal(), this isn't user-generated content.
242  $uselang = $request->getRawVal( 'uselang', self::API_DEFAULT_USELANG );
243  if ( $uselang === 'user' ) {
244  // Assume the parent context is going to return the user language
245  // for uselang=user (see T85635).
246  } else {
247  if ( $uselang === 'content' ) {
248  $uselang = MediaWikiServices::getInstance()->getContentLanguage()->getCode();
249  }
251  $this->getContext()->setLanguage( $code );
252  if ( !$this->mInternalMode ) {
253  global $wgLang;
254  $wgLang = $this->getContext()->getLanguage();
255  RequestContext::getMain()->setLanguage( $wgLang );
256  }
257  }
258 
259  // Set up the error formatter. This doesn't use $this->getParameter()
260  // because we're not ready to handle errors yet.
261  // Optimisation: Avoid slow getVal(), this isn't user-generated content.
262  $errorFormat = $request->getRawVal( 'errorformat', 'bc' );
263  $errorLangCode = $request->getRawVal( 'errorlang', 'uselang' );
264  $errorsUseDB = $request->getCheck( 'errorsuselocal' );
265  if ( in_array( $errorFormat, [ 'plaintext', 'wikitext', 'html', 'raw', 'none' ], true ) ) {
266  if ( $errorLangCode === 'uselang' ) {
267  $errorLang = $this->getLanguage();
268  } elseif ( $errorLangCode === 'content' ) {
269  $errorLang = MediaWikiServices::getInstance()->getContentLanguage();
270  } else {
271  $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode );
272  $errorLang = Language::factory( $errorLangCode );
273  }
274  $this->mErrorFormatter = new ApiErrorFormatter(
275  $this->mResult, $errorLang, $errorFormat, $errorsUseDB
276  );
277  } else {
278  $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
279  }
280  $this->mResult->setErrorFormatter( $this->getErrorFormatter() );
281 
282  $this->mModuleMgr = new ApiModuleManager( $this );
283  $this->mModuleMgr->addModules( self::$Modules, 'action' );
284  $this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
285  $this->mModuleMgr->addModules( self::$Formats, 'format' );
286  $this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' );
287 
288  Hooks::run( 'ApiMain::moduleManager', [ $this->mModuleMgr ] );
289 
290  $this->mContinuationManager = null;
291  $this->mEnableWrite = $enableWrite;
292 
293  $this->mCdnMaxAge = -1; // flag for executeActionWithErrorHandling()
294  $this->mCommit = false;
295  }
296 
301  public function isInternalMode() {
302  return $this->mInternalMode;
303  }
304 
310  public function getResult() {
311  return $this->mResult;
312  }
313 
318  public function lacksSameOriginSecurity() {
319  if ( $this->lacksSameOriginSecurity !== null ) {
321  }
322 
323  $request = $this->getRequest();
324 
325  // JSONP mode
326  if ( $request->getCheck( 'callback' ) ) {
327  $this->lacksSameOriginSecurity = true;
328  return true;
329  }
330 
331  // Anonymous CORS
332  if ( $request->getVal( 'origin' ) === '*' ) {
333  $this->lacksSameOriginSecurity = true;
334  return true;
335  }
336 
337  // Header to be used from XMLHTTPRequest when the request might
338  // otherwise be used for XSS.
339  if ( $request->getHeader( 'Treat-as-Untrusted' ) !== false ) {
340  $this->lacksSameOriginSecurity = true;
341  return true;
342  }
343 
344  // Allow extensions to override.
345  $this->lacksSameOriginSecurity = !Hooks::run( 'RequestHasSameOriginSecurity', [ $request ] );
347  }
348 
353  public function getErrorFormatter() {
354  return $this->mErrorFormatter;
355  }
356 
361  public function getContinuationManager() {
363  }
364 
369  public function setContinuationManager( ApiContinuationManager $manager = null ) {
370  if ( $manager !== null && $this->mContinuationManager !== null ) {
371  throw new UnexpectedValueException(
372  __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
373  ' when a manager is already set from ' . $this->mContinuationManager->getSource()
374  );
375  }
376  $this->mContinuationManager = $manager;
377  }
378 
384  public function getModule() {
385  return $this->mModule;
386  }
387 
393  public function getPrinter() {
394  return $this->mPrinter;
395  }
396 
402  public function setCacheMaxAge( $maxage ) {
403  $this->setCacheControl( [
404  'max-age' => $maxage,
405  's-maxage' => $maxage
406  ] );
407  }
408 
434  public function setCacheMode( $mode ) {
435  if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
436  wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" );
437 
438  // Ignore for forwards-compatibility
439  return;
440  }
441 
442  if ( !User::isEveryoneAllowed( 'read' ) ) {
443  // Private wiki, only private headers
444  if ( $mode !== 'private' ) {
445  wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
446 
447  return;
448  }
449  }
450 
451  if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
452  // User language is used for i18n, so we don't want to publicly
453  // cache. Anons are ok, because if they have non-default language
454  // then there's an appropriate Vary header set by whatever set
455  // their non-default language.
456  wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
457  "'anon-public-user-private' due to uselang=user\n" );
458  $mode = 'anon-public-user-private';
459  }
460 
461  wfDebug( __METHOD__ . ": setting cache mode $mode\n" );
462  $this->mCacheMode = $mode;
463  }
464 
475  public function setCacheControl( $directives ) {
476  $this->mCacheControl = $directives + $this->mCacheControl;
477  }
478 
486  public function createPrinterByName( $format ) {
487  $printer = $this->mModuleMgr->getModule( $format, 'format', /* $ignoreCache */ true );
488  if ( $printer === null ) {
489  $this->dieWithError(
490  [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
491  );
492  }
493 
494  return $printer;
495  }
496 
500  public function execute() {
501  if ( $this->mInternalMode ) {
502  $this->executeAction();
503  } else {
505  }
506  }
507 
512  protected function executeActionWithErrorHandling() {
513  // Verify the CORS header before executing the action
514  if ( !$this->handleCORS() ) {
515  // handleCORS() has sent a 403, abort
516  return;
517  }
518 
519  // Exit here if the request method was OPTIONS
520  // (assume there will be a followup GET or POST)
521  if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
522  return;
523  }
524 
525  // In case an error occurs during data output,
526  // clear the output buffer and print just the error information
527  $obLevel = ob_get_level();
528  ob_start();
529 
530  $t = microtime( true );
531  $isError = false;
532  try {
533  $this->executeAction();
534  $runTime = microtime( true ) - $t;
535  $this->logRequest( $runTime );
536  MediaWikiServices::getInstance()->getStatsdDataFactory()->timing(
537  'api.' . $this->mModule->getModuleName() . '.executeTiming', 1000 * $runTime
538  );
539  } catch ( Exception $e ) { // @todo Remove this block when HHVM is no longer supported
540  $this->handleException( $e );
541  $this->logRequest( microtime( true ) - $t, $e );
542  $isError = true;
543  } catch ( Throwable $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
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( $e ) {
570  // T65145: Rollback any open database transactions
571  if ( !$e instanceof ApiUsageException ) {
572  // ApiUsageExceptions are intentional, so don't rollback if that's the case
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  // Get desired HTTP code from an ApiUsageException. Don't use codes from other
599  // exception types, as they are unlikely to be intended as an HTTP code.
600  $httpCode = $e instanceof ApiUsageException ? $e->getCode() : 0;
601 
602  $failed = false;
603  try {
604  $this->printResult( $httpCode );
605  } catch ( ApiUsageException $ex ) {
606  // The error printer itself is failing. Try suppressing its request
607  // parameters and redo.
608  $failed = true;
609  $this->addWarning( 'apiwarn-errorprinterfailed' );
610  foreach ( $ex->getStatusValue()->getErrors() as $error ) {
611  try {
612  $this->mPrinter->addWarning( $error );
613  } catch ( Exception $ex2 ) { // @todo Remove this block when HHVM is no longer supported
614  // WTF?
615  $this->addWarning( $error );
616  } catch ( Throwable $ex2 ) {
617  // WTF?
618  $this->addWarning( $error );
619  }
620  }
621  }
622  if ( $failed ) {
623  $this->mPrinter = null;
624  $this->createErrorPrinter();
625  $this->mPrinter->forceDefaultParams();
626  if ( $httpCode ) {
627  $response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200
628  }
629  $this->printResult( $httpCode );
630  }
631  }
632 
643  public static function handleApiBeforeMainException( $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 ) { // @todo Remove this block when HHVM is no longer supported
651  // Nope, even that didn't work. Punt.
652  throw $e;
653  } catch ( Throwable $e2 ) {
654  // Nope, even that didn't work. Punt.
655  throw $e;
656  }
657 
658  // Reset cache headers
659  $main->sendCacheHeaders( true );
660 
661  ob_end_flush();
662  }
663 
678  protected function handleCORS() {
679  $originParam = $this->getParameter( 'origin' ); // defaults to null
680  if ( $originParam === null ) {
681  // No origin parameter, nothing to do
682  return true;
683  }
684 
685  $request = $this->getRequest();
686  $response = $request->response();
687 
688  $matchedOrigin = false;
689  $allowTiming = false;
690  $varyOrigin = true;
691 
692  if ( $originParam === '*' ) {
693  // Request for anonymous CORS
694  // Technically we should check for the presence of an Origin header
695  // and not process it as CORS if it's not set, but that would
696  // require us to vary on Origin for all 'origin=*' requests which
697  // we don't want to do.
698  $matchedOrigin = true;
699  $allowOrigin = '*';
700  $allowCredentials = 'false';
701  $varyOrigin = false; // No need to vary
702  } else {
703  // Non-anonymous CORS, check we allow the domain
704 
705  // Origin: header is a space-separated list of origins, check all of them
706  $originHeader = $request->getHeader( 'Origin' );
707  if ( $originHeader === false ) {
708  $origins = [];
709  } else {
710  $originHeader = trim( $originHeader );
711  $origins = preg_split( '/\s+/', $originHeader );
712  }
713 
714  if ( !in_array( $originParam, $origins ) ) {
715  // origin parameter set but incorrect
716  // Send a 403 response
717  $response->statusHeader( 403 );
718  $response->header( 'Cache-Control: no-cache' );
719  echo "'origin' parameter does not match Origin header\n";
720 
721  return false;
722  }
723 
724  $config = $this->getConfig();
725  $matchedOrigin = count( $origins ) === 1 && self::matchOrigin(
726  $originParam,
727  $config->get( 'CrossSiteAJAXdomains' ),
728  $config->get( 'CrossSiteAJAXdomainExceptions' )
729  );
730 
731  $allowOrigin = $originHeader;
732  $allowCredentials = 'true';
733  $allowTiming = $originHeader;
734  }
735 
736  if ( $matchedOrigin ) {
737  $requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
738  $preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
739  if ( $preflight ) {
740  // This is a CORS preflight request
741  if ( $requestedMethod !== 'POST' && $requestedMethod !== 'GET' ) {
742  // If method is not a case-sensitive match, do not set any additional headers and terminate.
743  $response->header( 'MediaWiki-CORS-Rejection: Unsupported method requested in preflight' );
744  return true;
745  }
746  // We allow the actual request to send the following headers
747  $requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
748  if ( $requestedHeaders !== false ) {
749  if ( !self::matchRequestedHeaders( $requestedHeaders ) ) {
750  $response->header( 'MediaWiki-CORS-Rejection: Unsupported header requested in preflight' );
751  return true;
752  }
753  $response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
754  }
755 
756  // We only allow the actual request to be GET or POST
757  $response->header( 'Access-Control-Allow-Methods: POST, GET' );
758  } elseif ( $request->getMethod() !== 'POST' && $request->getMethod() !== 'GET' ) {
759  // Unsupported non-preflight method, don't handle it as CORS
760  $response->header(
761  'MediaWiki-CORS-Rejection: Unsupported method for simple request or actual request'
762  );
763  return true;
764  }
765 
766  $response->header( "Access-Control-Allow-Origin: $allowOrigin" );
767  $response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
768  // https://www.w3.org/TR/resource-timing/#timing-allow-origin
769  if ( $allowTiming !== false ) {
770  $response->header( "Timing-Allow-Origin: $allowTiming" );
771  }
772 
773  if ( !$preflight ) {
774  $response->header(
775  'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag, '
776  . 'MediaWiki-Login-Suppressed'
777  );
778  }
779  } else {
780  $response->header( 'MediaWiki-CORS-Rejection: Origin mismatch' );
781  }
782 
783  if ( $varyOrigin ) {
784  $this->getOutput()->addVaryHeader( 'Origin' );
785  }
786 
787  return true;
788  }
789 
798  protected static function matchOrigin( $value, $rules, $exceptions ) {
799  foreach ( $rules as $rule ) {
800  if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
801  // Rule matches, check exceptions
802  foreach ( $exceptions as $exc ) {
803  if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
804  return false;
805  }
806  }
807 
808  return true;
809  }
810  }
811 
812  return false;
813  }
814 
822  protected static function matchRequestedHeaders( $requestedHeaders ) {
823  if ( trim( $requestedHeaders ) === '' ) {
824  return true;
825  }
826  $requestedHeaders = explode( ',', $requestedHeaders );
827  $allowedAuthorHeaders = array_flip( [
828  /* simple headers (see spec) */
829  'accept',
830  'accept-language',
831  'content-language',
832  'content-type',
833  /* non-authorable headers in XHR, which are however requested by some UAs */
834  'accept-encoding',
835  'dnt',
836  'origin',
837  /* MediaWiki whitelist */
838  'user-agent',
839  'api-user-agent',
840  ] );
841  foreach ( $requestedHeaders as $rHeader ) {
842  $rHeader = strtolower( trim( $rHeader ) );
843  if ( !isset( $allowedAuthorHeaders[$rHeader] ) ) {
844  LoggerFactory::getInstance( 'api-warning' )->warning(
845  'CORS preflight failed on requested header: {header}', [
846  'header' => $rHeader
847  ]
848  );
849  return false;
850  }
851  }
852  return true;
853  }
854 
863  protected static function wildcardToRegex( $wildcard ) {
864  $wildcard = preg_quote( $wildcard, '/' );
865  $wildcard = str_replace(
866  [ '\*', '\?' ],
867  [ '.*?', '.' ],
868  $wildcard
869  );
870 
871  return "/^https?:\/\/$wildcard$/";
872  }
873 
879  protected function sendCacheHeaders( $isError ) {
880  $response = $this->getRequest()->response();
881  $out = $this->getOutput();
882 
883  $out->addVaryHeader( 'Treat-as-Untrusted' );
884 
885  $config = $this->getConfig();
886 
887  if ( $config->get( 'VaryOnXFP' ) ) {
888  $out->addVaryHeader( 'X-Forwarded-Proto' );
889  }
890 
891  if ( !$isError && $this->mModule &&
892  ( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
893  ) {
894  $etag = $this->mModule->getConditionalRequestData( 'etag' );
895  if ( $etag !== null ) {
896  $response->header( "ETag: $etag" );
897  }
898  $lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
899  if ( $lastMod !== null ) {
900  $response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
901  }
902  }
903 
904  // The logic should be:
905  // $this->mCacheControl['max-age'] is set?
906  // Use it, the module knows better than our guess.
907  // !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
908  // Use 0 because we can guess caching is probably the wrong thing to do.
909  // Use $this->getParameter( 'maxage' ), which already defaults to 0.
910  $maxage = 0;
911  if ( isset( $this->mCacheControl['max-age'] ) ) {
912  $maxage = $this->mCacheControl['max-age'];
913  } elseif ( ( $this->mModule && !$this->mModule->isWriteMode() ) ||
914  $this->mCacheMode !== 'private'
915  ) {
916  $maxage = $this->getParameter( 'maxage' );
917  }
918  $privateCache = 'private, must-revalidate, max-age=' . $maxage;
919 
920  if ( $this->mCacheMode == 'private' ) {
921  $response->header( "Cache-Control: $privateCache" );
922  return;
923  }
924 
925  if ( $this->mCacheMode == 'anon-public-user-private' ) {
926  $out->addVaryHeader( 'Cookie' );
927  $response->header( $out->getVaryHeader() );
928  if ( MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
929  // Logged in or otherwise has session (e.g. anonymous users who have edited)
930  // Mark request private
931  $response->header( "Cache-Control: $privateCache" );
932 
933  return;
934  } // else anonymous, send public headers below
935  }
936 
937  // Send public headers
938  $response->header( $out->getVaryHeader() );
939 
940  // If nobody called setCacheMaxAge(), use the (s)maxage parameters
941  if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
942  $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
943  }
944  if ( !isset( $this->mCacheControl['max-age'] ) ) {
945  $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
946  }
947 
948  if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
949  // Public cache not requested
950  // Sending a Vary header in this case is harmless, and protects us
951  // against conditional calls of setCacheMaxAge().
952  $response->header( "Cache-Control: $privateCache" );
953 
954  return;
955  }
956 
957  $this->mCacheControl['public'] = true;
958 
959  // Send an Expires header
960  $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
961  $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
962  $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
963 
964  // Construct the Cache-Control header
965  $ccHeader = '';
966  $separator = '';
967  foreach ( $this->mCacheControl as $name => $value ) {
968  if ( is_bool( $value ) ) {
969  if ( $value ) {
970  $ccHeader .= $separator . $name;
971  $separator = ', ';
972  }
973  } else {
974  $ccHeader .= $separator . "$name=$value";
975  $separator = ', ';
976  }
977  }
978 
979  $response->header( "Cache-Control: $ccHeader" );
980  }
981 
985  private function createErrorPrinter() {
986  if ( !isset( $this->mPrinter ) ) {
987  $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
988  if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
989  $value = self::API_DEFAULT_FORMAT;
990  }
991  $this->mPrinter = $this->createPrinterByName( $value );
992  }
993 
994  // Printer may not be able to handle errors. This is particularly
995  // likely if the module returns something for getCustomPrinter().
996  if ( !$this->mPrinter->canPrintErrors() ) {
997  $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
998  }
999  }
1000 
1016  protected function errorMessagesFromException( $e, $type = 'error' ) {
1017  $messages = [];
1018  if ( $e instanceof ApiUsageException ) {
1019  foreach ( $e->getStatusValue()->getErrorsByType( $type ) as $error ) {
1020  $messages[] = ApiMessage::create( $error );
1021  }
1022  } elseif ( $type !== 'error' ) {
1023  // None of the rest have any messages for non-error types
1024  } else {
1025  // Something is seriously wrong
1026  $config = $this->getConfig();
1027  // TODO: Avoid embedding arbitrary class names in the error code.
1028  $class = preg_replace( '#^Wikimedia\\\Rdbms\\\#', '', get_class( $e ) );
1029  $code = 'internal_api_error_' . $class;
1030  $data = [ 'errorclass' => get_class( $e ) ];
1031  if ( $config->get( 'ShowExceptionDetails' ) ) {
1032  if ( $e instanceof ILocalizedException ) {
1033  $msg = $e->getMessageObject();
1034  } elseif ( $e instanceof MessageSpecifier ) {
1035  $msg = Message::newFromSpecifier( $e );
1036  } else {
1037  $msg = wfEscapeWikiText( $e->getMessage() );
1038  }
1039  $params = [ 'apierror-exceptioncaught', WebRequest::getRequestId(), $msg ];
1040  } else {
1041  $params = [ 'apierror-exceptioncaughttype', WebRequest::getRequestId(), get_class( $e ) ];
1042  }
1043 
1044  $messages[] = ApiMessage::create( $params, $code, $data );
1045  }
1046  return $messages;
1047  }
1048 
1054  protected function substituteResultWithError( $e ) {
1055  $result = $this->getResult();
1056  $formatter = $this->getErrorFormatter();
1057  $config = $this->getConfig();
1058  $errorCodes = [];
1059 
1060  // Remember existing warnings and errors across the reset
1061  $errors = $result->getResultData( [ 'errors' ] );
1062  $warnings = $result->getResultData( [ 'warnings' ] );
1063  $result->reset();
1064  if ( $warnings !== null ) {
1065  $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
1066  }
1067  if ( $errors !== null ) {
1068  $result->addValue( null, 'errors', $errors, ApiResult::NO_SIZE_CHECK );
1069 
1070  // Collect the copied error codes for the return value
1071  foreach ( $errors as $error ) {
1072  if ( isset( $error['code'] ) ) {
1073  $errorCodes[$error['code']] = true;
1074  }
1075  }
1076  }
1077 
1078  // Add errors from the exception
1079  $modulePath = $e instanceof ApiUsageException ? $e->getModulePath() : null;
1080  foreach ( $this->errorMessagesFromException( $e, 'error' ) as $msg ) {
1081  if ( ApiErrorFormatter::isValidApiCode( $msg->getApiCode() ) ) {
1082  $errorCodes[$msg->getApiCode()] = true;
1083  } else {
1084  LoggerFactory::getInstance( 'api-warning' )->error( 'Invalid API error code "{code}"', [
1085  'code' => $msg->getApiCode(),
1086  'exception' => $e,
1087  ] );
1088  $errorCodes['<invalid-code>'] = true;
1089  }
1090  $formatter->addError( $modulePath, $msg );
1091  }
1092  foreach ( $this->errorMessagesFromException( $e, 'warning' ) as $msg ) {
1093  $formatter->addWarning( $modulePath, $msg );
1094  }
1095 
1096  // Add additional data. Path depends on whether we're in BC mode or not.
1097  // Data depends on the type of exception.
1098  if ( $formatter instanceof ApiErrorFormatter_BackCompat ) {
1099  $path = [ 'error' ];
1100  } else {
1101  $path = null;
1102  }
1103  if ( $e instanceof ApiUsageException ) {
1104  $link = wfExpandUrl( wfScript( 'api' ) );
1105  $result->addContentValue(
1106  $path,
1107  'docref',
1108  trim(
1109  $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text()
1110  . ' '
1111  . $this->msg( 'api-usage-mailinglist-ref' )->inLanguage( $formatter->getLanguage() )->text()
1112  )
1113  );
1114  } elseif ( $config->get( 'ShowExceptionDetails' ) ) {
1115  $result->addContentValue(
1116  $path,
1117  'trace',
1118  $this->msg( 'api-exception-trace',
1119  get_class( $e ),
1120  $e->getFile(),
1121  $e->getLine(),
1123  )->inLanguage( $formatter->getLanguage() )->text()
1124  );
1125  }
1126 
1127  // Add the id and such
1128  $this->addRequestedFields( [ 'servedby' ] );
1129 
1130  return array_keys( $errorCodes );
1131  }
1132 
1138  protected function addRequestedFields( $force = [] ) {
1139  $result = $this->getResult();
1140 
1141  $requestid = $this->getParameter( 'requestid' );
1142  if ( $requestid !== null ) {
1143  $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
1144  }
1145 
1146  if ( $this->getConfig()->get( 'ShowHostnames' ) && (
1147  in_array( 'servedby', $force, true ) || $this->getParameter( 'servedby' )
1148  ) ) {
1149  $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
1150  }
1151 
1152  if ( $this->getParameter( 'curtimestamp' ) ) {
1153  $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601, time() ),
1155  }
1156 
1157  if ( $this->getParameter( 'responselanginfo' ) ) {
1158  $result->addValue( null, 'uselang', $this->getLanguage()->getCode(),
1160  $result->addValue( null, 'errorlang', $this->getErrorFormatter()->getLanguage()->getCode(),
1162  }
1163  }
1164 
1169  protected function setupExecuteAction() {
1170  $this->addRequestedFields();
1171 
1172  $params = $this->extractRequestParams();
1173  $this->mAction = $params['action'];
1174 
1175  return $params;
1176  }
1177 
1184  protected function setupModule() {
1185  // Instantiate the module requested by the user
1186  $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
1187  if ( $module === null ) {
1188  // Probably can't happen
1189  // @codeCoverageIgnoreStart
1190  $this->dieWithError(
1191  [ 'apierror-unknownaction', wfEscapeWikiText( $this->mAction ) ], 'unknown_action'
1192  );
1193  // @codeCoverageIgnoreEnd
1194  }
1195  $moduleParams = $module->extractRequestParams();
1196 
1197  // Check token, if necessary
1198  if ( $module->needsToken() === true ) {
1199  throw new MWException(
1200  "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
1201  'See documentation for ApiBase::needsToken for details.'
1202  );
1203  }
1204  if ( $module->needsToken() ) {
1205  if ( !$module->mustBePosted() ) {
1206  throw new MWException(
1207  "Module '{$module->getModuleName()}' must require POST to use tokens."
1208  );
1209  }
1210 
1211  if ( !isset( $moduleParams['token'] ) ) {
1212  // Probably can't happen
1213  // @codeCoverageIgnoreStart
1214  $module->dieWithError( [ 'apierror-missingparam', 'token' ] );
1215  // @codeCoverageIgnoreEnd
1216  }
1217 
1218  $module->requirePostedParameters( [ 'token' ] );
1219 
1220  if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
1221  $module->dieWithError( 'apierror-badtoken' );
1222  }
1223  }
1224 
1225  return $module;
1226  }
1227 
1231  private function getMaxLag() {
1232  $dbLag = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaxLag();
1233  $lagInfo = [
1234  'host' => $dbLag[0],
1235  'lag' => $dbLag[1],
1236  'type' => 'db'
1237  ];
1238 
1239  $jobQueueLagFactor = $this->getConfig()->get( 'JobQueueIncludeInMaxLagFactor' );
1240  if ( $jobQueueLagFactor ) {
1241  // Turn total number of jobs into seconds by using the configured value
1242  $totalJobs = array_sum( JobQueueGroup::singleton()->getQueueSizes() );
1243  $jobQueueLag = $totalJobs / (float)$jobQueueLagFactor;
1244  if ( $jobQueueLag > $lagInfo['lag'] ) {
1245  $lagInfo = [
1246  'host' => wfHostname(), // XXX: Is there a better value that could be used?
1247  'lag' => $jobQueueLag,
1248  'type' => 'jobqueue',
1249  'jobs' => $totalJobs,
1250  ];
1251  }
1252  }
1253 
1254  Hooks::runWithoutAbort( 'ApiMaxLagInfo', [ &$lagInfo ] );
1255 
1256  return $lagInfo;
1257  }
1258 
1265  protected function checkMaxLag( $module, $params ) {
1266  if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
1267  $maxLag = $params['maxlag'];
1268  $lagInfo = $this->getMaxLag();
1269  if ( $lagInfo['lag'] > $maxLag ) {
1270  $response = $this->getRequest()->response();
1271 
1272  $response->header( 'Retry-After: ' . max( (int)$maxLag, 5 ) );
1273  $response->header( 'X-Database-Lag: ' . (int)$lagInfo['lag'] );
1274 
1275  if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1276  $this->dieWithError(
1277  [ 'apierror-maxlag', $lagInfo['lag'], $lagInfo['host'] ],
1278  'maxlag',
1279  $lagInfo
1280  );
1281  }
1282 
1283  $this->dieWithError( [ 'apierror-maxlag-generic', $lagInfo['lag'] ], 'maxlag', $lagInfo );
1284  }
1285  }
1286 
1287  return true;
1288  }
1289 
1311  protected function checkConditionalRequestHeaders( $module ) {
1312  if ( $this->mInternalMode ) {
1313  // No headers to check in internal mode
1314  return true;
1315  }
1316 
1317  if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) {
1318  // Don't check POSTs
1319  return true;
1320  }
1321 
1322  $return304 = false;
1323 
1324  $ifNoneMatch = array_diff(
1325  $this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: [],
1326  [ '' ]
1327  );
1328  if ( $ifNoneMatch ) {
1329  if ( $ifNoneMatch === [ '*' ] ) {
1330  // API responses always "exist"
1331  $etag = '*';
1332  } else {
1333  $etag = $module->getConditionalRequestData( 'etag' );
1334  }
1335  }
1336  if ( $ifNoneMatch && $etag !== null ) {
1337  $test = substr( $etag, 0, 2 ) === 'W/' ? substr( $etag, 2 ) : $etag;
1338  $match = array_map( function ( $s ) {
1339  return substr( $s, 0, 2 ) === 'W/' ? substr( $s, 2 ) : $s;
1340  }, $ifNoneMatch );
1341  $return304 = in_array( $test, $match, true );
1342  } else {
1343  $value = trim( $this->getRequest()->getHeader( 'If-Modified-Since' ) );
1344 
1345  // Some old browsers sends sizes after the date, like this:
1346  // Wed, 20 Aug 2003 06:51:19 GMT; length=5202
1347  // Ignore that.
1348  $i = strpos( $value, ';' );
1349  if ( $i !== false ) {
1350  $value = trim( substr( $value, 0, $i ) );
1351  }
1352 
1353  if ( $value !== '' ) {
1354  try {
1355  $ts = new MWTimestamp( $value );
1356  if (
1357  // RFC 7231 IMF-fixdate
1358  $ts->getTimestamp( TS_RFC2822 ) === $value ||
1359  // RFC 850
1360  $ts->format( 'l, d-M-y H:i:s' ) . ' GMT' === $value ||
1361  // asctime (with and without space-padded day)
1362  $ts->format( 'D M j H:i:s Y' ) === $value ||
1363  $ts->format( 'D M j H:i:s Y' ) === $value
1364  ) {
1365  $config = $this->getConfig();
1366  $lastMod = $module->getConditionalRequestData( 'last-modified' );
1367  if ( $lastMod !== null ) {
1368  // Mix in some MediaWiki modification times
1369  $modifiedTimes = [
1370  'page' => $lastMod,
1371  'user' => $this->getUser()->getTouched(),
1372  'epoch' => $config->get( 'CacheEpoch' ),
1373  ];
1374 
1375  if ( $config->get( 'UseCdn' ) ) {
1376  // T46570: the core page itself may not change, but resources might
1377  $modifiedTimes['sepoch'] = wfTimestamp(
1378  TS_MW, time() - $config->get( 'CdnMaxAge' )
1379  );
1380  }
1381  Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this->getOutput() ] );
1382  $lastMod = max( $modifiedTimes );
1383  $return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
1384  }
1385  }
1386  } catch ( TimestampException $e ) {
1387  // Invalid timestamp, ignore it
1388  }
1389  }
1390  }
1391 
1392  if ( $return304 ) {
1393  $this->getRequest()->response()->statusHeader( 304 );
1394 
1395  // Avoid outputting the compressed representation of a zero-length body
1396  Wikimedia\suppressWarnings();
1397  ini_set( 'zlib.output_compression', 0 );
1398  Wikimedia\restoreWarnings();
1400 
1401  return false;
1402  }
1403 
1404  return true;
1405  }
1406 
1411  protected function checkExecutePermissions( $module ) {
1412  $user = $this->getUser();
1413  if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) &&
1414  !$user->isAllowed( 'read' )
1415  ) {
1416  $this->dieWithError( 'apierror-readapidenied' );
1417  }
1418 
1419  if ( $module->isWriteMode() ) {
1420  if ( !$this->mEnableWrite ) {
1421  $this->dieWithError( 'apierror-noapiwrite' );
1422  } elseif ( !$user->isAllowed( 'writeapi' ) ) {
1423  $this->dieWithError( 'apierror-writeapidenied' );
1424  } elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) {
1425  $this->dieWithError( 'apierror-promised-nonwrite-api' );
1426  }
1427 
1428  $this->checkReadOnly( $module );
1429  }
1430 
1431  // Allow extensions to stop execution for arbitrary reasons.
1432  $message = 'hookaborted';
1433  if ( !Hooks::run( 'ApiCheckCanExecute', [ $module, $user, &$message ] ) ) {
1434  $this->dieWithError( $message );
1435  }
1436  }
1437 
1442  protected function checkReadOnly( $module ) {
1443  if ( wfReadOnly() ) {
1444  $this->dieReadOnly();
1445  }
1446 
1447  if ( $module->isWriteMode()
1448  && $this->getUser()->isBot()
1449  && MediaWikiServices::getInstance()->getDBLoadBalancer()->getServerCount() > 1
1450  ) {
1451  $this->checkBotReadOnly();
1452  }
1453  }
1454 
1458  private function checkBotReadOnly() {
1459  // Figure out how many servers have passed the lag threshold
1460  $numLagged = 0;
1461  $lagLimit = $this->getConfig()->get( 'APIMaxLagThreshold' );
1462  $laggedServers = [];
1463  $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
1464  foreach ( $loadBalancer->getLagTimes() as $serverIndex => $lag ) {
1465  if ( $lag > $lagLimit ) {
1466  ++$numLagged;
1467  $laggedServers[] = $loadBalancer->getServerName( $serverIndex ) . " ({$lag}s)";
1468  }
1469  }
1470 
1471  // If a majority of replica DBs are too lagged then disallow writes
1472  $replicaCount = $loadBalancer->getServerCount() - 1;
1473  if ( $numLagged >= ceil( $replicaCount / 2 ) ) {
1474  $laggedServers = implode( ', ', $laggedServers );
1475  wfDebugLog(
1476  'api-readonly', // Deprecate this channel in favor of api-warning?
1477  "Api request failed as read only because the following DBs are lagged: $laggedServers"
1478  );
1479  LoggerFactory::getInstance( 'api-warning' )->warning(
1480  "Api request failed as read only because the following DBs are lagged: {laggeddbs}", [
1481  'laggeddbs' => $laggedServers,
1482  ]
1483  );
1484 
1485  $this->dieWithError(
1486  'readonly_lag',
1487  'readonly',
1488  [ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ]
1489  );
1490  }
1491  }
1492 
1497  protected function checkAsserts( $params ) {
1498  if ( isset( $params['assert'] ) ) {
1499  $user = $this->getUser();
1500  switch ( $params['assert'] ) {
1501  case 'user':
1502  if ( $user->isAnon() ) {
1503  $this->dieWithError( 'apierror-assertuserfailed' );
1504  }
1505  break;
1506  case 'bot':
1507  if ( !$user->isAllowed( 'bot' ) ) {
1508  $this->dieWithError( 'apierror-assertbotfailed' );
1509  }
1510  break;
1511  }
1512  }
1513  if ( isset( $params['assertuser'] ) ) {
1514  $assertUser = User::newFromName( $params['assertuser'], false );
1515  if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) {
1516  $this->dieWithError(
1517  [ 'apierror-assertnameduserfailed', wfEscapeWikiText( $params['assertuser'] ) ]
1518  );
1519  }
1520  }
1521  }
1522 
1528  protected function setupExternalResponse( $module, $params ) {
1529  $validMethods = [ 'GET', 'HEAD', 'POST', 'OPTIONS' ];
1530  $request = $this->getRequest();
1531 
1532  if ( !in_array( $request->getMethod(), $validMethods ) ) {
1533  $this->dieWithError( 'apierror-invalidmethod', null, null, 405 );
1534  }
1535 
1536  if ( !$request->wasPosted() && $module->mustBePosted() ) {
1537  // Module requires POST. GET request might still be allowed
1538  // if $wgDebugApi is true, otherwise fail.
1539  $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] );
1540  }
1541 
1542  // See if custom printer is used
1543  $this->mPrinter = $module->getCustomPrinter();
1544  if ( is_null( $this->mPrinter ) ) {
1545  // Create an appropriate printer
1546  $this->mPrinter = $this->createPrinterByName( $params['format'] );
1547  }
1548 
1549  if ( $request->getProtocol() === 'http' && (
1550  $request->getSession()->shouldForceHTTPS() ||
1551  ( $this->getUser()->isLoggedIn() &&
1552  $this->getUser()->requiresHTTPS() )
1553  ) ) {
1554  $this->addDeprecation( 'apiwarn-deprecation-httpsexpected', 'https-expected' );
1555  }
1556  }
1557 
1561  protected function executeAction() {
1562  $params = $this->setupExecuteAction();
1563 
1564  // Check asserts early so e.g. errors in parsing a module's parameters due to being
1565  // logged out don't override the client's intended "am I logged in?" check.
1566  $this->checkAsserts( $params );
1567 
1568  $module = $this->setupModule();
1569  $this->mModule = $module;
1570 
1571  if ( !$this->mInternalMode ) {
1572  $this->setRequestExpectations( $module );
1573  }
1574 
1575  $this->checkExecutePermissions( $module );
1576 
1577  if ( !$this->checkMaxLag( $module, $params ) ) {
1578  return;
1579  }
1580 
1581  if ( !$this->checkConditionalRequestHeaders( $module ) ) {
1582  return;
1583  }
1584 
1585  if ( !$this->mInternalMode ) {
1586  $this->setupExternalResponse( $module, $params );
1587  }
1588 
1589  $module->execute();
1590  Hooks::run( 'APIAfterExecute', [ &$module ] );
1591 
1592  $this->reportUnusedParams();
1593 
1594  if ( !$this->mInternalMode ) {
1596 
1597  $this->printResult();
1598  }
1599  }
1600 
1605  protected function setRequestExpectations( ApiBase $module ) {
1606  $limits = $this->getConfig()->get( 'TrxProfilerLimits' );
1607  $trxProfiler = Profiler::instance()->getTransactionProfiler();
1608  $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
1609  if ( $this->getRequest()->hasSafeMethod() ) {
1610  $trxProfiler->setExpectations( $limits['GET'], __METHOD__ );
1611  } elseif ( $this->getRequest()->wasPosted() && !$module->isWriteMode() ) {
1612  $trxProfiler->setExpectations( $limits['POST-nonwrite'], __METHOD__ );
1613  $this->getRequest()->markAsSafeRequest();
1614  } else {
1615  $trxProfiler->setExpectations( $limits['POST'], __METHOD__ );
1616  }
1617  }
1618 
1624  protected function logRequest( $time, $e = null ) {
1625  $request = $this->getRequest();
1626 
1627  $logCtx = [
1628  // https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/event-schemas/+/master/jsonschema/mediawiki/api/request
1629  '$schema' => '/mediawiki/api/request/0.0.1',
1630  'meta' => [
1631  'request_id' => WebRequest::getRequestId(),
1632  'id' => UIDGenerator::newUUIDv4(),
1633  'dt' => wfTimestamp( TS_ISO_8601 ),
1634  'domain' => $this->getConfig()->get( 'ServerName' ),
1635  // If using the EventBus extension (as intended) with this log channel,
1636  // this stream name will map to a Kafka topic.
1637  'stream' => 'mediawiki.api-request'
1638  ],
1639  'http' => [
1640  'method' => $request->getMethod(),
1641  'client_ip' => $request->getIP()
1642  ],
1643  'database' => WikiMap::getCurrentWikiDbDomain()->getId(),
1644  'backend_time_ms' => (int)round( $time * 1000 ),
1645  ];
1646 
1647  // If set, these headers will be logged in http.request_headers.
1648  $httpRequestHeadersToLog = [ 'accept-language', 'referer', 'user-agent' ];
1649  foreach ( $httpRequestHeadersToLog as $header ) {
1650  if ( $request->getHeader( $header ) ) {
1651  // Set the header in http.request_headers
1652  $logCtx['http']['request_headers'][$header] = $request->getHeader( $header );
1653  }
1654  }
1655 
1656  if ( $e ) {
1657  $logCtx['api_error_codes'] = [];
1658  foreach ( $this->errorMessagesFromException( $e ) as $msg ) {
1659  $logCtx['api_error_codes'][] = $msg->getApiCode();
1660  }
1661  }
1662 
1663  // Construct space separated message for 'api' log channel
1664  $msg = "API {$request->getMethod()} " .
1665  wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
1666  " {$logCtx['http']['client_ip']} " .
1667  "T={$logCtx['backend_time_ms']}ms";
1668 
1669  $sensitive = array_flip( $this->getSensitiveParams() );
1670  foreach ( $this->getParamsUsed() as $name ) {
1671  $value = $request->getVal( $name );
1672  if ( $value === null ) {
1673  continue;
1674  }
1675 
1676  if ( isset( $sensitive[$name] ) ) {
1677  $value = '[redacted]';
1678  $encValue = '[redacted]';
1679  } elseif ( strlen( $value ) > 256 ) {
1680  $value = substr( $value, 0, 256 );
1681  $encValue = $this->encodeRequestLogValue( $value ) . '[...]';
1682  } else {
1683  $encValue = $this->encodeRequestLogValue( $value );
1684  }
1685 
1686  $logCtx['params'][$name] = $value;
1687  $msg .= " {$name}={$encValue}";
1688  }
1689 
1690  // Log an unstructured message to the api channel.
1691  wfDebugLog( 'api', $msg, 'private' );
1692 
1693  // The api-request channel a structured data log channel.
1694  wfDebugLog( 'api-request', '', 'private', $logCtx );
1695  }
1696 
1702  protected function encodeRequestLogValue( $s ) {
1703  static $table;
1704  if ( !$table ) {
1705  $chars = ';@$!*(),/:';
1706  $numChars = strlen( $chars );
1707  for ( $i = 0; $i < $numChars; $i++ ) {
1708  $table[rawurlencode( $chars[$i] )] = $chars[$i];
1709  }
1710  }
1711 
1712  return strtr( rawurlencode( $s ), $table );
1713  }
1714 
1719  protected function getParamsUsed() {
1720  return array_keys( $this->mParamsUsed );
1721  }
1722 
1727  public function markParamsUsed( $params ) {
1728  $this->mParamsUsed += array_fill_keys( (array)$params, true );
1729  }
1730 
1736  protected function getSensitiveParams() {
1737  return array_keys( $this->mParamsSensitive );
1738  }
1739 
1745  public function markParamsSensitive( $params ) {
1746  $this->mParamsSensitive += array_fill_keys( (array)$params, true );
1747  }
1748 
1755  public function getVal( $name, $default = null ) {
1756  $this->mParamsUsed[$name] = true;
1757 
1758  $ret = $this->getRequest()->getVal( $name );
1759  if ( $ret === null ) {
1760  if ( $this->getRequest()->getArray( $name ) !== null ) {
1761  // See T12262 for why we don't just implode( '|', ... ) the
1762  // array.
1763  $this->addWarning( [ 'apiwarn-unsupportedarray', $name ] );
1764  }
1765  $ret = $default;
1766  }
1767  return $ret;
1768  }
1769 
1776  public function getCheck( $name ) {
1777  return $this->getVal( $name, null ) !== null;
1778  }
1779 
1787  public function getUpload( $name ) {
1788  $this->mParamsUsed[$name] = true;
1789 
1790  return $this->getRequest()->getUpload( $name );
1791  }
1792 
1797  protected function reportUnusedParams() {
1798  $paramsUsed = $this->getParamsUsed();
1799  $allParams = $this->getRequest()->getValueNames();
1800 
1801  if ( !$this->mInternalMode ) {
1802  // Printer has not yet executed; don't warn that its parameters are unused
1803  $printerParams = $this->mPrinter->encodeParamName(
1804  array_keys( $this->mPrinter->getFinalParams() ?: [] )
1805  );
1806  $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
1807  } else {
1808  $unusedParams = array_diff( $allParams, $paramsUsed );
1809  }
1810 
1811  if ( count( $unusedParams ) ) {
1812  $this->addWarning( [
1813  'apierror-unrecognizedparams',
1814  Message::listParam( array_map( 'wfEscapeWikiText', $unusedParams ), 'comma' ),
1815  count( $unusedParams )
1816  ] );
1817  }
1818  }
1819 
1825  protected function printResult( $httpCode = 0 ) {
1826  if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
1827  $this->addWarning( 'apiwarn-wgdebugapi' );
1828  }
1829 
1830  $printer = $this->mPrinter;
1831  $printer->initPrinter( false );
1832  if ( $httpCode ) {
1833  $printer->setHttpStatus( $httpCode );
1834  }
1835  $printer->execute();
1836  $printer->closePrinter();
1837  }
1838 
1842  public function isReadMode() {
1843  return false;
1844  }
1845 
1851  public function getAllowedParams() {
1852  return [
1853  'action' => [
1854  ApiBase::PARAM_DFLT => 'help',
1855  ApiBase::PARAM_TYPE => 'submodule',
1856  ],
1857  'format' => [
1858  ApiBase::PARAM_DFLT => self::API_DEFAULT_FORMAT,
1859  ApiBase::PARAM_TYPE => 'submodule',
1860  ],
1861  'maxlag' => [
1862  ApiBase::PARAM_TYPE => 'integer'
1863  ],
1864  'smaxage' => [
1865  ApiBase::PARAM_TYPE => 'integer',
1866  ApiBase::PARAM_DFLT => 0
1867  ],
1868  'maxage' => [
1869  ApiBase::PARAM_TYPE => 'integer',
1870  ApiBase::PARAM_DFLT => 0
1871  ],
1872  'assert' => [
1873  ApiBase::PARAM_TYPE => [ 'user', 'bot' ]
1874  ],
1875  'assertuser' => [
1876  ApiBase::PARAM_TYPE => 'user',
1877  ],
1878  'requestid' => null,
1879  'servedby' => false,
1880  'curtimestamp' => false,
1881  'responselanginfo' => false,
1882  'origin' => null,
1883  'uselang' => [
1884  ApiBase::PARAM_DFLT => self::API_DEFAULT_USELANG,
1885  ],
1886  'errorformat' => [
1887  ApiBase::PARAM_TYPE => [ 'plaintext', 'wikitext', 'html', 'raw', 'none', 'bc' ],
1888  ApiBase::PARAM_DFLT => 'bc',
1889  ],
1890  'errorlang' => [
1891  ApiBase::PARAM_DFLT => 'uselang',
1892  ],
1893  'errorsuselocal' => [
1894  ApiBase::PARAM_DFLT => false,
1895  ],
1896  ];
1897  }
1898 
1900  protected function getExamplesMessages() {
1901  return [
1902  'action=help'
1903  => 'apihelp-help-example-main',
1904  'action=help&recursivesubmodules=1'
1905  => 'apihelp-help-example-recursive',
1906  ];
1907  }
1908 
1909  public function modifyHelp( array &$help, array $options, array &$tocData ) {
1910  // Wish PHP had an "array_insert_before". Instead, we have to manually
1911  // reindex the array to get 'permissions' in the right place.
1912  $oldHelp = $help;
1913  $help = [];
1914  foreach ( $oldHelp as $k => $v ) {
1915  if ( $k === 'submodules' ) {
1916  $help['permissions'] = '';
1917  }
1918  $help[$k] = $v;
1919  }
1920  $help['datatypes'] = '';
1921  $help['templatedparams'] = '';
1922  $help['credits'] = '';
1923 
1924  // Fill 'permissions'
1925  $help['permissions'] .= Html::openElement( 'div',
1926  [ 'class' => 'apihelp-block apihelp-permissions' ] );
1927  $m = $this->msg( 'api-help-permissions' );
1928  if ( !$m->isDisabled() ) {
1929  $help['permissions'] .= Html::rawElement( 'div', [ 'class' => 'apihelp-block-head' ],
1930  $m->numParams( count( self::$mRights ) )->parse()
1931  );
1932  }
1933  $help['permissions'] .= Html::openElement( 'dl' );
1934  foreach ( self::$mRights as $right => $rightMsg ) {
1935  $help['permissions'] .= Html::element( 'dt', null, $right );
1936 
1937  $rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
1938  $help['permissions'] .= Html::rawElement( 'dd', null, $rightMsg );
1939 
1940  $groups = array_map( function ( $group ) {
1941  return $group == '*' ? 'all' : $group;
1942  }, User::getGroupsWithPermission( $right ) );
1943 
1944  $help['permissions'] .= Html::rawElement( 'dd', null,
1945  $this->msg( 'api-help-permissions-granted-to' )
1946  ->numParams( count( $groups ) )
1947  ->params( Message::listParam( $groups ) )
1948  ->parse()
1949  );
1950  }
1951  $help['permissions'] .= Html::closeElement( 'dl' );
1952  $help['permissions'] .= Html::closeElement( 'div' );
1953 
1954  // Fill 'datatypes', 'templatedparams', and 'credits', if applicable
1955  if ( empty( $options['nolead'] ) ) {
1956  $level = $options['headerlevel'];
1957  $tocnumber = &$options['tocnumber'];
1958 
1959  $header = $this->msg( 'api-help-datatypes-header' )->parse();
1960 
1961  $id = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_PRIMARY );
1962  $idFallback = Sanitizer::escapeIdForAttribute( 'main/datatypes', 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/datatypes' && $idFallback !== 'main/datatypes' ) {
1972  $headline = '<div id="main/datatypes"></div>' . $headline;
1973  }
1974  $help['datatypes'] .= $headline;
1975  $help['datatypes'] .= $this->msg( 'api-help-datatypes' )->parseAsBlock();
1976  if ( !isset( $tocData['main/datatypes'] ) ) {
1977  $tocnumber[$level]++;
1978  $tocData['main/datatypes'] = [
1979  'toclevel' => count( $tocnumber ),
1980  'level' => $level,
1981  'anchor' => 'main/datatypes',
1982  'line' => $header,
1983  'number' => implode( '.', $tocnumber ),
1984  'index' => false,
1985  ];
1986  }
1987 
1988  $header = $this->msg( 'api-help-templatedparams-header' )->parse();
1989 
1990  $id = Sanitizer::escapeIdForAttribute( 'main/templatedparams', Sanitizer::ID_PRIMARY );
1991  $idFallback = Sanitizer::escapeIdForAttribute( 'main/templatedparams', Sanitizer::ID_FALLBACK );
1992  $headline = Linker::makeHeadline( min( 6, $level ),
1993  ' class="apihelp-header">',
1994  $id,
1995  $header,
1996  '',
1997  $idFallback
1998  );
1999  // Ensure we have a sane anchor
2000  if ( $id !== 'main/templatedparams' && $idFallback !== 'main/templatedparams' ) {
2001  $headline = '<div id="main/templatedparams"></div>' . $headline;
2002  }
2003  $help['templatedparams'] .= $headline;
2004  $help['templatedparams'] .= $this->msg( 'api-help-templatedparams' )->parseAsBlock();
2005  if ( !isset( $tocData['main/templatedparams'] ) ) {
2006  $tocnumber[$level]++;
2007  $tocData['main/templatedparams'] = [
2008  'toclevel' => count( $tocnumber ),
2009  'level' => $level,
2010  'anchor' => 'main/templatedparams',
2011  'line' => $header,
2012  'number' => implode( '.', $tocnumber ),
2013  'index' => false,
2014  ];
2015  }
2016 
2017  $header = $this->msg( 'api-credits-header' )->parse();
2018  $id = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_PRIMARY );
2019  $idFallback = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_FALLBACK );
2020  $headline = Linker::makeHeadline( min( 6, $level ),
2021  ' class="apihelp-header">',
2022  $id,
2023  $header,
2024  '',
2025  $idFallback
2026  );
2027  // Ensure we have a sane anchor
2028  if ( $id !== 'main/credits' && $idFallback !== 'main/credits' ) {
2029  $headline = '<div id="main/credits"></div>' . $headline;
2030  }
2031  $help['credits'] .= $headline;
2032  $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
2033  if ( !isset( $tocData['main/credits'] ) ) {
2034  $tocnumber[$level]++;
2035  $tocData['main/credits'] = [
2036  'toclevel' => count( $tocnumber ),
2037  'level' => $level,
2038  'anchor' => 'main/credits',
2039  'line' => $header,
2040  'number' => implode( '.', $tocnumber ),
2041  'index' => false,
2042  ];
2043  }
2044  }
2045  }
2046 
2048 
2053  public function canApiHighLimits() {
2054  if ( !isset( $this->mCanApiHighLimits ) ) {
2055  $this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' );
2056  }
2057 
2058  return $this->mCanApiHighLimits;
2059  }
2060 
2065  public function getModuleManager() {
2066  return $this->mModuleMgr;
2067  }
2068 
2077  public function getUserAgent() {
2078  return trim(
2079  $this->getRequest()->getHeader( 'Api-user-agent' ) . ' ' .
2080  $this->getRequest()->getHeader( 'User-agent' )
2081  );
2082  }
2083 }
2084 
setContext(IContextSource $context)
getAllowedParams()
See ApiBase for description.
Definition: ApiMain.php:1851
getModuleManager()
Overrides to return this instance&#39;s module manager.
Definition: ApiMain.php:2065
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below...
Definition: ApiBase.php:94
static matchRequestedHeaders( $requestedHeaders)
Attempt to validate the value of Access-Control-Request-Headers against a list of headers that we all...
Definition: ApiMain.php:822
getUserAgent()
Fetches the user agent used for this request.
Definition: ApiMain.php:2077
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition: ApiBase.php:261
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
setCacheMaxAge( $maxage)
Set how long the response should be cached.
Definition: ApiMain.php:402
getContinuationManager()
Get the continuation manager.
Definition: ApiMain.php:361
static $Modules
List of available modules: action name => module class.
Definition: ApiMain.php:55
$mEnableWrite
Definition: ApiMain.php:150
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
Formats errors and warnings for the API, and add them to the associated ApiResult.
addRequestedFields( $force=[])
Add requested fields to the result.
Definition: ApiMain.php:1138
static getRequestId()
Get the unique request ID.
Definition: WebRequest.php:275
executeActionWithErrorHandling()
Execute an action, and in case of an error, erase whatever partial results have been accumulated...
Definition: ApiMain.php:512
modifyHelp(array &$help, array $options, array &$tocData)
Definition: ApiMain.php:1909
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
const ID_PRIMARY
Tells escapeUrlForHtml() to encode the ID using the wiki&#39;s primary encoding.
Definition: Sanitizer.php:66
This class holds a list of modules and handles instantiation.
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:1972
bool null $lacksSameOriginSecurity
Cached return value from self::lacksSameOriginSecurity()
Definition: ApiMain.php:161
$mCdnMaxAge
Definition: ApiMain.php:151
static isEveryoneAllowed( $right)
Check if all users may be assumed to have the given permission.
Definition: User.php:4854
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
addDeprecation( $msg, $feature, $data=[])
Add a deprecation warning for this module.
Definition: ApiBase.php:1942
initPrinter( $unused=false)
Initialize the printer function and prepare the output headers.
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2147
checkExecutePermissions( $module)
Check for sufficient permissions to execute.
Definition: ApiMain.php:1411
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
Definition: ApiBase.php:55
static instance()
Singleton.
Definition: Profiler.php:62
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
getMaxLag()
Definition: ApiMain.php:1231
wfHostname()
Fetch server name for use in error reporting etc.
Exception used to abort API execution with an error.
$mCacheControl
Definition: ApiMain.php:156
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing &#39;/&#39;...
Definition: Html.php:251
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
An IContextSource implementation which will inherit context from another source but allow individual ...
static static static ApiFormatBase $mPrinter
Definition: ApiMain.php:133
checkReadOnly( $module)
Check if the DB is read-only for this user.
Definition: ApiMain.php:1442
static isValidApiCode( $code)
Test whether a code is a valid API error code.
This manages continuation state.
isInternalMode()
Return true if the API was started by other PHP code using FauxRequest.
Definition: ApiMain.php:301
$value
setCacheControl( $directives)
Set directives (key/value pairs) for the Cache-Control header.
Definition: ApiMain.php:475
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition: ApiBase.php:2009
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user...
Definition: ApiBase.php:761
static preOutputCommit(IContextSource $context, callable $postCommitWork=null)
This function commits all DB and session changes as needed before the client can receive a response (...
Definition: MediaWiki.php:588
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 MediaWikiServices
Definition: injection.txt:23
const GETHEADER_LIST
Flag to make WebRequest::getHeader return an array of values.
Definition: WebRequest.php:48
msg( $key)
Get a Message object with context set Parameters are the same as wfMessage()
canApiHighLimits()
Check whether the current user is allowed to use high limits.
Definition: ApiMain.php:2053
errorMessagesFromException( $e, $type='error')
Create an error message for the given exception.
Definition: ApiMain.php:1016
static rollbackMasterChangesAndLog( $e)
Roll back any open database transactions and log the stack trace of the exception.
A helper class for throttling authentication attempts.
this hook is for auditing only $response
Definition: hooks.txt:767
const API_DEFAULT_FORMAT
When no format parameter is given, this format will be used.
Definition: ApiMain.php:45
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition: hooks.txt:1781
IContextSource $context
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
ApiBase $mModule
Definition: ApiMain.php:153
static static static $mRights
List of user roles that are specifically relevant to the API.
Definition: ApiMain.php:130
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition: ApiBase.php:876
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=(\*-\*)") 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:1970
setupModule()
Set up the module for response.
Definition: ApiMain.php:1184
__construct( $context=null, $enableWrite=false)
Constructs an instance of ApiMain that utilizes the module and format specified by $request...
Definition: ApiMain.php:170
const ID_FALLBACK
Tells escapeUrlForHtml() to encode the ID using the fallback encoding, or return false if no fallback...
Definition: Sanitizer.php:74
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition: hooks.txt:3039
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
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:767
const API_DEFAULT_USELANG
When no uselang parameter is given, this language will be used.
Definition: ApiMain.php:50
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 just before the function returns a value If you return true
Definition: hooks.txt:1972
checkMaxLag( $module, $params)
Check the max lag if necessary.
Definition: ApiMain.php:1265
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
createPrinterByName( $format)
Create an instance of an output formatter by its name.
Definition: ApiMain.php:486
setRequestExpectations(ApiBase $module)
Set database connection, query, and write expectations given this module request. ...
Definition: ApiMain.php:1605
setupExternalResponse( $module, $params)
Check POST for external response and setup result printer.
Definition: ApiMain.php:1528
wfReadOnly()
Check whether the wiki is in read-only mode.
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness, which urlencode encodes by default.
$wgLang
Definition: Setup.php:922
printResult( $httpCode=0)
Print results using the current printer.
Definition: ApiMain.php:1825
static getMain()
Get the RequestContext object associated with the main request.
static getRedactedTraceAsString( $e)
Generate a string representation of an exception&#39;s stack trace.
static getGroupsWithPermission( $role)
Get all the groups who have a given permission.
Definition: User.php:4812
setCacheMode( $mode)
Set the type of caching headers which will be sent.
Definition: ApiMain.php:434
static escapeIdForAttribute( $id, $mode=self::ID_PRIMARY)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid HTM...
Definition: Sanitizer.php:1295
static newUUIDv4( $flags=0)
Return an RFC4122 compliant v4 UUID.
checkAsserts( $params)
Check asserts of the user&#39;s rights.
Definition: ApiMain.php:1497
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
handleException( $e)
Handle an exception as an API response.
Definition: ApiMain.php:569
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition: ApiBase.php:265
getContext()
Get the base IContextSource object.
$params
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:41
executeAction()
Execute the actual module, without any error handling.
Definition: ApiMain.php:1561
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:1972
isReadMode()
Definition: ApiMain.php:1842
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:315
handleCORS()
Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
Definition: ApiMain.php:678
setupExecuteAction()
Set up for the execution.
Definition: ApiMain.php:1169
getSensitiveParams()
Get the request parameters that should be considered sensitive.
Definition: ApiMain.php:1736
$mCacheMode
Definition: ApiMain.php:155
Format errors and warnings in the old style, for backwards compatibility.
$mErrorFormatter
Definition: ApiMain.php:146
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 null
Definition: hooks.txt:767
lacksSameOriginSecurity()
Get the security flag for the current request.
Definition: ApiMain.php:318
static runWithoutAbort( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:231
static wildcardToRegex( $wildcard)
Helper function to convert wildcard string into a regex &#39;*&#39; => &#39;.
Definition: ApiMain.php:863
const NO_SIZE_CHECK
For addValue() and similar functions, do not check size while adding a value Don&#39;t use this unless yo...
Definition: ApiResult.php:58
static factory( $code)
Get a cached or new language object for a given language code.
Definition: Language.php:216
This class represents the result of the API operations.
Definition: ApiResult.php:35
markParamsSensitive( $params)
Mark parameters as sensitive.
Definition: ApiMain.php:1745
$help
Definition: mcc.php:32
$mParamsSensitive
Definition: ApiMain.php:158
$header
$mCanApiHighLimits
Definition: ApiMain.php:2047
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:767
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
Definition: distributors.txt:9
static static $Formats
List of available formats: format name => format class.
Definition: ApiMain.php:113
getParamsUsed()
Get the request parameters used in the course of the preceding execute() request. ...
Definition: ApiMain.php:1719
getExamplesMessages()
Definition: ApiMain.php:1900
getErrorFormatter()
Get the ApiErrorFormatter object associated with current request.
Definition: ApiMain.php:353
getCheck( $name)
Get a boolean request value, and register the fact that the parameter was used, for logging...
Definition: ApiMain.php:1776
logRequest( $time, $e=null)
Log the preceding request.
Definition: ApiMain.php:1624
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
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:35
sendCacheHeaders( $isError)
Send caching headers.
Definition: ApiMain.php:879
checkBotReadOnly()
Check whether we are readonly for bots.
Definition: ApiMain.php:1458
getModule()
Get the API module object.
Definition: ApiMain.php:384
checkConditionalRequestHeaders( $module)
Check selected RFC 7232 precondition headers.
Definition: ApiMain.php:1311
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition: Linker.php:1749
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:2183
static appendDebugInfoToApiResult(IContextSource $context, ApiResult $result)
Append the debug info to given ApiResult.
Definition: MWDebug.php:481
dieReadOnly()
Helper function for readonly errors.
Definition: ApiBase.php:2108
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition: Message.php:429
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
$mParamsUsed
Definition: ApiMain.php:157
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition: ApiBase.php:1928
static sanitizeLangCode( $code)
Accepts a language code and ensures it&#39;s sane.
static handleApiBeforeMainException( $e)
Handle an exception from the ApiBeforeMain hook.
Definition: ApiMain.php:643
reportUnusedParams()
Report unused parameters, so the client gets a hint in case it gave us parameters we don&#39;t know...
Definition: ApiMain.php:1797
wfClearOutputBuffers()
More legible than passing a &#39;false&#39; parameter to wfResetOutputBuffers():
MediaWiki Logger LoggerFactory implements a PSR [0] compatible message logging system Named Psr Log LoggerInterface instances can be obtained from the MediaWiki Logger LoggerFactory::getInstance() static method. MediaWiki\Logger\LoggerFactory expects a class implementing the MediaWiki\Logger\Spi interface to act as a factory for new Psr\Log\LoggerInterface instances. The "Spi" in MediaWiki\Logger\Spi stands for "service provider interface". An SPI is an API intended to be implemented or extended by a third party. This software design pattern is intended to enable framework extension and replaceable components. It is specifically used in the MediaWiki\Logger\LoggerFactory service to allow alternate PSR-3 logging implementations to be easily integrated with MediaWiki. The service provider interface allows the backend logging library to be implemented in multiple ways. The $wgMWLoggerDefaultSpi global provides the classname of the default MediaWiki\Logger\Spi implementation to be loaded at runtime. This can either be the name of a class implementing the MediaWiki\Logger\Spi with a zero argument const ructor or a callable that will return an MediaWiki\Logger\Spi instance. Alternately the MediaWiki\Logger\LoggerFactory MediaWiki Logger LoggerFactory
Definition: logger.txt:5
This abstract class implements many basic API functions, and is the base of all API classes...
Definition: ApiBase.php:42
static singleton( $domain=false)
substituteResultWithError( $e)
Replace the result data with the information about an exception.
Definition: ApiMain.php:1054
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
getPrinter()
Get the result formatter object.
Definition: ApiMain.php:393
$messages
getStatusValue()
Fetch the error status.
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:535
markParamsUsed( $params)
Mark parameters as used.
Definition: ApiMain.php:1727
getUpload( $name)
Get a request upload, and register the fact that it was used, for logging.
Definition: ApiMain.php:1787
setContinuationManager(ApiContinuationManager $manager=null)
Set the continuation manager.
Definition: ApiMain.php:369
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:2621
$mInternalMode
Definition: ApiMain.php:151
Interface for MediaWiki-localized exceptions.
$mModuleMgr
Definition: ApiMain.php:146
encodeRequestLogValue( $s)
Encode a value in a format suitable for a space-separated log line.
Definition: ApiMain.php:1702
execute()
Execute api request.
Definition: ApiMain.php:500
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1454
static listParam(array $list, $type='text')
Definition: Message.php:1128
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
static matchOrigin( $value, $rules, $exceptions)
Attempt to match an Origin header against a set of rules and a set of exceptions. ...
Definition: ApiMain.php:798
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:40
getResult()
Get the ApiResult object associated with current request.
Definition: ApiMain.php:310
createErrorPrinter()
Create the printer for error output.
Definition: ApiMain.php:985
isWriteMode()
Indicates whether this module requires write mode.
Definition: ApiBase.php:427
ApiContinuationManager null $mContinuationManager
Definition: ApiMain.php:148
getVal( $name, $default=null)
Get a request value, and register the fact that it was used, for logging.
Definition: ApiMain.php:1755