MediaWiki  master
ApiMain.php
Go to the documentation of this file.
1 <?php
31 use Wikimedia\Timestamp\TimestampException;
32 
49 class ApiMain extends ApiBase {
53  private const API_DEFAULT_FORMAT = 'jsonfm';
54 
58  private const API_DEFAULT_USELANG = 'user';
59 
63  private const MODULES = [
64  'login' => [
65  'class' => ApiLogin::class,
66  'services' => [
67  'AuthManager',
68  ],
69  ],
70  'clientlogin' => [
71  'class' => ApiClientLogin::class,
72  'services' => [
73  'AuthManager',
74  ],
75  ],
76  'logout' => ApiLogout::class,
77  'createaccount' => [
78  'class' => ApiAMCreateAccount::class,
79  'services' => [
80  'AuthManager',
81  ],
82  ],
83  'linkaccount' => [
84  'class' => ApiLinkAccount::class,
85  'services' => [
86  'AuthManager',
87  ],
88  ],
89  'unlinkaccount' => [
90  'class' => ApiRemoveAuthenticationData::class,
91  'services' => [
92  'AuthManager',
93  ],
94  ],
95  'changeauthenticationdata' => [
96  'class' => ApiChangeAuthenticationData::class,
97  'services' => [
98  'AuthManager',
99  ],
100  ],
101  'removeauthenticationdata' => [
102  'class' => ApiRemoveAuthenticationData::class,
103  'services' => [
104  'AuthManager',
105  ],
106  ],
107  'resetpassword' => [
108  'class' => ApiResetPassword::class,
109  'services' => [
110  'PasswordReset',
111  ]
112  ],
113  'query' => ApiQuery::class,
114  'expandtemplates' => [
115  'class' => ApiExpandTemplates::class,
116  'services' => [
117  'RevisionStore',
118  'Parser',
119  ]
120  ],
121  'parse' => [
122  'class' => ApiParse::class,
123  'services' => [
124  'RevisionLookup',
125  'SkinFactory',
126  'LanguageNameUtils',
127  'LinkBatchFactory',
128  'LinkCache',
129  'ContentHandlerFactory',
130  'Parser',
131  'WikiPageFactory',
132  ]
133  ],
134  'stashedit' => [
135  'class' => ApiStashEdit::class,
136  'services' => [
137  'ContentHandlerFactory',
138  'PageEditStash',
139  'RevisionLookup',
140  'StatsdDataFactory',
141  'WikiPageFactory',
142  ]
143  ],
144  'opensearch' => [
145  'class' => ApiOpenSearch::class,
146  'services' => [
147  'LinkBatchFactory',
148  'SearchEngineConfig',
149  'SearchEngineFactory',
150  ]
151  ],
152  'feedcontributions' => [
153  'class' => ApiFeedContributions::class,
154  'services' => [
155  'RevisionStore',
156  'TitleParser',
157  'LinkRenderer',
158  'LinkBatchFactory',
159  'HookContainer',
160  'DBLoadBalancer',
161  'NamespaceInfo',
162  'ActorMigration',
163  ]
164  ],
165  'feedrecentchanges' => [
166  'class' => ApiFeedRecentChanges::class,
167  'services' => [
168  'SpecialPageFactory',
169  ]
170  ],
171  'feedwatchlist' => [
172  'class' => ApiFeedWatchlist::class,
173  'services' => [
174  'Parser',
175  ]
176  ],
177  'help' => [
178  'class' => ApiHelp::class,
179  'services' => [
180  'SkinFactory',
181  ]
182  ],
183  'paraminfo' => [
184  'class' => ApiParamInfo::class,
185  'services' => [
186  'UserFactory',
187  ],
188  ],
189  'rsd' => ApiRsd::class,
190  'compare' => [
191  'class' => ApiComparePages::class,
192  'services' => [
193  'RevisionStore',
194  'SlotRoleRegistry',
195  'ContentHandlerFactory',
196  ]
197  ],
198  'tokens' => ApiTokens::class,
199  'checktoken' => ApiCheckToken::class,
200  'cspreport' => ApiCSPReport::class,
201  'validatepassword' => [
202  'class' => ApiValidatePassword::class,
203  'services' => [
204  'AuthManager',
205  'UserFactory',
206  ]
207  ],
208 
209  // Write modules
210  'purge' => [
211  'class' => ApiPurge::class,
212  'services' => [
213  'WikiPageFactory',
214  ],
215  ],
216  'setnotificationtimestamp' => [
217  'class' => ApiSetNotificationTimestamp::class,
218  'services' => [
219  'DBLoadBalancer',
220  'RevisionStore',
221  'WatchedItemStore',
222  ]
223  ],
224  'rollback' => [
225  'class' => ApiRollback::class,
226  'services' => [
227  'RollbackPageFactory',
228  'WatchlistManager',
229  'UserOptionsLookup',
230  ]
231  ],
232  'delete' => [
233  'class' => ApiDelete::class,
234  'services' => [
235  'RepoGroup',
236  'WatchlistManager',
237  'UserOptionsLookup',
238  ]
239  ],
240  'undelete' => [
241  'class' => ApiUndelete::class,
242  'services' => [
243  'WatchlistManager',
244  'UserOptionsLookup',
245  ]
246  ],
247  'protect' => [
248  'class' => ApiProtect::class,
249  'services' => [
250  'WatchlistManager',
251  'UserOptionsLookup',
252  ]
253  ],
254  'block' => [
255  'class' => ApiBlock::class,
256  'services' => [
257  'BlockPermissionCheckerFactory',
258  'BlockUserFactory',
259  'TitleFactory',
260  'UserIdentityLookup',
261  'WatchedItemStore',
262  'BlockUtils',
263  'BlockActionInfo',
264  'WatchlistManager',
265  'UserOptionsLookup',
266  ]
267  ],
268  'unblock' => [
269  'class' => ApiUnblock::class,
270  'services' => [
271  'BlockPermissionCheckerFactory',
272  'UnblockUserFactory',
273  'UserIdentityLookup',
274  ]
275  ],
276  'move' => [
277  'class' => ApiMove::class,
278  'services' => [
279  'MovePageFactory',
280  'RepoGroup',
281  'WatchlistManager',
282  'UserOptionsLookup',
283  ]
284  ],
285  'edit' => [
286  'class' => ApiEditPage::class,
287  'services' => [
288  'ContentHandlerFactory',
289  'RevisionLookup',
290  'WatchedItemStore',
291  'WikiPageFactory',
292  'WatchlistManager',
293  'UserOptionsLookup',
294  ]
295  ],
296  'upload' => [
297  'class' => ApiUpload::class,
298  'services' => [
299  'JobQueueGroup',
300  'WatchlistManager',
301  'UserOptionsLookup',
302  ]
303  ],
304  'filerevert' => [
305  'class' => ApiFileRevert::class,
306  'services' => [
307  'RepoGroup',
308  ]
309  ],
310  'emailuser' => ApiEmailUser::class,
311  'watch' => [
312  'class' => ApiWatch::class,
313  'services' => [
314  'WatchlistManager',
315  ]
316  ],
317  'patrol' => [
318  'class' => ApiPatrol::class,
319  'services' => [
320  'RevisionStore',
321  ]
322  ],
323  'import' => [
324  'class' => ApiImport::class,
325  'services' => [
326  'WikiImporterFactory',
327  ]
328  ],
329  'clearhasmsg' => [
330  'class' => ApiClearHasMsg::class,
331  'services' => [
332  'TalkPageNotificationManager',
333  ]
334  ],
335  'userrights' => [
336  'class' => ApiUserrights::class,
337  'services' => [
338  'UserGroupManager',
339  ]
340  ],
341  'options' => [
342  'class' => ApiOptions::class,
343  'services' => [
344  'UserOptionsManager',
345  'PreferencesFactory',
346  ],
347  ],
348  'imagerotate' => [
349  'class' => ApiImageRotate::class,
350  'services' => [
351  'RepoGroup',
352  'TempFSFileFactory',
353  ]
354  ],
355  'revisiondelete' => ApiRevisionDelete::class,
356  'managetags' => ApiManageTags::class,
357  'tag' => [
358  'class' => ApiTag::class,
359  'services' => [
360  'DBLoadBalancer',
361  'RevisionStore',
362  ]
363  ],
364  'mergehistory' => [
365  'class' => ApiMergeHistory::class,
366  'services' => [
367  'MergeHistoryFactory',
368  ],
369  ],
370  'setpagelanguage' => [
371  'class' => ApiSetPageLanguage::class,
372  'services' => [
373  'DBLoadBalancer',
374  'LanguageNameUtils',
375  ]
376  ],
377  'changecontentmodel' => [
378  'class' => ApiChangeContentModel::class,
379  'services' => [
380  'ContentHandlerFactory',
381  'ContentModelChangeFactory',
382  ]
383  ],
384  ];
385 
389  private const FORMATS = [
390  'json' => ApiFormatJson::class,
391  'jsonfm' => ApiFormatJson::class,
392  'php' => ApiFormatPhp::class,
393  'phpfm' => ApiFormatPhp::class,
394  'xml' => ApiFormatXml::class,
395  'xmlfm' => ApiFormatXml::class,
396  'rawfm' => ApiFormatJson::class,
397  'none' => ApiFormatNone::class,
398  ];
399 
406  private const RIGHTS_MAP = [
407  'writeapi' => [
408  'msg' => 'right-writeapi',
409  'params' => []
410  ],
411  'apihighlimits' => [
412  'msg' => 'api-help-right-apihighlimits',
414  ]
415  ];
416 
418  private $mPrinter;
419 
421  private $mModuleMgr;
422 
424  private $mResult;
425 
428 
431 
434 
436  private $mAction;
437 
439  private $mEnableWrite;
440 
442  private $mInternalMode;
443 
445  private $mModule;
446 
448  private $mCacheMode = 'private';
449 
451  private $mCacheControl = [];
452 
454  private $mParamsUsed = [];
455 
457  private $mParamsSensitive = [];
458 
460  private $lacksSameOriginSecurity = null;
461 
470  public function __construct( $context = null, $enableWrite = false ) {
471  if ( $context === null ) {
473  } elseif ( $context instanceof WebRequest ) {
474  // BC for pre-1.19
475  $request = $context;
477  }
478  // We set a derivative context so we can change stuff later
479  $derivativeContext = new DerivativeContext( $context );
480  $this->setContext( $derivativeContext );
481 
482  if ( isset( $request ) ) {
483  $derivativeContext->setRequest( $request );
484  } else {
485  $request = $this->getRequest();
486  }
487 
488  $this->mInternalMode = ( $request instanceof FauxRequest );
489 
490  // Special handling for the main module: $parent === $this
491  parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
492 
493  $config = $this->getConfig();
494 
495  if ( !$this->mInternalMode ) {
496  // If we're in a mode that breaks the same-origin policy, strip
497  // user credentials for security.
498  if ( $this->lacksSameOriginSecurity() ) {
499  global $wgUser;
500  wfDebug( "API: stripping user credentials when the same-origin policy is not applied" );
501  $user = new User();
502  $wgUser = $user;
503  $derivativeContext->setUser( $user );
504  $request->response()->header( 'MediaWiki-Login-Suppressed: true' );
505  }
506  }
507 
508  // TODO inject stuff, see T265644
509  $services = MediaWikiServices::getInstance();
510  $this->mParamValidator = new ApiParamValidator(
511  $this,
512  $services->getObjectFactory()
513  );
514 
515  $this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
516 
517  // Setup uselang. This doesn't use $this->getParameter()
518  // because we're not ready to handle errors yet.
519  // Optimisation: Avoid slow getVal(), this isn't user-generated content.
520  $uselang = $request->getRawVal( 'uselang', self::API_DEFAULT_USELANG );
521  if ( $uselang === 'user' ) {
522  // Assume the parent context is going to return the user language
523  // for uselang=user (see T85635).
524  } else {
525  if ( $uselang === 'content' ) {
526  $uselang = $services->getContentLanguage()->getCode();
527  }
528  $code = RequestContext::sanitizeLangCode( $uselang );
529  $derivativeContext->setLanguage( $code );
530  if ( !$this->mInternalMode ) {
531  global $wgLang;
532  $wgLang = $derivativeContext->getLanguage();
533  RequestContext::getMain()->setLanguage( $wgLang );
534  }
535  }
536 
537  // Set up the error formatter. This doesn't use $this->getParameter()
538  // because we're not ready to handle errors yet.
539  // Optimisation: Avoid slow getVal(), this isn't user-generated content.
540  $errorFormat = $request->getRawVal( 'errorformat', 'bc' );
541  $errorLangCode = $request->getRawVal( 'errorlang', 'uselang' );
542  $errorsUseDB = $request->getCheck( 'errorsuselocal' );
543  if ( in_array( $errorFormat, [ 'plaintext', 'wikitext', 'html', 'raw', 'none' ], true ) ) {
544  if ( $errorLangCode === 'uselang' ) {
545  $errorLang = $this->getLanguage();
546  } elseif ( $errorLangCode === 'content' ) {
547  $errorLang = $services->getContentLanguage();
548  } else {
549  $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode );
550  $errorLang = $services->getLanguageFactory()->getLanguage( $errorLangCode );
551  }
552  $this->mErrorFormatter = new ApiErrorFormatter(
553  $this->mResult,
554  $errorLang,
555  $errorFormat,
556  $errorsUseDB
557  );
558  } else {
559  $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
560  }
561  $this->mResult->setErrorFormatter( $this->getErrorFormatter() );
562 
563  $this->mModuleMgr = new ApiModuleManager(
564  $this,
565  $services->getObjectFactory()
566  );
567  $this->mModuleMgr->addModules( self::MODULES, 'action' );
568  $this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
569  $this->mModuleMgr->addModules( self::FORMATS, 'format' );
570  $this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' );
571 
572  $this->getHookRunner()->onApiMain__moduleManager( $this->mModuleMgr );
573 
574  $this->mContinuationManager = null;
575  $this->mEnableWrite = $enableWrite;
576  }
577 
582  public function isInternalMode() {
583  return $this->mInternalMode;
584  }
585 
591  public function getResult() {
592  return $this->mResult;
593  }
594 
599  public function lacksSameOriginSecurity() {
600  if ( $this->lacksSameOriginSecurity !== null ) {
602  }
603 
604  $request = $this->getRequest();
605 
606  // JSONP mode
607  if ( $request->getCheck( 'callback' ) ) {
608  $this->lacksSameOriginSecurity = true;
609  return true;
610  }
611 
612  // Anonymous CORS
613  if ( $request->getVal( 'origin' ) === '*' ) {
614  $this->lacksSameOriginSecurity = true;
615  return true;
616  }
617 
618  // Header to be used from XMLHTTPRequest when the request might
619  // otherwise be used for XSS.
620  if ( $request->getHeader( 'Treat-as-Untrusted' ) !== false ) {
621  $this->lacksSameOriginSecurity = true;
622  return true;
623  }
624 
625  // Allow extensions to override.
626  $this->lacksSameOriginSecurity = !$this->getHookRunner()
627  ->onRequestHasSameOriginSecurity( $request );
629  }
630 
635  public function getErrorFormatter() {
636  return $this->mErrorFormatter;
637  }
638 
642  public function getContinuationManager() {
644  }
645 
649  public function setContinuationManager( ApiContinuationManager $manager = null ) {
650  if ( $manager !== null && $this->mContinuationManager !== null ) {
651  throw new UnexpectedValueException(
652  __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
653  ' when a manager is already set from ' . $this->mContinuationManager->getSource()
654  );
655  }
656  $this->mContinuationManager = $manager;
657  }
658 
664  return $this->mParamValidator;
665  }
666 
672  public function getModule() {
673  return $this->mModule;
674  }
675 
681  public function getPrinter() {
682  return $this->mPrinter;
683  }
684 
690  public function setCacheMaxAge( $maxage ) {
691  $this->setCacheControl( [
692  'max-age' => $maxage,
693  's-maxage' => $maxage
694  ] );
695  }
696 
722  public function setCacheMode( $mode ) {
723  if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
724  wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"" );
725 
726  // Ignore for forwards-compatibility
727  return;
728  }
729 
730  if ( !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) ) {
731  // Private wiki, only private headers
732  if ( $mode !== 'private' ) {
733  wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki" );
734 
735  return;
736  }
737  }
738 
739  if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
740  // User language is used for i18n, so we don't want to publicly
741  // cache. Anons are ok, because if they have non-default language
742  // then there's an appropriate Vary header set by whatever set
743  // their non-default language.
744  wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
745  "'anon-public-user-private' due to uselang=user" );
746  $mode = 'anon-public-user-private';
747  }
748 
749  wfDebug( __METHOD__ . ": setting cache mode $mode" );
750  $this->mCacheMode = $mode;
751  }
752 
763  public function setCacheControl( $directives ) {
764  $this->mCacheControl = $directives + $this->mCacheControl;
765  }
766 
774  public function createPrinterByName( $format ) {
775  $printer = $this->mModuleMgr->getModule( $format, 'format', /* $ignoreCache */ true );
776  if ( $printer === null ) {
777  $this->dieWithError(
778  [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
779  );
780  }
781 
782  return $printer;
783  }
784 
788  public function execute() {
789  if ( $this->mInternalMode ) {
790  $this->executeAction();
791  } else {
793  }
794  }
795 
800  protected function executeActionWithErrorHandling() {
801  // Verify the CORS header before executing the action
802  if ( !$this->handleCORS() ) {
803  // handleCORS() has sent a 403, abort
804  return;
805  }
806 
807  // Exit here if the request method was OPTIONS
808  // (assume there will be a followup GET or POST)
809  if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
810  return;
811  }
812 
813  // In case an error occurs during data output,
814  // clear the output buffer and print just the error information
815  $obLevel = ob_get_level();
816  ob_start();
817 
818  $t = microtime( true );
819  $isError = false;
820  try {
821  $this->executeAction();
822  $runTime = microtime( true ) - $t;
823  $this->logRequest( $runTime );
824  MediaWikiServices::getInstance()->getStatsdDataFactory()->timing(
825  'api.' . $this->mModule->getModuleName() . '.executeTiming', 1000 * $runTime
826  );
827  } catch ( Throwable $e ) {
828  $this->handleException( $e );
829  $this->logRequest( microtime( true ) - $t, $e );
830  $isError = true;
831  }
832 
833  // Disable the client cache on the output so that BlockManager::trackBlockWithCookie is executed
834  // as part of MediaWiki::preOutputCommit().
835  if (
836  $this->mCacheMode === 'private'
837  || (
838  $this->mCacheMode === 'anon-public-user-private'
839  && SessionManager::getGlobalSession()->isPersistent()
840  )
841  ) {
842  $this->getContext()->getOutput()->enableClientCache( false );
843  $this->getContext()->getOutput()->considerCacheSettingsFinal();
844  }
845 
846  // Commit DBs and send any related cookies and headers
848 
849  // Send cache headers after any code which might generate an error, to
850  // avoid sending public cache headers for errors.
851  $this->sendCacheHeaders( $isError );
852 
853  // Executing the action might have already messed with the output
854  // buffers.
855  while ( ob_get_level() > $obLevel ) {
856  ob_end_flush();
857  }
858  }
859 
866  protected function handleException( Throwable $e ) {
867  // T65145: Rollback any open database transactions
868  if ( !$e instanceof ApiUsageException ) {
869  // ApiUsageExceptions are intentional, so don't rollback if that's the case
871  $e,
872  MWExceptionHandler::CAUGHT_BY_ENTRYPOINT
873  );
874  }
875 
876  // Allow extra cleanup and logging
877  $this->getHookRunner()->onApiMain__onException( $this, $e );
878 
879  // Handle any kind of exception by outputting properly formatted error message.
880  // If this fails, an unhandled exception should be thrown so that global error
881  // handler will process and log it.
882 
883  $errCodes = $this->substituteResultWithError( $e );
884 
885  // Error results should not be cached
886  $this->setCacheMode( 'private' );
887 
888  $response = $this->getRequest()->response();
889  $headerStr = 'MediaWiki-API-Error: ' . implode( ', ', $errCodes );
890  $response->header( $headerStr );
891 
892  // Reset and print just the error message
893  ob_clean();
894 
895  // Printer may not be initialized if the extractRequestParams() fails for the main module
896  $this->createErrorPrinter();
897 
898  // Get desired HTTP code from an ApiUsageException. Don't use codes from other
899  // exception types, as they are unlikely to be intended as an HTTP code.
900  $httpCode = $e instanceof ApiUsageException ? $e->getCode() : 0;
901 
902  $failed = false;
903  try {
904  $this->printResult( $httpCode );
905  } catch ( ApiUsageException $ex ) {
906  // The error printer itself is failing. Try suppressing its request
907  // parameters and redo.
908  $failed = true;
909  $this->addWarning( 'apiwarn-errorprinterfailed' );
910  foreach ( $ex->getStatusValue()->getErrors() as $error ) {
911  try {
912  $this->mPrinter->addWarning( $error );
913  } catch ( Throwable $ex2 ) {
914  // WTF?
915  $this->addWarning( $error );
916  }
917  }
918  }
919  if ( $failed ) {
920  $this->mPrinter = null;
921  $this->createErrorPrinter();
922  $this->mPrinter->forceDefaultParams();
923  if ( $httpCode ) {
924  $response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200
925  }
926  $this->printResult( $httpCode );
927  }
928  }
929 
940  public static function handleApiBeforeMainException( Throwable $e ) {
941  ob_start();
942 
943  try {
944  $main = new self( RequestContext::getMain(), false );
945  $main->handleException( $e );
946  $main->logRequest( 0, $e );
947  } catch ( Throwable $e2 ) {
948  // Nope, even that didn't work. Punt.
949  throw $e;
950  }
951 
952  // Reset cache headers
953  $main->sendCacheHeaders( true );
954 
955  ob_end_flush();
956  }
957 
972  protected function handleCORS() {
973  $originParam = $this->getParameter( 'origin' ); // defaults to null
974  if ( $originParam === null ) {
975  // No origin parameter, nothing to do
976  return true;
977  }
978 
979  $request = $this->getRequest();
980  $response = $request->response();
981 
982  $allowTiming = false;
983  $varyOrigin = true;
984 
985  if ( $originParam === '*' ) {
986  // Request for anonymous CORS
987  // Technically we should check for the presence of an Origin header
988  // and not process it as CORS if it's not set, but that would
989  // require us to vary on Origin for all 'origin=*' requests which
990  // we don't want to do.
991  $matchedOrigin = true;
992  $allowOrigin = '*';
993  $allowCredentials = 'false';
994  $varyOrigin = false; // No need to vary
995  } else {
996  // Non-anonymous CORS, check we allow the domain
997 
998  // Origin: header is a space-separated list of origins, check all of them
999  $originHeader = $request->getHeader( 'Origin' );
1000  if ( $originHeader === false ) {
1001  $origins = [];
1002  } else {
1003  $originHeader = trim( $originHeader );
1004  $origins = preg_split( '/\s+/', $originHeader );
1005  }
1006 
1007  if ( !in_array( $originParam, $origins ) ) {
1008  // origin parameter set but incorrect
1009  // Send a 403 response
1010  $response->statusHeader( 403 );
1011  $response->header( 'Cache-Control: no-cache' );
1012  echo "'origin' parameter does not match Origin header\n";
1013 
1014  return false;
1015  }
1016 
1017  $config = $this->getConfig();
1018  $origin = Origin::parseHeaderList( $origins );
1019  $matchedOrigin = $origin->match(
1020  $config->get( 'CrossSiteAJAXdomains' ),
1021  $config->get( 'CrossSiteAJAXdomainExceptions' )
1022  );
1023 
1024  $allowOrigin = $originHeader;
1025  $allowCredentials = 'true';
1026  $allowTiming = $originHeader;
1027  }
1028 
1029  if ( $matchedOrigin ) {
1030  $requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
1031  $preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
1032  if ( $preflight ) {
1033  // We allow the actual request to send the following headers
1034  $requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
1035  $allowedHeaders = $this->getConfig()->get( 'AllowedCorsHeaders' );
1036  if ( $requestedHeaders !== false ) {
1037  if ( !self::matchRequestedHeaders( $requestedHeaders, $allowedHeaders ) ) {
1038  $response->header( 'MediaWiki-CORS-Rejection: Unsupported header requested in preflight' );
1039  return true;
1040  }
1041  $response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
1042  }
1043 
1044  // We only allow the actual request to be GET, POST, or HEAD
1045  $response->header( 'Access-Control-Allow-Methods: POST, GET, HEAD' );
1046  }
1047 
1048  $response->header( "Access-Control-Allow-Origin: $allowOrigin" );
1049  $response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
1050  // https://www.w3.org/TR/resource-timing/#timing-allow-origin
1051  if ( $allowTiming !== false ) {
1052  $response->header( "Timing-Allow-Origin: $allowTiming" );
1053  }
1054 
1055  if ( !$preflight ) {
1056  $response->header(
1057  'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag, '
1058  . 'MediaWiki-Login-Suppressed'
1059  );
1060  }
1061  } else {
1062  $response->header( 'MediaWiki-CORS-Rejection: Origin mismatch' );
1063  }
1064 
1065  if ( $varyOrigin ) {
1066  $this->getOutput()->addVaryHeader( 'Origin' );
1067  }
1068 
1069  return true;
1070  }
1071 
1080  protected static function matchRequestedHeaders( $requestedHeaders, $allowedHeaders ) {
1081  if ( trim( $requestedHeaders ) === '' ) {
1082  return true;
1083  }
1084  $requestedHeaders = explode( ',', $requestedHeaders );
1085  $allowedHeaders = array_change_key_case(
1086  array_fill_keys( $allowedHeaders, true ), CASE_LOWER );
1087  foreach ( $requestedHeaders as $rHeader ) {
1088  $rHeader = strtolower( trim( $rHeader ) );
1089  if ( !isset( $allowedHeaders[$rHeader] ) ) {
1090  LoggerFactory::getInstance( 'api-warning' )->warning(
1091  'CORS preflight failed on requested header: {header}', [
1092  'header' => $rHeader
1093  ]
1094  );
1095  return false;
1096  }
1097  }
1098  return true;
1099  }
1100 
1106  protected function sendCacheHeaders( $isError ) {
1107  $response = $this->getRequest()->response();
1108  $out = $this->getOutput();
1109 
1110  $out->addVaryHeader( 'Treat-as-Untrusted' );
1111 
1112  $config = $this->getConfig();
1113 
1114  if ( $config->get( 'VaryOnXFP' ) ) {
1115  $out->addVaryHeader( 'X-Forwarded-Proto' );
1116  }
1117 
1118  if ( !$isError && $this->mModule &&
1119  ( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
1120  ) {
1121  $etag = $this->mModule->getConditionalRequestData( 'etag' );
1122  if ( $etag !== null ) {
1123  $response->header( "ETag: $etag" );
1124  }
1125  $lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
1126  if ( $lastMod !== null ) {
1127  $response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
1128  }
1129  }
1130 
1131  // The logic should be:
1132  // $this->mCacheControl['max-age'] is set?
1133  // Use it, the module knows better than our guess.
1134  // !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
1135  // Use 0 because we can guess caching is probably the wrong thing to do.
1136  // Use $this->getParameter( 'maxage' ), which already defaults to 0.
1137  $maxage = 0;
1138  if ( isset( $this->mCacheControl['max-age'] ) ) {
1139  $maxage = $this->mCacheControl['max-age'];
1140  } elseif ( ( $this->mModule && !$this->mModule->isWriteMode() ) ||
1141  $this->mCacheMode !== 'private'
1142  ) {
1143  $maxage = $this->getParameter( 'maxage' );
1144  }
1145  $privateCache = 'private, must-revalidate, max-age=' . $maxage;
1146 
1147  if ( $this->mCacheMode == 'private' ) {
1148  $response->header( "Cache-Control: $privateCache" );
1149  return;
1150  }
1151 
1152  if ( $this->mCacheMode == 'anon-public-user-private' ) {
1153  $out->addVaryHeader( 'Cookie' );
1154  $response->header( $out->getVaryHeader() );
1155  if ( SessionManager::getGlobalSession()->isPersistent() ) {
1156  // Logged in or otherwise has session (e.g. anonymous users who have edited)
1157  // Mark request private
1158  $response->header( "Cache-Control: $privateCache" );
1159 
1160  return;
1161  } // else anonymous, send public headers below
1162  }
1163 
1164  // Send public headers
1165  $response->header( $out->getVaryHeader() );
1166 
1167  // If nobody called setCacheMaxAge(), use the (s)maxage parameters
1168  if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
1169  $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
1170  }
1171  if ( !isset( $this->mCacheControl['max-age'] ) ) {
1172  $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
1173  }
1174 
1175  if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
1176  // Public cache not requested
1177  // Sending a Vary header in this case is harmless, and protects us
1178  // against conditional calls of setCacheMaxAge().
1179  $response->header( "Cache-Control: $privateCache" );
1180 
1181  return;
1182  }
1183 
1184  $this->mCacheControl['public'] = true;
1185 
1186  // Send an Expires header
1187  $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
1188  $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
1189  $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
1190 
1191  // Construct the Cache-Control header
1192  $ccHeader = '';
1193  $separator = '';
1194  foreach ( $this->mCacheControl as $name => $value ) {
1195  if ( is_bool( $value ) ) {
1196  if ( $value ) {
1197  $ccHeader .= $separator . $name;
1198  $separator = ', ';
1199  }
1200  } else {
1201  $ccHeader .= $separator . "$name=$value";
1202  $separator = ', ';
1203  }
1204  }
1205 
1206  $response->header( "Cache-Control: $ccHeader" );
1207  }
1208 
1212  private function createErrorPrinter() {
1213  if ( !isset( $this->mPrinter ) ) {
1214  $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
1215  if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
1216  $value = self::API_DEFAULT_FORMAT;
1217  }
1218  $this->mPrinter = $this->createPrinterByName( $value );
1219  }
1220 
1221  // Printer may not be able to handle errors. This is particularly
1222  // likely if the module returns something for getCustomPrinter().
1223  if ( !$this->mPrinter->canPrintErrors() ) {
1224  $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
1225  }
1226  }
1227 
1243  protected function errorMessagesFromException( Throwable $e, $type = 'error' ) {
1244  $messages = [];
1245  if ( $e instanceof ApiUsageException ) {
1246  foreach ( $e->getStatusValue()->getErrorsByType( $type ) as $error ) {
1247  $messages[] = ApiMessage::create( $error );
1248  }
1249  } elseif ( $type !== 'error' ) {
1250  // None of the rest have any messages for non-error types
1251  } else {
1252  // Something is seriously wrong
1253  $config = $this->getConfig();
1254  // TODO: Avoid embedding arbitrary class names in the error code.
1255  $class = preg_replace( '#^Wikimedia\\\Rdbms\\\#', '', get_class( $e ) );
1256  $code = 'internal_api_error_' . $class;
1257  $data = [ 'errorclass' => get_class( $e ) ];
1258  if ( $config->get( 'ShowExceptionDetails' ) ) {
1259  if ( $e instanceof ILocalizedException ) {
1260  $msg = $e->getMessageObject();
1261  } elseif ( $e instanceof MessageSpecifier ) {
1262  $msg = Message::newFromSpecifier( $e );
1263  } else {
1264  $msg = wfEscapeWikiText( $e->getMessage() );
1265  }
1266  $params = [ 'apierror-exceptioncaught', WebRequest::getRequestId(), $msg ];
1267  } else {
1268  $params = [ 'apierror-exceptioncaughttype', WebRequest::getRequestId(), get_class( $e ) ];
1269  }
1270 
1271  $messages[] = ApiMessage::create( $params, $code, $data );
1272  }
1273  return $messages;
1274  }
1275 
1281  protected function substituteResultWithError( Throwable $e ) {
1282  $result = $this->getResult();
1283  $formatter = $this->getErrorFormatter();
1284  $config = $this->getConfig();
1285  $errorCodes = [];
1286 
1287  // Remember existing warnings and errors across the reset
1288  $errors = $result->getResultData( [ 'errors' ] );
1289  $warnings = $result->getResultData( [ 'warnings' ] );
1290  $result->reset();
1291  if ( $warnings !== null ) {
1292  $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
1293  }
1294  if ( $errors !== null ) {
1295  $result->addValue( null, 'errors', $errors, ApiResult::NO_SIZE_CHECK );
1296 
1297  // Collect the copied error codes for the return value
1298  foreach ( $errors as $error ) {
1299  if ( isset( $error['code'] ) ) {
1300  $errorCodes[$error['code']] = true;
1301  }
1302  }
1303  }
1304 
1305  // Add errors from the exception
1306  $modulePath = $e instanceof ApiUsageException ? $e->getModulePath() : null;
1307  foreach ( $this->errorMessagesFromException( $e, 'error' ) as $msg ) {
1308  if ( ApiErrorFormatter::isValidApiCode( $msg->getApiCode() ) ) {
1309  $errorCodes[$msg->getApiCode()] = true;
1310  } else {
1311  LoggerFactory::getInstance( 'api-warning' )->error( 'Invalid API error code "{code}"', [
1312  'code' => $msg->getApiCode(),
1313  'exception' => $e,
1314  ] );
1315  $errorCodes['<invalid-code>'] = true;
1316  }
1317  $formatter->addError( $modulePath, $msg );
1318  }
1319  foreach ( $this->errorMessagesFromException( $e, 'warning' ) as $msg ) {
1320  $formatter->addWarning( $modulePath, $msg );
1321  }
1322 
1323  // Add additional data. Path depends on whether we're in BC mode or not.
1324  // Data depends on the type of exception.
1325  if ( $formatter instanceof ApiErrorFormatter_BackCompat ) {
1326  $path = [ 'error' ];
1327  } else {
1328  $path = null;
1329  }
1330  if ( $e instanceof ApiUsageException ) {
1331  $link = wfExpandUrl( wfScript( 'api' ) );
1332  $result->addContentValue(
1333  $path,
1334  'docref',
1335  trim(
1336  $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text()
1337  . ' '
1338  . $this->msg( 'api-usage-mailinglist-ref' )->inLanguage( $formatter->getLanguage() )->text()
1339  )
1340  );
1341  } elseif ( $config->get( 'ShowExceptionDetails' ) ) {
1342  $result->addContentValue(
1343  $path,
1344  'trace',
1345  $this->msg( 'api-exception-trace',
1346  get_class( $e ),
1347  $e->getFile(),
1348  $e->getLine(),
1350  )->inLanguage( $formatter->getLanguage() )->text()
1351  );
1352  }
1353 
1354  // Add the id and such
1355  $this->addRequestedFields( [ 'servedby' ] );
1356 
1357  return array_keys( $errorCodes );
1358  }
1359 
1365  protected function addRequestedFields( $force = [] ) {
1366  $result = $this->getResult();
1367 
1368  $requestid = $this->getParameter( 'requestid' );
1369  if ( $requestid !== null ) {
1370  $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
1371  }
1372 
1373  if ( $this->getConfig()->get( 'ShowHostnames' ) && (
1374  in_array( 'servedby', $force, true ) || $this->getParameter( 'servedby' )
1375  ) ) {
1376  $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
1377  }
1378 
1379  if ( $this->getParameter( 'curtimestamp' ) ) {
1380  $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601 ), ApiResult::NO_SIZE_CHECK );
1381  }
1382 
1383  if ( $this->getParameter( 'responselanginfo' ) ) {
1384  $result->addValue(
1385  null,
1386  'uselang',
1387  $this->getLanguage()->getCode(),
1389  );
1390  $result->addValue(
1391  null,
1392  'errorlang',
1393  $this->getErrorFormatter()->getLanguage()->getCode(),
1395  );
1396  }
1397  }
1398 
1403  protected function setupExecuteAction() {
1404  $this->addRequestedFields();
1405 
1406  $params = $this->extractRequestParams();
1407  $this->mAction = $params['action'];
1408 
1409  return $params;
1410  }
1411 
1418  protected function setupModule() {
1419  // Instantiate the module requested by the user
1420  $module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
1421  if ( $module === null ) {
1422  // Probably can't happen
1423  // @codeCoverageIgnoreStart
1424  $this->dieWithError(
1425  [ 'apierror-unknownaction', wfEscapeWikiText( $this->mAction ) ],
1426  'unknown_action'
1427  );
1428  // @codeCoverageIgnoreEnd
1429  }
1430  $moduleParams = $module->extractRequestParams();
1431 
1432  // Check token, if necessary
1433  if ( $module->needsToken() === true ) {
1434  throw new MWException(
1435  "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
1436  'See documentation for ApiBase::needsToken for details.'
1437  );
1438  }
1439  if ( $module->needsToken() ) {
1440  if ( !$module->mustBePosted() ) {
1441  throw new MWException(
1442  "Module '{$module->getModuleName()}' must require POST to use tokens."
1443  );
1444  }
1445 
1446  if ( !isset( $moduleParams['token'] ) ) {
1447  // Probably can't happen
1448  // @codeCoverageIgnoreStart
1449  $module->dieWithError( [ 'apierror-missingparam', 'token' ] );
1450  // @codeCoverageIgnoreEnd
1451  }
1452 
1453  $module->requirePostedParameters( [ 'token' ] );
1454 
1455  if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
1456  $module->dieWithError( 'apierror-badtoken' );
1457  }
1458  }
1459 
1460  return $module;
1461  }
1462 
1466  private function getMaxLag() {
1467  $dbLag = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaxLag();
1468  $lagInfo = [
1469  'host' => $dbLag[0],
1470  'lag' => $dbLag[1],
1471  'type' => 'db'
1472  ];
1473 
1474  $jobQueueLagFactor = $this->getConfig()->get( 'JobQueueIncludeInMaxLagFactor' );
1475  if ( $jobQueueLagFactor ) {
1476  // Turn total number of jobs into seconds by using the configured value
1477  $totalJobs = array_sum( JobQueueGroup::singleton()->getQueueSizes() );
1478  $jobQueueLag = $totalJobs / (float)$jobQueueLagFactor;
1479  if ( $jobQueueLag > $lagInfo['lag'] ) {
1480  $lagInfo = [
1481  'host' => wfHostname(), // XXX: Is there a better value that could be used?
1482  'lag' => $jobQueueLag,
1483  'type' => 'jobqueue',
1484  'jobs' => $totalJobs,
1485  ];
1486  }
1487  }
1488 
1489  $this->getHookRunner()->onApiMaxLagInfo( $lagInfo );
1490 
1491  return $lagInfo;
1492  }
1493 
1500  protected function checkMaxLag( $module, $params ) {
1501  if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
1502  $maxLag = $params['maxlag'];
1503  $lagInfo = $this->getMaxLag();
1504  if ( $lagInfo['lag'] > $maxLag ) {
1505  $response = $this->getRequest()->response();
1506 
1507  $response->header( 'Retry-After: ' . max( (int)$maxLag, 5 ) );
1508  $response->header( 'X-Database-Lag: ' . (int)$lagInfo['lag'] );
1509 
1510  if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1511  $this->dieWithError(
1512  [ 'apierror-maxlag', $lagInfo['lag'], $lagInfo['host'] ],
1513  'maxlag',
1514  $lagInfo
1515  );
1516  }
1517 
1518  $this->dieWithError( [ 'apierror-maxlag-generic', $lagInfo['lag'] ], 'maxlag', $lagInfo );
1519  }
1520  }
1521 
1522  return true;
1523  }
1524 
1546  protected function checkConditionalRequestHeaders( $module ) {
1547  if ( $this->mInternalMode ) {
1548  // No headers to check in internal mode
1549  return true;
1550  }
1551 
1552  if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) {
1553  // Don't check POSTs
1554  return true;
1555  }
1556 
1557  $return304 = false;
1558 
1559  $ifNoneMatch = array_diff(
1560  $this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: [],
1561  [ '' ]
1562  );
1563  if ( $ifNoneMatch ) {
1564  // @phan-suppress-next-line PhanImpossibleTypeComparison
1565  if ( $ifNoneMatch === [ '*' ] ) {
1566  // API responses always "exist"
1567  $etag = '*';
1568  } else {
1569  $etag = $module->getConditionalRequestData( 'etag' );
1570  }
1571  }
1572  if ( $ifNoneMatch && $etag !== null ) {
1573  $test = substr( $etag, 0, 2 ) === 'W/' ? substr( $etag, 2 ) : $etag;
1574  $match = array_map( static function ( $s ) {
1575  return substr( $s, 0, 2 ) === 'W/' ? substr( $s, 2 ) : $s;
1576  }, $ifNoneMatch );
1577  $return304 = in_array( $test, $match, true );
1578  } else {
1579  $value = trim( $this->getRequest()->getHeader( 'If-Modified-Since' ) );
1580 
1581  // Some old browsers sends sizes after the date, like this:
1582  // Wed, 20 Aug 2003 06:51:19 GMT; length=5202
1583  // Ignore that.
1584  $i = strpos( $value, ';' );
1585  if ( $i !== false ) {
1586  $value = trim( substr( $value, 0, $i ) );
1587  }
1588 
1589  if ( $value !== '' ) {
1590  try {
1591  $ts = new MWTimestamp( $value );
1592  if (
1593  // RFC 7231 IMF-fixdate
1594  $ts->getTimestamp( TS_RFC2822 ) === $value ||
1595  // RFC 850
1596  $ts->format( 'l, d-M-y H:i:s' ) . ' GMT' === $value ||
1597  // asctime (with and without space-padded day)
1598  $ts->format( 'D M j H:i:s Y' ) === $value ||
1599  $ts->format( 'D M j H:i:s Y' ) === $value
1600  ) {
1601  $config = $this->getConfig();
1602  $lastMod = $module->getConditionalRequestData( 'last-modified' );
1603  if ( $lastMod !== null ) {
1604  // Mix in some MediaWiki modification times
1605  $modifiedTimes = [
1606  'page' => $lastMod,
1607  'user' => $this->getUser()->getTouched(),
1608  'epoch' => $config->get( 'CacheEpoch' ),
1609  ];
1610 
1611  if ( $config->get( 'UseCdn' ) ) {
1612  // T46570: the core page itself may not change, but resources might
1613  $modifiedTimes['sepoch'] = wfTimestamp(
1614  TS_MW, time() - $config->get( 'CdnMaxAge' )
1615  );
1616  }
1617  $this->getHookRunner()->onOutputPageCheckLastModified( $modifiedTimes, $this->getOutput() );
1618  $lastMod = max( $modifiedTimes );
1619  $return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
1620  }
1621  }
1622  } catch ( TimestampException $e ) {
1623  // Invalid timestamp, ignore it
1624  }
1625  }
1626  }
1627 
1628  if ( $return304 ) {
1629  $this->getRequest()->response()->statusHeader( 304 );
1630 
1631  // Avoid outputting the compressed representation of a zero-length body
1632  Wikimedia\suppressWarnings();
1633  ini_set( 'zlib.output_compression', 0 );
1634  Wikimedia\restoreWarnings();
1635  wfResetOutputBuffers( false );
1636 
1637  return false;
1638  }
1639 
1640  return true;
1641  }
1642 
1647  protected function checkExecutePermissions( $module ) {
1648  $user = $this->getUser();
1649  if ( $module->isReadMode() && !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) &&
1650  !$this->getAuthority()->isAllowed( 'read' )
1651  ) {
1652  $this->dieWithError( 'apierror-readapidenied' );
1653  }
1654 
1655  if ( $module->isWriteMode() ) {
1656  if ( !$this->mEnableWrite ) {
1657  $this->dieWithError( 'apierror-noapiwrite' );
1658  } elseif ( !$this->getAuthority()->isAllowed( 'writeapi' ) ) {
1659  $this->dieWithError( 'apierror-writeapidenied' );
1660  } elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) {
1661  $this->dieWithError( 'apierror-promised-nonwrite-api' );
1662  }
1663 
1664  $this->checkReadOnly( $module );
1665  }
1666 
1667  // Allow extensions to stop execution for arbitrary reasons.
1668  // TODO: change hook to accept Authority
1669  $message = 'hookaborted';
1670  if ( !$this->getHookRunner()->onApiCheckCanExecute( $module, $user, $message ) ) {
1671  $this->dieWithError( $message );
1672  }
1673  }
1674 
1679  protected function checkReadOnly( $module ) {
1680  if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
1681  $this->dieReadOnly();
1682  }
1683 
1684  if ( $module->isWriteMode()
1685  && $this->getUser()->isBot()
1686  && MediaWikiServices::getInstance()->getDBLoadBalancer()->getServerCount() > 1
1687  ) {
1688  $this->checkBotReadOnly();
1689  }
1690  }
1691 
1695  private function checkBotReadOnly() {
1696  // Figure out how many servers have passed the lag threshold
1697  $numLagged = 0;
1698  $lagLimit = $this->getConfig()->get( 'APIMaxLagThreshold' );
1699  $laggedServers = [];
1700  $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
1701  foreach ( $loadBalancer->getLagTimes() as $serverIndex => $lag ) {
1702  if ( $lag > $lagLimit ) {
1703  ++$numLagged;
1704  $laggedServers[] = $loadBalancer->getServerName( $serverIndex ) . " ({$lag}s)";
1705  }
1706  }
1707 
1708  // If a majority of replica DBs are too lagged then disallow writes
1709  $replicaCount = $loadBalancer->getServerCount() - 1;
1710  if ( $numLagged >= ceil( $replicaCount / 2 ) ) {
1711  $laggedServers = implode( ', ', $laggedServers );
1712  wfDebugLog(
1713  'api-readonly', // Deprecate this channel in favor of api-warning?
1714  "Api request failed as read only because the following DBs are lagged: $laggedServers"
1715  );
1716  LoggerFactory::getInstance( 'api-warning' )->warning(
1717  "Api request failed as read only because the following DBs are lagged: {laggeddbs}", [
1718  'laggeddbs' => $laggedServers,
1719  ]
1720  );
1721 
1722  $this->dieWithError(
1723  'readonly_lag',
1724  'readonly',
1725  [ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ]
1726  );
1727  }
1728  }
1729 
1734  protected function checkAsserts( $params ) {
1735  if ( isset( $params['assert'] ) ) {
1736  $user = $this->getUser();
1737  switch ( $params['assert'] ) {
1738  case 'anon':
1739  if ( $user->isRegistered() ) {
1740  $this->dieWithError( 'apierror-assertanonfailed' );
1741  }
1742  break;
1743  case 'user':
1744  if ( !$user->isRegistered() ) {
1745  $this->dieWithError( 'apierror-assertuserfailed' );
1746  }
1747  break;
1748  case 'bot':
1749  if ( !$this->getAuthority()->isAllowed( 'bot' ) ) {
1750  $this->dieWithError( 'apierror-assertbotfailed' );
1751  }
1752  break;
1753  }
1754  }
1755  if ( isset( $params['assertuser'] ) ) {
1756  // TODO inject stuff, see T265644
1757  $assertUser = MediaWikiServices::getInstance()->getUserFactory()
1758  ->newFromName( $params['assertuser'], UserFactory::RIGOR_NONE );
1759  if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) {
1760  $this->dieWithError(
1761  [ 'apierror-assertnameduserfailed', wfEscapeWikiText( $params['assertuser'] ) ]
1762  );
1763  }
1764  }
1765  }
1766 
1772  protected function setupExternalResponse( $module, $params ) {
1773  $validMethods = [ 'GET', 'HEAD', 'POST', 'OPTIONS' ];
1774  $request = $this->getRequest();
1775 
1776  if ( !in_array( $request->getMethod(), $validMethods ) ) {
1777  $this->dieWithError( 'apierror-invalidmethod', null, null, 405 );
1778  }
1779 
1780  if ( !$request->wasPosted() && $module->mustBePosted() ) {
1781  // Module requires POST. GET request might still be allowed
1782  // if $wgDebugApi is true, otherwise fail.
1783  $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] );
1784  }
1785 
1786  if ( $request->wasPosted() && !$request->getHeader( 'Content-Type' ) ) {
1787  $this->addDeprecation(
1788  'apiwarn-deprecation-post-without-content-type', 'post-without-content-type'
1789  );
1790  }
1791 
1792  // See if custom printer is used
1793  $this->mPrinter = $module->getCustomPrinter();
1794  if ( $this->mPrinter === null ) {
1795  // Create an appropriate printer
1796  $this->mPrinter = $this->createPrinterByName( $params['format'] );
1797  }
1798 
1799  if ( $request->getProtocol() === 'http' &&
1800  (
1801  $this->getConfig()->get( 'ForceHTTPS' ) ||
1802  $request->getSession()->shouldForceHTTPS() ||
1803  ( $this->getUser()->isRegistered() &&
1804  $this->getUser()->requiresHTTPS() )
1805  )
1806  ) {
1807  $this->addDeprecation( 'apiwarn-deprecation-httpsexpected', 'https-expected' );
1808  }
1809  }
1810 
1814  protected function executeAction() {
1815  $params = $this->setupExecuteAction();
1816 
1817  // Check asserts early so e.g. errors in parsing a module's parameters due to being
1818  // logged out don't override the client's intended "am I logged in?" check.
1819  $this->checkAsserts( $params );
1820 
1821  $module = $this->setupModule();
1822  $this->mModule = $module;
1823 
1824  if ( !$this->mInternalMode ) {
1825  $this->setRequestExpectations( $module );
1826  }
1827 
1828  $this->checkExecutePermissions( $module );
1829 
1830  if ( !$this->checkMaxLag( $module, $params ) ) {
1831  return;
1832  }
1833 
1834  if ( !$this->checkConditionalRequestHeaders( $module ) ) {
1835  return;
1836  }
1837 
1838  if ( !$this->mInternalMode ) {
1839  $this->setupExternalResponse( $module, $params );
1840  }
1841 
1842  $module->execute();
1843  $this->getHookRunner()->onAPIAfterExecute( $module );
1844 
1845  $this->reportUnusedParams();
1846 
1847  if ( !$this->mInternalMode ) {
1849 
1850  $this->printResult();
1851  }
1852  }
1853 
1858  protected function setRequestExpectations( ApiBase $module ) {
1859  $limits = $this->getConfig()->get( 'TrxProfilerLimits' );
1860  $trxProfiler = Profiler::instance()->getTransactionProfiler();
1861  $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
1862  if ( $this->getRequest()->hasSafeMethod() ) {
1863  $trxProfiler->setExpectations( $limits['GET'], __METHOD__ );
1864  } elseif ( $this->getRequest()->wasPosted() && !$module->isWriteMode() ) {
1865  $trxProfiler->setExpectations( $limits['POST-nonwrite'], __METHOD__ );
1866  $this->getRequest()->markAsSafeRequest();
1867  } else {
1868  $trxProfiler->setExpectations( $limits['POST'], __METHOD__ );
1869  }
1870  }
1871 
1877  protected function logRequest( $time, Throwable $e = null ) {
1878  $request = $this->getRequest();
1879 
1880  $user = $this->getUser();
1881  $performer = [
1882  'user_text' => $user->getName(),
1883  ];
1884  if ( $user->isRegistered() ) {
1885  $performer['user_id'] = $user->getId();
1886  }
1887  $logCtx = [
1888  // https://gerrit.wikimedia.org/g/mediawiki/event-schemas/+/master/jsonschema/mediawiki/api/request
1889  '$schema' => '/mediawiki/api/request/1.0.0',
1890  'meta' => [
1891  'request_id' => WebRequest::getRequestId(),
1892  'id' => MediaWikiServices::getInstance()
1893  ->getGlobalIdGenerator()->newUUIDv4(),
1894  'dt' => wfTimestamp( TS_ISO_8601 ),
1895  'domain' => $this->getConfig()->get( 'ServerName' ),
1896  // If using the EventBus extension (as intended) with this log channel,
1897  // this stream name will map to a Kafka topic.
1898  'stream' => 'mediawiki.api-request'
1899  ],
1900  'http' => [
1901  'method' => $request->getMethod(),
1902  'client_ip' => $request->getIP()
1903  ],
1904  'performer' => $performer,
1905  'database' => WikiMap::getCurrentWikiDbDomain()->getId(),
1906  'backend_time_ms' => (int)round( $time * 1000 ),
1907  ];
1908 
1909  // If set, these headers will be logged in http.request_headers.
1910  $httpRequestHeadersToLog = [ 'accept-language', 'referer', 'user-agent' ];
1911  foreach ( $httpRequestHeadersToLog as $header ) {
1912  if ( $request->getHeader( $header ) ) {
1913  // Set the header in http.request_headers
1914  $logCtx['http']['request_headers'][$header] = $request->getHeader( $header );
1915  }
1916  }
1917 
1918  if ( $e ) {
1919  $logCtx['api_error_codes'] = [];
1920  foreach ( $this->errorMessagesFromException( $e ) as $msg ) {
1921  $logCtx['api_error_codes'][] = $msg->getApiCode();
1922  }
1923  }
1924 
1925  // Construct space separated message for 'api' log channel
1926  $msg = "API {$request->getMethod()} " .
1927  wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
1928  " {$logCtx['http']['client_ip']} " .
1929  "T={$logCtx['backend_time_ms']}ms";
1930 
1931  $sensitive = array_fill_keys( $this->getSensitiveParams(), true );
1932  foreach ( $this->getParamsUsed() as $name ) {
1933  $value = $request->getVal( $name );
1934  if ( $value === null ) {
1935  continue;
1936  }
1937 
1938  if ( isset( $sensitive[$name] ) ) {
1939  $value = '[redacted]';
1940  $encValue = '[redacted]';
1941  } elseif ( strlen( $value ) > 256 ) {
1942  $value = substr( $value, 0, 256 );
1943  $encValue = $this->encodeRequestLogValue( $value ) . '[...]';
1944  } else {
1945  $encValue = $this->encodeRequestLogValue( $value );
1946  }
1947 
1948  $logCtx['params'][$name] = $value;
1949  $msg .= " {$name}={$encValue}";
1950  }
1951 
1952  // Log an unstructured message to the api channel.
1953  wfDebugLog( 'api', $msg, 'private' );
1954 
1955  // The api-request channel a structured data log channel.
1956  wfDebugLog( 'api-request', '', 'private', $logCtx );
1957  }
1958 
1964  protected function encodeRequestLogValue( $s ) {
1965  static $table = [];
1966  if ( !$table ) {
1967  $chars = ';@$!*(),/:';
1968  $numChars = strlen( $chars );
1969  for ( $i = 0; $i < $numChars; $i++ ) {
1970  $table[rawurlencode( $chars[$i] )] = $chars[$i];
1971  }
1972  }
1973 
1974  return strtr( rawurlencode( $s ), $table );
1975  }
1976 
1981  protected function getParamsUsed() {
1982  return array_keys( $this->mParamsUsed );
1983  }
1984 
1989  public function markParamsUsed( $params ) {
1990  $this->mParamsUsed += array_fill_keys( (array)$params, true );
1991  }
1992 
1998  protected function getSensitiveParams() {
1999  return array_keys( $this->mParamsSensitive );
2000  }
2001 
2011  public function markParamsSensitive( $params ) {
2012  $this->mParamsSensitive += array_fill_keys( (array)$params, true );
2013  }
2014 
2021  public function getVal( $name, $default = null ) {
2022  $this->mParamsUsed[$name] = true;
2023 
2024  $ret = $this->getRequest()->getVal( $name );
2025  if ( $ret === null ) {
2026  if ( $this->getRequest()->getArray( $name ) !== null ) {
2027  // See T12262 for why we don't just implode( '|', ... ) the
2028  // array.
2029  $this->addWarning( [ 'apiwarn-unsupportedarray', $name ] );
2030  }
2031  $ret = $default;
2032  }
2033  return $ret;
2034  }
2035 
2042  public function getCheck( $name ) {
2043  $this->mParamsUsed[$name] = true;
2044  return $this->getRequest()->getCheck( $name );
2045  }
2046 
2054  public function getUpload( $name ) {
2055  $this->mParamsUsed[$name] = true;
2056 
2057  return $this->getRequest()->getUpload( $name );
2058  }
2059 
2064  protected function reportUnusedParams() {
2065  $paramsUsed = $this->getParamsUsed();
2066  $allParams = $this->getRequest()->getValueNames();
2067 
2068  if ( !$this->mInternalMode ) {
2069  // Printer has not yet executed; don't warn that its parameters are unused
2070  $printerParams = $this->mPrinter->encodeParamName(
2071  array_keys( $this->mPrinter->getFinalParams() ?: [] )
2072  );
2073  $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
2074  } else {
2075  $unusedParams = array_diff( $allParams, $paramsUsed );
2076  }
2077 
2078  if ( count( $unusedParams ) ) {
2079  $this->addWarning( [
2080  'apierror-unrecognizedparams',
2081  Message::listParam( array_map( 'wfEscapeWikiText', $unusedParams ), 'comma' ),
2082  count( $unusedParams )
2083  ] );
2084  }
2085  }
2086 
2092  protected function printResult( $httpCode = 0 ) {
2093  if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
2094  $this->addWarning( 'apiwarn-wgdebugapi' );
2095  }
2096 
2097  $printer = $this->mPrinter;
2098  $printer->initPrinter( false );
2099  if ( $httpCode ) {
2100  $printer->setHttpStatus( $httpCode );
2101  }
2102  $printer->execute();
2103  $printer->closePrinter();
2104  }
2105 
2109  public function isReadMode() {
2110  return false;
2111  }
2112 
2118  public function getAllowedParams() {
2119  return [
2120  'action' => [
2121  ApiBase::PARAM_DFLT => 'help',
2122  ApiBase::PARAM_TYPE => 'submodule',
2123  ],
2124  'format' => [
2126  ApiBase::PARAM_TYPE => 'submodule',
2127  ],
2128  'maxlag' => [
2129  ApiBase::PARAM_TYPE => 'integer'
2130  ],
2131  'smaxage' => [
2132  ApiBase::PARAM_TYPE => 'integer',
2133  ApiBase::PARAM_DFLT => 0
2134  ],
2135  'maxage' => [
2136  ApiBase::PARAM_TYPE => 'integer',
2137  ApiBase::PARAM_DFLT => 0
2138  ],
2139  'assert' => [
2140  ApiBase::PARAM_TYPE => [ 'anon', 'user', 'bot' ]
2141  ],
2142  'assertuser' => [
2143  ApiBase::PARAM_TYPE => 'user',
2144  UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
2145  ],
2146  'requestid' => null,
2147  'servedby' => false,
2148  'curtimestamp' => false,
2149  'responselanginfo' => false,
2150  'origin' => null,
2151  'uselang' => [
2153  ],
2154  'errorformat' => [
2155  ApiBase::PARAM_TYPE => [ 'plaintext', 'wikitext', 'html', 'raw', 'none', 'bc' ],
2156  ApiBase::PARAM_DFLT => 'bc',
2158  ],
2159  'errorlang' => [
2160  ApiBase::PARAM_DFLT => 'uselang',
2161  ],
2162  'errorsuselocal' => [
2163  ApiBase::PARAM_DFLT => false,
2164  ],
2165  ];
2166  }
2167 
2169  protected function getExamplesMessages() {
2170  return [
2171  'action=help'
2172  => 'apihelp-help-example-main',
2173  'action=help&recursivesubmodules=1'
2174  => 'apihelp-help-example-recursive',
2175  ];
2176  }
2177 
2182  public function modifyHelp( array &$help, array $options, array &$tocData ) {
2183  // Wish PHP had an "array_insert_before". Instead, we have to manually
2184  // reindex the array to get 'permissions' in the right place.
2185  $oldHelp = $help;
2186  $help = [];
2187  foreach ( $oldHelp as $k => $v ) {
2188  if ( $k === 'submodules' ) {
2189  $help['permissions'] = '';
2190  }
2191  $help[$k] = $v;
2192  }
2193  $help['datatypes'] = '';
2194  $help['templatedparams'] = '';
2195  $help['credits'] = '';
2196 
2197  // Fill 'permissions'
2198  $help['permissions'] .= Html::openElement( 'div',
2199  [ 'class' => 'apihelp-block apihelp-permissions' ] );
2200  $m = $this->msg( 'api-help-permissions' );
2201  if ( !$m->isDisabled() ) {
2202  $help['permissions'] .= Html::rawElement( 'div', [ 'class' => 'apihelp-block-head' ],
2203  $m->numParams( count( self::RIGHTS_MAP ) )->parse()
2204  );
2205  }
2206  $help['permissions'] .= Html::openElement( 'dl' );
2207  // TODO inject stuff, see T265644
2208  $groupPermissionsLookup = MediaWikiServices::getInstance()->getGroupPermissionsLookup();
2209  foreach ( self::RIGHTS_MAP as $right => $rightMsg ) {
2210  $help['permissions'] .= Html::element( 'dt', null, $right );
2211 
2212  $rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
2213  $help['permissions'] .= Html::rawElement( 'dd', null, $rightMsg );
2214 
2215  $groups = array_map( static function ( $group ) {
2216  return $group == '*' ? 'all' : $group;
2217  }, $groupPermissionsLookup->getGroupsWithPermission( $right ) );
2218 
2219  $help['permissions'] .= Html::rawElement( 'dd', null,
2220  $this->msg( 'api-help-permissions-granted-to' )
2221  ->numParams( count( $groups ) )
2222  ->params( Message::listParam( $groups ) )
2223  ->parse()
2224  );
2225  }
2226  $help['permissions'] .= Html::closeElement( 'dl' );
2227  $help['permissions'] .= Html::closeElement( 'div' );
2228 
2229  // Fill 'datatypes', 'templatedparams', and 'credits', if applicable
2230  if ( empty( $options['nolead'] ) ) {
2231  $level = $options['headerlevel'];
2232  $tocnumber = &$options['tocnumber'];
2233 
2234  $header = $this->msg( 'api-help-datatypes-header' )->parse();
2235 
2236  $id = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_PRIMARY );
2237  $idFallback = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_FALLBACK );
2238  $headline = Linker::makeHeadline( min( 6, $level ),
2239  ' class="apihelp-header">',
2240  $id,
2241  $header,
2242  '',
2243  $idFallback
2244  );
2245  // Ensure we have a sane anchor
2246  if ( $id !== 'main/datatypes' && $idFallback !== 'main/datatypes' ) {
2247  $headline = '<div id="main/datatypes"></div>' . $headline;
2248  }
2249  $help['datatypes'] .= $headline;
2250  $help['datatypes'] .= $this->msg( 'api-help-datatypes-top' )->parseAsBlock();
2251  $help['datatypes'] .= '<dl>';
2252  foreach ( $this->getParamValidator()->knownTypes() as $type ) {
2253  $m = $this->msg( "api-help-datatype-$type" );
2254  if ( !$m->isDisabled() ) {
2255  $id = "main/datatype/$type";
2256  $help['datatypes'] .= '<dt id="' . htmlspecialchars( $id ) . '">';
2258  if ( $encId !== $id ) {
2259  $help['datatypes'] .= '<span id="' . htmlspecialchars( $encId ) . '"></span>';
2260  }
2262  if ( $encId2 !== $id && $encId2 !== $encId ) {
2263  $help['datatypes'] .= '<span id="' . htmlspecialchars( $encId2 ) . '"></span>';
2264  }
2265  $help['datatypes'] .= htmlspecialchars( $type ) . '</dt><dd>' . $m->parseAsBlock() . "</dd>";
2266  }
2267  }
2268  $help['datatypes'] .= '</dl>';
2269  if ( !isset( $tocData['main/datatypes'] ) ) {
2270  $tocnumber[$level]++;
2271  $tocData['main/datatypes'] = [
2272  'toclevel' => count( $tocnumber ),
2273  'level' => $level,
2274  'anchor' => 'main/datatypes',
2275  'line' => $header,
2276  'number' => implode( '.', $tocnumber ),
2277  'index' => false,
2278  ];
2279  }
2280 
2281  $header = $this->msg( 'api-help-templatedparams-header' )->parse();
2282 
2283  $id = Sanitizer::escapeIdForAttribute( 'main/templatedparams', Sanitizer::ID_PRIMARY );
2284  $idFallback = Sanitizer::escapeIdForAttribute( 'main/templatedparams', Sanitizer::ID_FALLBACK );
2285  $headline = Linker::makeHeadline( min( 6, $level ),
2286  ' class="apihelp-header">',
2287  $id,
2288  $header,
2289  '',
2290  $idFallback
2291  );
2292  // Ensure we have a sane anchor
2293  if ( $id !== 'main/templatedparams' && $idFallback !== 'main/templatedparams' ) {
2294  $headline = '<div id="main/templatedparams"></div>' . $headline;
2295  }
2296  $help['templatedparams'] .= $headline;
2297  $help['templatedparams'] .= $this->msg( 'api-help-templatedparams' )->parseAsBlock();
2298  if ( !isset( $tocData['main/templatedparams'] ) ) {
2299  $tocnumber[$level]++;
2300  $tocData['main/templatedparams'] = [
2301  'toclevel' => count( $tocnumber ),
2302  'level' => $level,
2303  'anchor' => 'main/templatedparams',
2304  'line' => $header,
2305  'number' => implode( '.', $tocnumber ),
2306  'index' => false,
2307  ];
2308  }
2309 
2310  $header = $this->msg( 'api-credits-header' )->parse();
2311  $id = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_PRIMARY );
2312  $idFallback = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_FALLBACK );
2313  $headline = Linker::makeHeadline( min( 6, $level ),
2314  ' class="apihelp-header">',
2315  $id,
2316  $header,
2317  '',
2318  $idFallback
2319  );
2320  // Ensure we have a sane anchor
2321  if ( $id !== 'main/credits' && $idFallback !== 'main/credits' ) {
2322  $headline = '<div id="main/credits"></div>' . $headline;
2323  }
2324  $help['credits'] .= $headline;
2325  $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
2326  if ( !isset( $tocData['main/credits'] ) ) {
2327  $tocnumber[$level]++;
2328  $tocData['main/credits'] = [
2329  'toclevel' => count( $tocnumber ),
2330  'level' => $level,
2331  'anchor' => 'main/credits',
2332  'line' => $header,
2333  'number' => implode( '.', $tocnumber ),
2334  'index' => false,
2335  ];
2336  }
2337  }
2338  }
2339 
2340  private $mCanApiHighLimits = null;
2341 
2346  public function canApiHighLimits() {
2347  if ( !isset( $this->mCanApiHighLimits ) ) {
2348  $this->mCanApiHighLimits = $this->getAuthority()->isAllowed( 'apihighlimits' );
2349  }
2350 
2351  return $this->mCanApiHighLimits;
2352  }
2353 
2358  public function getModuleManager() {
2359  return $this->mModuleMgr;
2360  }
2361 
2370  public function getUserAgent() {
2371  return trim(
2372  $this->getRequest()->getHeader( 'Api-user-agent' ) . ' ' .
2373  $this->getRequest()->getHeader( 'User-agent' )
2374  );
2375  }
2376 }
2377 
ApiUsageException\getStatusValue
getStatusValue()
Fetch the error status.
Definition: ApiUsageException.php:107
ApiMain\$mModuleMgr
ApiModuleManager $mModuleMgr
Definition: ApiMain.php:421
ApiMain\executeActionWithErrorHandling
executeActionWithErrorHandling()
Execute an action, and in case of an error, erase whatever partial results have been accumulated,...
Definition: ApiMain.php:800
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:38
ApiMain
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:49
ContextSource\$context
IContextSource $context
Definition: ContextSource.php:39
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:72
Sanitizer\ID_FALLBACK
const ID_FALLBACK
Tells escapeUrlForHtml() to encode the ID using the fallback encoding, or return false if no fallback...
Definition: Sanitizer.php:78
Message\newFromSpecifier
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition: Message.php:406
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:35
ApiUsageException
Exception used to abort API execution with an error.
Definition: ApiUsageException.php:29
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:47
wfResetOutputBuffers
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
Definition: GlobalFunctions.php:1604
ApiMain\substituteResultWithError
substituteResultWithError(Throwable $e)
Replace the result data with the information about a throwable.
Definition: ApiMain.php:1281
ApiMain\checkReadOnly
checkReadOnly( $module)
Check if the DB is read-only for this user.
Definition: ApiMain.php:1679
ApiBase\addWarning
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition: ApiBase.php:1293
RequestContext\sanitizeLangCode
static sanitizeLangCode( $code)
Accepts a language code and ensures it's sane.
Definition: RequestContext.php:327
WikiMap\getCurrentWikiDbDomain
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
ApiErrorFormatter_BackCompat
Format errors and warnings in the old style, for backwards compatibility.
Definition: ApiErrorFormatter_BackCompat.php:30
ApiErrorFormatter\isValidApiCode
static isValidApiCode( $code)
Test whether a code is a valid API error code.
Definition: ApiErrorFormatter.php:79
ApiMain\getParamsUsed
getParamsUsed()
Get the request parameters used in the course of the preceding execute() request.
Definition: ApiMain.php:1981
Profiler\instance
static instance()
Singleton.
Definition: Profiler.php:69
ApiMain\MODULES
const MODULES
List of available modules: action name => module class.
Definition: ApiMain.php:63
ApiMain\createErrorPrinter
createErrorPrinter()
Create the printer for error output.
Definition: ApiMain.php:1212
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:186
ApiMain\getErrorFormatter
getErrorFormatter()
Get the ApiErrorFormatter object associated with current request.
Definition: ApiMain.php:635
ApiMain\getVal
getVal( $name, $default=null)
Get a request value, and register the fact that it was used, for logging.
Definition: ApiMain.php:2021
ApiMain\errorMessagesFromException
errorMessagesFromException(Throwable $e, $type='error')
Create an error message for the given throwable.
Definition: ApiMain.php:1243
ApiUsageException\getModulePath
getModulePath()
Fetch the responsible module name.
Definition: ApiUsageException.php:99
ApiContinuationManager
This manages continuation state.
Definition: ApiContinuationManager.php:26
Sanitizer\escapeIdForAttribute
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:811
ApiBase\dieWithError
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition: ApiBase.php:1374
ApiMain\$mEnableWrite
bool $mEnableWrite
Definition: ApiMain.php:439
ApiMain\sendCacheHeaders
sendCacheHeaders( $isError)
Send caching headers.
Definition: ApiMain.php:1106
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1692
ApiBase\PARAM_TYPE
const PARAM_TYPE
Definition: ApiBase.php:72
ApiFormatBase
This is the abstract base class for API formatters.
Definition: ApiFormatBase.php:30
ApiMain\handleException
handleException(Throwable $e)
Handle a throwable as an API response.
Definition: ApiMain.php:866
MessageSpecifier
Definition: MessageSpecifier.php:24
wfUrlencode
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
Definition: GlobalFunctions.php:292
ApiMain\isReadMode
isReadMode()
Definition: ApiMain.php:2109
ApiMain\handleCORS
handleCORS()
Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
Definition: ApiMain.php:972
MediaWiki\Api\Validator\ApiParamValidator
This wraps a bunch of the API-specific parameter validation logic.
Definition: ApiParamValidator.php:38
ApiMain\checkConditionalRequestHeaders
checkConditionalRequestHeaders( $module)
Check selected RFC 7232 precondition headers.
Definition: ApiMain.php:1546
ApiBase\dieWithErrorOrDebug
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:1539
ApiMain\lacksSameOriginSecurity
lacksSameOriginSecurity()
Get the security flag for the current request.
Definition: ApiMain.php:599
wfHostname
wfHostname()
Get host name of the current machine, for use in error reporting.
Definition: GlobalFunctions.php:1245
ApiMain\getParamValidator
getParamValidator()
Get the parameter validator.
Definition: ApiMain.php:663
ApiResult\NO_SIZE_CHECK
const NO_SIZE_CHECK
For addValue() and similar functions, do not check size while adding a value Don't use this unless yo...
Definition: ApiResult.php:58
ContextSource\getRequest
getRequest()
Definition: ContextSource.php:81
ApiMain\$mCacheControl
array $mCacheControl
Definition: ApiMain.php:451
ApiMain\encodeRequestLogValue
encodeRequestLogValue( $s)
Encode a value in a format suitable for a space-separated log line.
Definition: ApiMain.php:1964
ApiMain\$mResult
ApiResult $mResult
Definition: ApiMain.php:424
ContextSource\getUser
getUser()
Definition: ContextSource.php:136
ApiMain\getMaxLag
getMaxLag()
Definition: ApiMain.php:1466
ApiMain\$mContinuationManager
ApiContinuationManager null $mContinuationManager
Definition: ApiMain.php:433
$wgLang
$wgLang
Definition: Setup.php:807
wfDebugLog
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Definition: GlobalFunctions.php:958
Message\listParam
static listParam(array $list, $type='text')
Definition: Message.php:1213
ApiMain\FORMATS
const FORMATS
List of available formats: format name => format class.
Definition: ApiMain.php:389
ApiBase
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:55
ApiMain\$mErrorFormatter
ApiErrorFormatter $mErrorFormatter
Definition: ApiMain.php:427
Wikimedia\ParamValidator\ParamValidator::TypeDef\UserDef
Type definition for user types.
Definition: UserDef.php:25
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:153
ApiMain\matchRequestedHeaders
static matchRequestedHeaders( $requestedHeaders, $allowedHeaders)
Attempt to validate the value of Access-Control-Request-Headers against a list of headers that we all...
Definition: ApiMain.php:1080
Html\closeElement
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:318
ApiMain\getModule
getModule()
Get the API module object.
Definition: ApiMain.php:672
MWExceptionHandler\getRedactedTraceAsString
static getRedactedTraceAsString(Throwable $e)
Generate a string representation of a throwable's stack trace.
Definition: MWExceptionHandler.php:359
DerivativeContext
An IContextSource implementation which will inherit context from another source but allow individual ...
Definition: DerivativeContext.php:33
ApiMain\markParamsUsed
markParamsUsed( $params)
Mark parameters as used.
Definition: ApiMain.php:1989
MWException
MediaWiki exception.
Definition: MWException.php:29
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
ApiMain\setupExecuteAction
setupExecuteAction()
Set up for the execution.
Definition: ApiMain.php:1403
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2306
ApiResult
This class represents the result of the API operations.
Definition: ApiResult.php:35
ApiMain\$lacksSameOriginSecurity
bool null $lacksSameOriginSecurity
Cached return value from self::lacksSameOriginSecurity()
Definition: ApiMain.php:460
ApiMain\$mParamsSensitive
array $mParamsSensitive
Definition: ApiMain.php:457
ContextSource\getOutput
getOutput()
Definition: ContextSource.php:126
Linker\makeHeadline
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition: Linker.php:1974
ApiMain\getResult
getResult()
Get the ApiResult object associated with current request.
Definition: ApiMain.php:591
ApiMain\getSensitiveParams
getSensitiveParams()
Get the request parameters that should be considered sensitive.
Definition: ApiMain.php:1998
ApiMain\checkMaxLag
checkMaxLag( $module, $params)
Check the max lag if necessary.
Definition: ApiMain.php:1500
ApiMain\API_DEFAULT_USELANG
const API_DEFAULT_USELANG
When no uselang parameter is given, this language will be used.
Definition: ApiMain.php:58
ApiBase\extractRequestParams
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:707
ApiMain\getExamplesMessages
getExamplesMessages()
Returns usage examples for this module.Return value has query strings as keys, with values being eith...
Definition: ApiMain.php:2169
ApiMain\setCacheControl
setCacheControl( $directives)
Set directives (key/value pairs) for the Cache-Control header.
Definition: ApiMain.php:763
ApiMain\getAllowedParams
getAllowedParams()
See ApiBase for description.
Definition: ApiMain.php:2118
ApiMain\setupExternalResponse
setupExternalResponse( $module, $params)
Check POST for external response and setup result printer.
Definition: ApiMain.php:1772
ApiMain\createPrinterByName
createPrinterByName( $format)
Create an instance of an output formatter by its name.
Definition: ApiMain.php:774
ApiMain\getModuleManager
getModuleManager()
Overrides to return this instance's module manager.
Definition: ApiMain.php:2358
ApiMain\$mCanApiHighLimits
$mCanApiHighLimits
Definition: ApiMain.php:2340
ApiMessage\create
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:43
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:894
ApiMain\addRequestedFields
addRequestedFields( $force=[])
Add requested fields to the result.
Definition: ApiMain.php:1365
ContextSource\setContext
setContext(IContextSource $context)
Definition: ContextSource.php:63
ILocalizedException
Interface for MediaWiki-localized exceptions.
Definition: ILocalizedException.php:29
ApiMain\canApiHighLimits
canApiHighLimits()
Check whether the current user is allowed to use high limits.
Definition: ApiMain.php:2346
ApiMain\$mPrinter
ApiFormatBase $mPrinter
Definition: ApiMain.php:418
ApiMain\checkBotReadOnly
checkBotReadOnly()
Check whether we are readonly for bots.
Definition: ApiMain.php:1695
ApiMain\getContinuationManager
getContinuationManager()
Definition: ApiMain.php:642
ApiModuleManager
This class holds a list of modules and handles instantiation.
Definition: ApiModuleManager.php:33
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:197
ApiMain\RIGHTS_MAP
const RIGHTS_MAP
List of user roles that are specifically relevant to the API.
Definition: ApiMain.php:406
ApiMain\checkAsserts
checkAsserts( $params)
Check asserts of the user's rights.
Definition: ApiMain.php:1734
MWDebug\appendDebugInfoToApiResult
static appendDebugInfoToApiResult(IContextSource $context, ApiResult $result)
Append the debug info to given ApiResult.
Definition: MWDebug.php:670
$s
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
Definition: mergeMessageFileList.php:206
ApiMain\setContinuationManager
setContinuationManager(ApiContinuationManager $manager=null)
Definition: ApiMain.php:649
MediaWiki\preOutputCommit
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:668
ApiMain\checkExecutePermissions
checkExecutePermissions( $module)
Check for sufficient permissions to execute.
Definition: ApiMain.php:1647
ApiBase\addDeprecation
addDeprecation( $msg, $feature, $data=[])
Add a deprecation warning for this module.
Definition: ApiBase.php:1307
ContextSource\getAuthority
getAuthority()
Definition: ContextSource.php:144
$header
$header
Definition: updateCredits.php:37
ApiBase\LIMIT_SML2
const LIMIT_SML2
Slow query, apihighlimits limit.
Definition: ApiBase.php:169
ApiBase\dieReadOnly
dieReadOnly()
Helper function for readonly errors.
Definition: ApiBase.php:1460
MediaWiki\Session\SessionManager
This serves as the entry point to the MediaWiki session handling system.
Definition: SessionManager.php:83
ApiMain\__construct
__construct( $context=null, $enableWrite=false)
Constructs an instance of ApiMain that utilizes the module and format specified by $request.
Definition: ApiMain.php:470
ApiMain\reportUnusedParams
reportUnusedParams()
Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,...
Definition: ApiMain.php:2064
ApiBase\getPermissionManager
getPermissionManager()
Obtain a PermissionManager instance that subclasses may use in their authorization checks.
Definition: ApiBase.php:628
ApiMain\execute
execute()
Execute api request.
Definition: ApiMain.php:788
ApiBase\isWriteMode
isWriteMode()
Indicates whether this module requires write mode.
Definition: ApiBase.php:342
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1456
ApiMain\$mInternalMode
bool $mInternalMode
Definition: ApiMain.php:442
ApiMain\markParamsSensitive
markParamsSensitive( $params)
Mark parameters as sensitive.
Definition: ApiMain.php:2011
ApiMain\setRequestExpectations
setRequestExpectations(ApiBase $module)
Set database connection, query, and write expectations given this module request.
Definition: ApiMain.php:1858
ApiMain\$mParamValidator
ApiParamValidator $mParamValidator
Definition: ApiMain.php:430
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:484
ApiMain\logRequest
logRequest( $time, Throwable $e=null)
Log the preceding request.
Definition: ApiMain.php:1877
ApiMain\getCheck
getCheck( $name)
Get a boolean request value, and register the fact that the parameter was used, for logging.
Definition: ApiMain.php:2042
ApiMain\printResult
printResult( $httpCode=0)
Print results using the current printer.
Definition: ApiMain.php:2092
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:42
ApiErrorFormatter
Formats errors and warnings for the API, and add them to the associated ApiResult.
Definition: ApiErrorFormatter.php:34
ApiMain\handleApiBeforeMainException
static handleApiBeforeMainException(Throwable $e)
Handle a throwable from the ApiBeforeMain hook.
Definition: ApiMain.php:940
MWExceptionHandler\rollbackMasterChangesAndLog
static rollbackMasterChangesAndLog(Throwable $e, $catcher=self::CAUGHT_BY_OTHER)
Roll back any open database transactions and log the stack trace of the throwable.
Definition: MWExceptionHandler.php:126
JobQueueGroup\singleton
static singleton( $domain=false)
Definition: JobQueueGroup.php:114
ApiBase\LIMIT_BIG2
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition: ApiBase.php:165
ApiMain\API_DEFAULT_FORMAT
const API_DEFAULT_FORMAT
When no format parameter is given, this format will be used.
Definition: ApiMain.php:53
WebRequest\GETHEADER_LIST
const GETHEADER_LIST
Flag to make WebRequest::getHeader return an array of values.
Definition: WebRequest.php:72
ApiMain\getUpload
getUpload( $name)
Get a request upload, and register the fact that it was used, for logging.
Definition: ApiMain.php:2054
ApiMain\$mAction
string null $mAction
Definition: ApiMain.php:436
ApiMain\modifyHelp
modifyHelp(array &$help, array $options, array &$tocData)
Called from ApiHelp before the pieces are joined together and returned.This exists mainly for ApiMain...
Definition: ApiMain.php:2182
Sanitizer\ID_PRIMARY
const ID_PRIMARY
Tells escapeUrlForHtml() to encode the ID using the wiki's primary encoding.
Definition: Sanitizer.php:70
WebRequest\getRequestId
static getRequestId()
Get the current request ID.
Definition: WebRequest.php:330
$path
$path
Definition: NoLocalSettings.php:25
ApiBase\PARAM_DFLT
const PARAM_DFLT
Definition: ApiBase.php:70
ApiBase\getParameter
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition: ApiBase.php:827
Html\openElement
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:254
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:212
ApiMain\executeAction
executeAction()
Execute the actual module, without any error handling.
Definition: ApiMain.php:1814
ApiMain\setupModule
setupModule()
Set up the module for response.
Definition: ApiMain.php:1418
$help
$help
Definition: mcc.php:32
ApiMain\$mCacheMode
string $mCacheMode
Definition: ApiMain.php:448
$t
$t
Definition: testCompression.php:74
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:234
ApiMain\isInternalMode
isInternalMode()
Return true if the API was started by other PHP code using FauxRequest.
Definition: ApiMain.php:582
ApiMain\getUserAgent
getUserAgent()
Fetches the user agent used for this request.
Definition: ApiMain.php:2370
ApiBase\PARAM_HELP_MSG_PER_VALUE
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
Definition: ApiBase.php:138
ApiMain\setCacheMaxAge
setCacheMaxAge( $maxage)
Set how long the response should be cached.
Definition: ApiMain.php:690
ApiBase\getHookRunner
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition: ApiBase.php:653
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
MediaWiki\Rest\HeaderParser\Origin
A class to assist with the parsing of Origin header according to the RFC 6454 https://tools....
Definition: Origin.php:12
ApiFormatBase\initPrinter
initPrinter( $unused=false)
Initialize the printer function and prepare the output headers.
Definition: ApiFormatBase.php:189
ApiMain\setCacheMode
setCacheMode( $mode)
Set the type of caching headers which will be sent.
Definition: ApiMain.php:722
MediaWiki\User\UserFactory
Creates User objects.
Definition: UserFactory.php:41
ApiMain\getPrinter
getPrinter()
Get the result formatter object.
Definition: ApiMain.php:681
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:474
ApiMain\$mParamsUsed
array $mParamsUsed
Definition: ApiMain.php:454
$type
$type
Definition: testCompression.php:52
ApiMain\$mModule
ApiBase $mModule
Definition: ApiMain.php:445