26use Wikimedia\Timestamp\TimestampException;
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,
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,
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,
132 'msg' =>
'right-writeapi',
136 'msg' =>
'api-help-right-apihighlimits',
174 $context = RequestContext::getMain();
178 $context = RequestContext::getMain();
183 if ( isset( $request ) ) {
189 $this->mInternalMode = ( $request instanceof
FauxRequest );
192 parent::__construct( $this, $this->mInternalMode ?
'main_int' :
'main' );
196 if ( !$this->mInternalMode ) {
199 $originHeader = $request->getHeader(
'Origin' );
200 if ( $originHeader ===
false ) {
203 $originHeader = trim( $originHeader );
204 $origins = preg_split(
'/\s+/', $originHeader );
206 $sessionCookies = array_intersect(
207 array_keys( $_COOKIE ),
208 MediaWiki\Session\SessionManager::singleton()->getVaryCookies()
210 if ( $origins && $sessionCookies && (
211 count( $origins ) !== 1 || !self::matchOrigin(
213 $config->get(
'CrossSiteAJAXdomains' ),
214 $config->get(
'CrossSiteAJAXdomainExceptions' )
217 LoggerFactory::getInstance(
'cors' )->warning(
218 'Non-whitelisted CORS request with session cookies', [
219 'origin' => $originHeader,
220 'cookies' => $sessionCookies,
221 'ip' => $request->getIP(),
222 'userAgent' => $this->getUserAgent(),
223 'wiki' => WikiMap::getCurrentWikiDbDomain()->getId(),
232 wfDebug(
"API: stripping user credentials when the same-origin policy is not applied\n" );
233 $wgUser =
new User();
235 $request->response()->header(
'MediaWiki-Login-Suppressed: true' );
244 $uselang = $request->getRawVal(
'uselang', self::API_DEFAULT_USELANG );
245 if ( $uselang ===
'user' ) {
249 if ( $uselang ===
'content' ) {
250 $uselang = MediaWikiServices::getInstance()->getContentLanguage()->getCode();
252 $code = RequestContext::sanitizeLangCode( $uselang );
254 if ( !$this->mInternalMode ) {
257 RequestContext::getMain()->setLanguage(
$wgLang );
264 $errorFormat = $request->getRawVal(
'errorformat',
'bc' );
265 $errorLangCode = $request->getRawVal(
'errorlang',
'uselang' );
266 $errorsUseDB = $request->getCheck(
'errorsuselocal' );
267 if ( in_array( $errorFormat, [
'plaintext',
'wikitext',
'html',
'raw',
'none' ],
true ) ) {
268 if ( $errorLangCode ===
'uselang' ) {
270 } elseif ( $errorLangCode ===
'content' ) {
271 $errorLang = MediaWikiServices::getInstance()->getContentLanguage();
273 $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode );
274 $errorLang = Language::factory( $errorLangCode );
277 $this->mResult, $errorLang, $errorFormat, $errorsUseDB
286 MediaWikiServices::getInstance()->getObjectFactory()
288 $this->mModuleMgr->addModules( self::$Modules,
'action' );
289 $this->mModuleMgr->addModules( $config->get(
'APIModules' ),
'action' );
290 $this->mModuleMgr->addModules( self::$Formats,
'format' );
291 $this->mModuleMgr->addModules( $config->get(
'APIFormatModules' ),
'format' );
293 Hooks::run(
'ApiMain::moduleManager', [ $this->mModuleMgr ] );
295 $this->mContinuationManager =
null;
296 $this->mEnableWrite = $enableWrite;
298 $this->mCdnMaxAge = -1;
306 return $this->mInternalMode;
315 return $this->mResult;
324 return $this->lacksSameOriginSecurity;
330 if ( $request->getCheck(
'callback' ) ) {
336 if ( $request->getVal(
'origin' ) ===
'*' ) {
343 if ( $request->getHeader(
'Treat-as-Untrusted' ) !==
false ) {
350 return $this->lacksSameOriginSecurity;
358 return $this->mErrorFormatter;
366 return $this->mContinuationManager;
374 if ( $manager !==
null && $this->mContinuationManager !==
null ) {
375 throw new UnexpectedValueException(
376 __METHOD__ .
': tried to set manager from ' . $manager->getSource() .
377 ' when a manager is already set from ' . $this->mContinuationManager->getSource()
380 $this->mContinuationManager = $manager;
389 return $this->mModule;
398 return $this->mPrinter;
408 'max-age' => $maxage,
409 's-maxage' => $maxage
439 if ( !in_array( $mode, [
'private',
'public',
'anon-public-user-private' ] ) ) {
440 wfDebug( __METHOD__ .
": unrecognised cache mode \"$mode\"\n" );
446 if ( !User::isEveryoneAllowed(
'read' ) ) {
448 if ( $mode !==
'private' ) {
449 wfDebug( __METHOD__ .
": ignoring request for $mode cache mode, private wiki\n" );
455 if ( $mode ===
'public' && $this->
getParameter(
'uselang' ) ===
'user' ) {
460 wfDebug( __METHOD__ .
": downgrading cache mode 'public' to " .
461 "'anon-public-user-private' due to uselang=user\n" );
462 $mode =
'anon-public-user-private';
465 wfDebug( __METHOD__ .
": setting cache mode $mode\n" );
466 $this->mCacheMode = $mode;
480 $this->mCacheControl = $directives + $this->mCacheControl;
491 $printer = $this->mModuleMgr->getModule( $format,
'format',
true );
492 if ( $printer ===
null ) {
505 if ( $this->mInternalMode ) {
525 if ( $this->
getRequest()->getMethod() ===
'OPTIONS' ) {
531 $obLevel = ob_get_level();
534 $t = microtime(
true );
538 $runTime = microtime(
true ) -
$t;
540 MediaWikiServices::getInstance()->getStatsdDataFactory()->timing(
541 'api.' . $this->mModule->getModuleName() .
'.executeTiming', 1000 * $runTime
543 }
catch ( Exception $e ) {
547 }
catch ( Throwable $e ) {
554 MediaWiki::preOutputCommit( $this->
getContext() );
562 while ( ob_get_level() > $obLevel ) {
577 MWExceptionHandler::rollbackMasterChangesAndLog( $e );
581 Hooks::run(
'ApiMain::onException', [ $this, $e ] );
593 $headerStr =
'MediaWiki-API-Error: ' . implode(
', ', $errCodes );
613 $this->
addWarning(
'apiwarn-errorprinterfailed' );
616 $this->mPrinter->addWarning( $error );
617 }
catch ( Exception $ex2 ) {
620 }
catch ( Throwable $ex2 ) {
627 $this->mPrinter =
null;
629 $this->mPrinter->forceDefaultParams();
651 $main =
new self( RequestContext::getMain(), false );
652 $main->handleException( $e );
653 $main->logRequest( 0, $e );
654 }
catch ( Exception $e2 ) {
657 }
catch ( Throwable $e2 ) {
663 $main->sendCacheHeaders(
true );
684 if ( $originParam ===
null ) {
692 $matchedOrigin =
false;
693 $allowTiming =
false;
696 if ( $originParam ===
'*' ) {
702 $matchedOrigin =
true;
704 $allowCredentials =
'false';
710 $originHeader = $request->getHeader(
'Origin' );
711 if ( $originHeader ===
false ) {
714 $originHeader = trim( $originHeader );
715 $origins = preg_split(
'/\s+/', $originHeader );
718 if ( !in_array( $originParam, $origins ) ) {
722 $response->header(
'Cache-Control: no-cache' );
723 echo
"'origin' parameter does not match Origin header\n";
729 $matchedOrigin = count( $origins ) === 1 && self::matchOrigin(
731 $config->get(
'CrossSiteAJAXdomains' ),
732 $config->get(
'CrossSiteAJAXdomainExceptions' )
735 $allowOrigin = $originHeader;
736 $allowCredentials =
'true';
737 $allowTiming = $originHeader;
740 if ( $matchedOrigin ) {
741 $requestedMethod = $request->getHeader(
'Access-Control-Request-Method' );
742 $preflight = $request->getMethod() ===
'OPTIONS' && $requestedMethod !==
false;
745 if ( $requestedMethod !==
'POST' && $requestedMethod !==
'GET' ) {
747 $response->header(
'MediaWiki-CORS-Rejection: Unsupported method requested in preflight' );
751 $requestedHeaders = $request->getHeader(
'Access-Control-Request-Headers' );
752 if ( $requestedHeaders !==
false ) {
753 if ( !self::matchRequestedHeaders( $requestedHeaders ) ) {
754 $response->header(
'MediaWiki-CORS-Rejection: Unsupported header requested in preflight' );
757 $response->header(
'Access-Control-Allow-Headers: ' . $requestedHeaders );
761 $response->header(
'Access-Control-Allow-Methods: POST, GET' );
762 } elseif ( $request->getMethod() !==
'POST' && $request->getMethod() !==
'GET' ) {
765 'MediaWiki-CORS-Rejection: Unsupported method for simple request or actual request'
770 $response->header(
"Access-Control-Allow-Origin: $allowOrigin" );
771 $response->header(
"Access-Control-Allow-Credentials: $allowCredentials" );
773 if ( $allowTiming !==
false ) {
774 $response->header(
"Timing-Allow-Origin: $allowTiming" );
779 'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag, '
780 .
'MediaWiki-Login-Suppressed'
784 $response->header(
'MediaWiki-CORS-Rejection: Origin mismatch' );
788 $this->
getOutput()->addVaryHeader(
'Origin' );
802 protected static function matchOrigin( $value, $rules, $exceptions ) {
803 foreach ( $rules as $rule ) {
804 if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
806 foreach ( $exceptions as $exc ) {
807 if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
827 if ( trim( $requestedHeaders ) ===
'' ) {
830 $requestedHeaders = explode(
',', $requestedHeaders );
831 $allowedAuthorHeaders = array_flip( [
845 foreach ( $requestedHeaders as $rHeader ) {
846 $rHeader = strtolower( trim( $rHeader ) );
847 if ( !isset( $allowedAuthorHeaders[$rHeader] ) ) {
848 LoggerFactory::getInstance(
'api-warning' )->warning(
849 'CORS preflight failed on requested header: {header}', [
868 $wildcard = preg_quote( $wildcard,
'/' );
869 $wildcard = str_replace(
875 return "/^https?:\/\/$wildcard$/";
887 $out->addVaryHeader(
'Treat-as-Untrusted' );
891 if ( $config->get(
'VaryOnXFP' ) ) {
892 $out->addVaryHeader(
'X-Forwarded-Proto' );
895 if ( !$isError && $this->mModule &&
898 $etag = $this->mModule->getConditionalRequestData(
'etag' );
899 if ( $etag !==
null ) {
902 $lastMod = $this->mModule->getConditionalRequestData(
'last-modified' );
903 if ( $lastMod !==
null ) {
915 if ( isset( $this->mCacheControl[
'max-age'] ) ) {
916 $maxage = $this->mCacheControl[
'max-age'];
917 } elseif ( ( $this->mModule && !$this->mModule->isWriteMode() ) ||
918 $this->mCacheMode !==
'private'
922 $privateCache =
'private, must-revalidate, max-age=' . $maxage;
924 if ( $this->mCacheMode ==
'private' ) {
925 $response->header(
"Cache-Control: $privateCache" );
929 if ( $this->mCacheMode ==
'anon-public-user-private' ) {
930 $out->addVaryHeader(
'Cookie' );
931 $response->header( $out->getVaryHeader() );
932 if (
MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
935 $response->header(
"Cache-Control: $privateCache" );
942 $response->header( $out->getVaryHeader() );
945 if ( !isset( $this->mCacheControl[
's-maxage'] ) ) {
946 $this->mCacheControl[
's-maxage'] = $this->
getParameter(
'smaxage' );
948 if ( !isset( $this->mCacheControl[
'max-age'] ) ) {
949 $this->mCacheControl[
'max-age'] = $this->
getParameter(
'maxage' );
952 if ( !$this->mCacheControl[
's-maxage'] && !$this->mCacheControl[
'max-age'] ) {
956 $response->header(
"Cache-Control: $privateCache" );
961 $this->mCacheControl[
'public'] =
true;
964 $maxAge = min( $this->mCacheControl[
's-maxage'], $this->mCacheControl[
'max-age'] );
965 $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
971 foreach ( $this->mCacheControl as $name => $value ) {
972 if ( is_bool( $value ) ) {
974 $ccHeader .= $separator . $name;
978 $ccHeader .= $separator .
"$name=$value";
983 $response->header(
"Cache-Control: $ccHeader" );
990 if ( !isset( $this->mPrinter ) ) {
991 $value = $this->
getRequest()->getVal(
'format', self::API_DEFAULT_FORMAT );
992 if ( !$this->mModuleMgr->isDefined( $value,
'format' ) ) {
993 $value = self::API_DEFAULT_FORMAT;
1000 if ( !$this->mPrinter->canPrintErrors() ) {
1023 foreach ( $e->getStatusValue()->getErrorsByType(
$type ) as $error ) {
1026 } elseif (
$type !==
'error' ) {
1032 $class = preg_replace(
'#^Wikimedia\\\Rdbms\\\#',
'', get_class( $e ) );
1033 $code =
'internal_api_error_' . $class;
1034 $data = [
'errorclass' => get_class( $e ) ];
1035 if ( $config->get(
'ShowExceptionDetails' ) ) {
1037 $msg = $e->getMessageObject();
1043 $params = [
'apierror-exceptioncaught', WebRequest::getRequestId(), $msg ];
1045 $params = [
'apierror-exceptioncaughttype', WebRequest::getRequestId(), get_class( $e ) ];
1065 $errors = $result->getResultData( [
'errors' ] );
1066 $warnings = $result->getResultData( [
'warnings' ] );
1068 if ( $warnings !==
null ) {
1069 $result->addValue(
null,
'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
1071 if ( $errors !==
null ) {
1072 $result->addValue(
null,
'errors', $errors, ApiResult::NO_SIZE_CHECK );
1075 foreach ( $errors as $error ) {
1076 if ( isset( $error[
'code'] ) ) {
1077 $errorCodes[$error[
'code']] =
true;
1086 $errorCodes[$msg->getApiCode()] =
true;
1088 LoggerFactory::getInstance(
'api-warning' )->error(
'Invalid API error code "{code}"', [
1089 'code' => $msg->getApiCode(),
1092 $errorCodes[
'<invalid-code>'] =
true;
1094 $formatter->addError( $modulePath, $msg );
1097 $formatter->addWarning( $modulePath, $msg );
1103 $path = [
'error' ];
1109 $result->addContentValue(
1113 $this->
msg(
'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text()
1115 . $this->msg(
'api-usage-mailinglist-ref' )->inLanguage( $formatter->getLanguage() )->text()
1118 } elseif ( $config->get(
'ShowExceptionDetails' ) ) {
1119 $result->addContentValue(
1122 $this->
msg(
'api-exception-trace',
1126 MWExceptionHandler::getRedactedTraceAsString( $e )
1127 )->inLanguage( $formatter->getLanguage() )->text()
1134 return array_keys( $errorCodes );
1146 if ( $requestid !==
null ) {
1147 $result->addValue(
null,
'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
1150 if ( $this->
getConfig()->
get(
'ShowHostnames' ) && (
1151 in_array(
'servedby', $force,
true ) || $this->
getParameter(
'servedby' )
1153 $result->addValue(
null,
'servedby',
wfHostname(), ApiResult::NO_SIZE_CHECK );
1157 $result->addValue(
null,
'curtimestamp',
wfTimestamp( TS_ISO_8601 ), ApiResult::NO_SIZE_CHECK );
1161 $result->addValue(
null,
'uselang', $this->
getLanguage()->getCode(),
1162 ApiResult::NO_SIZE_CHECK );
1164 ApiResult::NO_SIZE_CHECK );
1176 $this->mAction = $params[
'action'];
1189 $module = $this->mModuleMgr->getModule( $this->mAction,
'action' );
1190 if ( $module ===
null ) {
1194 [
'apierror-unknownaction',
wfEscapeWikiText( $this->mAction ) ],
'unknown_action'
1198 $moduleParams = $module->extractRequestParams();
1201 if ( $module->needsToken() ===
true ) {
1203 "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
1204 'See documentation for ApiBase::needsToken for details.'
1207 if ( $module->needsToken() ) {
1208 if ( !$module->mustBePosted() ) {
1210 "Module '{$module->getModuleName()}' must require POST to use tokens."
1214 if ( !isset( $moduleParams[
'token'] ) ) {
1217 $module->dieWithError( [
'apierror-missingparam',
'token' ] );
1221 $module->requirePostedParameters( [
'token' ] );
1223 if ( !$module->validateToken( $moduleParams[
'token'], $moduleParams ) ) {
1224 $module->dieWithError(
'apierror-badtoken' );
1235 $dbLag = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaxLag();
1237 'host' => $dbLag[0],
1242 $jobQueueLagFactor = $this->
getConfig()->get(
'JobQueueIncludeInMaxLagFactor' );
1243 if ( $jobQueueLagFactor ) {
1245 $totalJobs = array_sum( JobQueueGroup::singleton()->getQueueSizes() );
1246 $jobQueueLag = $totalJobs / (float)$jobQueueLagFactor;
1247 if ( $jobQueueLag > $lagInfo[
'lag'] ) {
1250 'lag' => $jobQueueLag,
1251 'type' =>
'jobqueue',
1252 'jobs' => $totalJobs,
1257 Hooks::runWithoutAbort(
'ApiMaxLagInfo', [ &$lagInfo ] );
1269 if ( $module->shouldCheckMaxlag() && isset( $params[
'maxlag'] ) ) {
1270 $maxLag = $params[
'maxlag'];
1272 if ( $lagInfo[
'lag'] > $maxLag ) {
1275 $response->header(
'Retry-After: ' . max( (
int)$maxLag, 5 ) );
1276 $response->header(
'X-Database-Lag: ' . (
int)$lagInfo[
'lag'] );
1278 if ( $this->
getConfig()->
get(
'ShowHostnames' ) ) {
1280 [
'apierror-maxlag', $lagInfo[
'lag'], $lagInfo[
'host'] ],
1286 $this->
dieWithError( [
'apierror-maxlag-generic', $lagInfo[
'lag'] ],
'maxlag', $lagInfo );
1315 if ( $this->mInternalMode ) {
1320 if ( $this->
getRequest()->getMethod() !==
'GET' && $this->
getRequest()->getMethod() !==
'HEAD' ) {
1327 $ifNoneMatch = array_diff(
1328 $this->
getRequest()->getHeader(
'If-None-Match', WebRequest::GETHEADER_LIST ) ?: [],
1331 if ( $ifNoneMatch ) {
1332 if ( $ifNoneMatch === [
'*' ] ) {
1336 $etag = $module->getConditionalRequestData(
'etag' );
1339 if ( $ifNoneMatch && $etag !==
null ) {
1340 $test = substr( $etag, 0, 2 ) ===
'W/' ? substr( $etag, 2 ) : $etag;
1341 $match = array_map(
function (
$s ) {
1342 return substr(
$s, 0, 2 ) ===
'W/' ? substr(
$s, 2 ) :
$s;
1344 $return304 = in_array( $test, $match,
true );
1346 $value = trim( $this->
getRequest()->getHeader(
'If-Modified-Since' ) );
1351 $i = strpos( $value,
';' );
1352 if ( $i !==
false ) {
1353 $value = trim( substr( $value, 0, $i ) );
1356 if ( $value !==
'' ) {
1361 $ts->getTimestamp( TS_RFC2822 ) === $value ||
1363 $ts->format(
'l, d-M-y H:i:s' ) .
' GMT' === $value ||
1365 $ts->format(
'D M j H:i:s Y' ) === $value ||
1366 $ts->format(
'D M j H:i:s Y' ) === $value
1369 $lastMod = $module->getConditionalRequestData(
'last-modified' );
1370 if ( $lastMod !==
null ) {
1374 'user' => $this->
getUser()->getTouched(),
1375 'epoch' => $config->get(
'CacheEpoch' ),
1378 if ( $config->get(
'UseCdn' ) ) {
1381 TS_MW, time() - $config->get(
'CdnMaxAge' )
1384 Hooks::run(
'OutputPageCheckLastModified', [ &$modifiedTimes, $this->
getOutput() ] );
1385 $lastMod = max( $modifiedTimes );
1386 $return304 =
wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
1389 }
catch ( TimestampException $e ) {
1396 $this->
getRequest()->response()->statusHeader( 304 );
1399 Wikimedia\suppressWarnings();
1400 ini_set(
'zlib.output_compression', 0 );
1401 Wikimedia\restoreWarnings();
1416 if ( $module->isReadMode() && !$this->getPermissionManager()->isEveryoneAllowed(
'read' ) &&
1417 !$this->getPermissionManager()->userHasRight( $user,
'read' )
1422 if ( $module->isWriteMode() ) {
1423 if ( !$this->mEnableWrite ) {
1427 } elseif ( $this->
getRequest()->getHeader(
'Promise-Non-Write-API-Action' ) ) {
1428 $this->
dieWithError(
'apierror-promised-nonwrite-api' );
1435 $message =
'hookaborted';
1436 if ( !Hooks::run(
'ApiCheckCanExecute', [ $module, $user, &$message ] ) ) {
1450 if ( $module->isWriteMode()
1451 && $this->getUser()->isBot()
1452 && MediaWikiServices::getInstance()->getDBLoadBalancer()->getServerCount() > 1
1464 $lagLimit = $this->
getConfig()->get(
'APIMaxLagThreshold' );
1465 $laggedServers = [];
1466 $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
1467 foreach ( $loadBalancer->getLagTimes() as $serverIndex => $lag ) {
1468 if ( $lag > $lagLimit ) {
1470 $laggedServers[] = $loadBalancer->getServerName( $serverIndex ) .
" ({$lag}s)";
1475 $replicaCount = $loadBalancer->getServerCount() - 1;
1476 if ( $numLagged >= ceil( $replicaCount / 2 ) ) {
1477 $laggedServers = implode(
', ', $laggedServers );
1480 "Api request failed as read only because the following DBs are lagged: $laggedServers"
1482 LoggerFactory::getInstance(
'api-warning' )->warning(
1483 "Api request failed as read only because the following DBs are lagged: {laggeddbs}", [
1484 'laggeddbs' => $laggedServers,
1491 [
'readonlyreason' =>
"Waiting for $numLagged lagged database(s)" ]
1501 if ( isset( $params[
'assert'] ) ) {
1503 switch ( $params[
'assert'] ) {
1505 if ( $user->isAnon() ) {
1516 if ( isset( $params[
'assertuser'] ) ) {
1517 $assertUser = User::newFromName( $params[
'assertuser'],
false );
1518 if ( !$assertUser || !$this->
getUser()->equals( $assertUser ) ) {
1520 [
'apierror-assertnameduserfailed',
wfEscapeWikiText( $params[
'assertuser'] ) ]
1532 $validMethods = [
'GET',
'HEAD',
'POST',
'OPTIONS' ];
1535 if ( !in_array( $request->getMethod(), $validMethods ) ) {
1536 $this->
dieWithError(
'apierror-invalidmethod',
null,
null, 405 );
1539 if ( !$request->wasPosted() && $module->mustBePosted() ) {
1545 if ( $request->wasPosted() && !$request->getHeader(
'Content-Type' ) ) {
1547 'apiwarn-deprecation-post-without-content-type',
'post-without-content-type'
1552 $this->mPrinter = $module->getCustomPrinter();
1553 if ( is_null( $this->mPrinter ) ) {
1558 if ( $request->getProtocol() ===
'http' &&
1560 $this->getConfig()->get(
'ForceHTTPS' ) ||
1561 $request->getSession()->shouldForceHTTPS() ||
1562 ( $this->getUser()->isLoggedIn() &&
1563 $this->getUser()->requiresHTTPS() )
1566 $this->
addDeprecation(
'apiwarn-deprecation-httpsexpected',
'https-expected' );
1581 $this->mModule = $module;
1583 if ( !$this->mInternalMode ) {
1597 if ( !$this->mInternalMode ) {
1602 Hooks::run(
'APIAfterExecute', [ &$module ] );
1606 if ( !$this->mInternalMode ) {
1618 $limits = $this->
getConfig()->get(
'TrxProfilerLimits' );
1619 $trxProfiler = Profiler::instance()->getTransactionProfiler();
1620 $trxProfiler->setLogger( LoggerFactory::getInstance(
'DBPerformance' ) );
1621 if ( $this->
getRequest()->hasSafeMethod() ) {
1622 $trxProfiler->setExpectations( $limits[
'GET'], __METHOD__ );
1624 $trxProfiler->setExpectations( $limits[
'POST-nonwrite'], __METHOD__ );
1627 $trxProfiler->setExpectations( $limits[
'POST'], __METHOD__ );
1641 '$schema' =>
'/mediawiki/api/request/0.0.1',
1643 'request_id' => WebRequest::getRequestId(),
1646 'domain' => $this->
getConfig()->get(
'ServerName' ),
1649 'stream' =>
'mediawiki.api-request'
1652 'method' => $request->getMethod(),
1653 'client_ip' => $request->getIP()
1655 'database' => WikiMap::getCurrentWikiDbDomain()->getId(),
1656 'backend_time_ms' => (int)round( $time * 1000 ),
1660 $httpRequestHeadersToLog = [
'accept-language',
'referer',
'user-agent' ];
1661 foreach ( $httpRequestHeadersToLog as
$header ) {
1662 if ( $request->getHeader(
$header ) ) {
1664 $logCtx[
'http'][
'request_headers'][
$header] = $request->getHeader(
$header );
1669 $logCtx[
'api_error_codes'] = [];
1671 $logCtx[
'api_error_codes'][] = $msg->getApiCode();
1676 $msg =
"API {$request->getMethod()} " .
1678 " {$logCtx['http']['client_ip']} " .
1679 "T={$logCtx['backend_time_ms']}ms";
1683 $value = $request->getVal( $name );
1684 if ( $value ===
null ) {
1688 if ( isset( $sensitive[$name] ) ) {
1689 $value =
'[redacted]';
1690 $encValue =
'[redacted]';
1691 } elseif ( strlen( $value ) > 256 ) {
1692 $value = substr( $value, 0, 256 );
1698 $logCtx[
'params'][$name] = $value;
1699 $msg .=
" {$name}={$encValue}";
1706 wfDebugLog(
'api-request',
'',
'private', $logCtx );
1718 $numChars = strlen(
$chars );
1719 for ( $i = 0; $i < $numChars; $i++ ) {
1724 return strtr( rawurlencode(
$s ), $table );
1732 return array_keys( $this->mParamsUsed );
1740 $this->mParamsUsed += array_fill_keys( (array)$params,
true );
1749 return array_keys( $this->mParamsSensitive );
1758 $this->mParamsSensitive += array_fill_keys( (array)$params,
true );
1767 public function getVal( $name, $default =
null ) {
1768 $this->mParamsUsed[$name] =
true;
1771 if ( $ret ===
null ) {
1772 if ( $this->
getRequest()->getArray( $name ) !==
null ) {
1775 $this->
addWarning( [
'apiwarn-unsupportedarray', $name ] );
1789 return $this->
getVal( $name,
null ) !==
null;
1800 $this->mParamsUsed[$name] =
true;
1802 return $this->
getRequest()->getUpload( $name );
1811 $allParams = $this->
getRequest()->getValueNames();
1813 if ( !$this->mInternalMode ) {
1815 $printerParams = $this->mPrinter->encodeParamName(
1816 array_keys( $this->mPrinter->getFinalParams() ?: [] )
1818 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
1820 $unusedParams = array_diff( $allParams, $paramsUsed );
1823 if ( count( $unusedParams ) ) {
1825 'apierror-unrecognizedparams',
1827 count( $unusedParams )
1838 if ( $this->
getConfig()->
get(
'DebugAPI' ) !==
false ) {
1842 $printer = $this->mPrinter;
1843 $printer->initPrinter(
false );
1845 $printer->setHttpStatus( $httpCode );
1847 $printer->execute();
1848 $printer->closePrinter();
1890 'requestid' =>
null,
1891 'servedby' =>
false,
1892 'curtimestamp' =>
false,
1893 'responselanginfo' =>
false,
1905 'errorsuselocal' => [
1915 =>
'apihelp-help-example-main',
1916 'action=help&recursivesubmodules=1'
1917 =>
'apihelp-help-example-recursive',
1930 foreach ( $oldHelp as $k => $v ) {
1931 if ( $k ===
'submodules' ) {
1932 $help[
'permissions'] =
'';
1936 $help[
'datatypes'] =
'';
1937 $help[
'templatedparams'] =
'';
1938 $help[
'credits'] =
'';
1941 $help[
'permissions'] .= Html::openElement(
'div',
1942 [
'class' =>
'apihelp-block apihelp-permissions' ] );
1943 $m = $this->
msg(
'api-help-permissions' );
1944 if ( !$m->isDisabled() ) {
1945 $help[
'permissions'] .= Html::rawElement(
'div', [
'class' =>
'apihelp-block-head' ],
1946 $m->numParams( count( self::$mRights ) )->parse()
1949 $help[
'permissions'] .= Html::openElement(
'dl' );
1950 foreach ( self::$mRights as $right => $rightMsg ) {
1951 $help[
'permissions'] .= Html::element(
'dt',
null, $right );
1953 $rightMsg = $this->
msg( $rightMsg[
'msg'], $rightMsg[
'params'] )->parse();
1954 $help[
'permissions'] .= Html::rawElement(
'dd',
null, $rightMsg );
1956 $groups = array_map(
function ( $group ) {
1957 return $group ==
'*' ?
'all' : $group;
1960 $help[
'permissions'] .= Html::rawElement(
'dd',
null,
1961 $this->
msg(
'api-help-permissions-granted-to' )
1962 ->numParams( count( $groups ) )
1967 $help[
'permissions'] .= Html::closeElement(
'dl' );
1968 $help[
'permissions'] .= Html::closeElement(
'div' );
1971 if ( empty( $options[
'nolead'] ) ) {
1972 $level = $options[
'headerlevel'];
1973 $tocnumber = &$options[
'tocnumber'];
1975 $header = $this->
msg(
'api-help-datatypes-header' )->parse();
1977 $id = Sanitizer::escapeIdForAttribute(
'main/datatypes', Sanitizer::ID_PRIMARY );
1978 $idFallback = Sanitizer::escapeIdForAttribute(
'main/datatypes', Sanitizer::ID_FALLBACK );
1980 ' class="apihelp-header">',
1987 if ( $id !==
'main/datatypes' && $idFallback !==
'main/datatypes' ) {
1988 $headline =
'<div id="main/datatypes"></div>' . $headline;
1990 $help[
'datatypes'] .= $headline;
1991 $help[
'datatypes'] .= $this->
msg(
'api-help-datatypes' )->parseAsBlock();
1992 if ( !isset( $tocData[
'main/datatypes'] ) ) {
1993 $tocnumber[$level]++;
1994 $tocData[
'main/datatypes'] = [
1995 'toclevel' => count( $tocnumber ),
1997 'anchor' =>
'main/datatypes',
1999 'number' => implode(
'.', $tocnumber ),
2004 $header = $this->
msg(
'api-help-templatedparams-header' )->parse();
2006 $id = Sanitizer::escapeIdForAttribute(
'main/templatedparams', Sanitizer::ID_PRIMARY );
2007 $idFallback = Sanitizer::escapeIdForAttribute(
'main/templatedparams', Sanitizer::ID_FALLBACK );
2009 ' class="apihelp-header">',
2016 if ( $id !==
'main/templatedparams' && $idFallback !==
'main/templatedparams' ) {
2017 $headline =
'<div id="main/templatedparams"></div>' . $headline;
2019 $help[
'templatedparams'] .= $headline;
2020 $help[
'templatedparams'] .= $this->
msg(
'api-help-templatedparams' )->parseAsBlock();
2021 if ( !isset( $tocData[
'main/templatedparams'] ) ) {
2022 $tocnumber[$level]++;
2023 $tocData[
'main/templatedparams'] = [
2024 'toclevel' => count( $tocnumber ),
2026 'anchor' =>
'main/templatedparams',
2028 'number' => implode(
'.', $tocnumber ),
2033 $header = $this->
msg(
'api-credits-header' )->parse();
2034 $id = Sanitizer::escapeIdForAttribute(
'main/credits', Sanitizer::ID_PRIMARY );
2035 $idFallback = Sanitizer::escapeIdForAttribute(
'main/credits', Sanitizer::ID_FALLBACK );
2037 ' class="apihelp-header">',
2044 if ( $id !==
'main/credits' && $idFallback !==
'main/credits' ) {
2045 $headline =
'<div id="main/credits"></div>' . $headline;
2047 $help[
'credits'] .= $headline;
2048 $help[
'credits'] .= $this->
msg(
'api-credits' )->useDatabase(
false )->parseAsBlock();
2049 if ( !isset( $tocData[
'main/credits'] ) ) {
2050 $tocnumber[$level]++;
2051 $tocData[
'main/credits'] = [
2052 'toclevel' => count( $tocnumber ),
2054 'anchor' =>
'main/credits',
2056 'number' => implode(
'.', $tocnumber ),
2070 if ( !isset( $this->mCanApiHighLimits ) ) {
2072 ->userHasRight( $this->
getUser(),
'apihighlimits' );
2075 return $this->mCanApiHighLimits;
2083 return $this->mModuleMgr;
2096 $this->
getRequest()->getHeader(
'Api-user-agent' ) .
' ' .
2097 $this->
getRequest()->getHeader(
'User-agent' )
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
wfReadOnly()
Check whether the wiki is in read-only mode.
wfHostname()
Get host name of the current machine, for use in error reporting.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfClearOutputBuffers()
More legible than passing a 'false' parameter to wfResetOutputBuffers():
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
This abstract class implements many basic API functions, and is the base of all API classes.
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
dieWithErrorOrDebug( $msg, $code=null, $data=null, $httpCode=null)
Will only set a warning instead of failing if the global $wgDebugAPI is set to true.
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below.
isWriteMode()
Indicates whether this module requires write mode.
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
dieReadOnly()
Helper function for readonly errors.
addDeprecation( $msg, $feature, $data=[])
Add a deprecation warning for this module.
const LIMIT_SML2
Slow query, apihighlimits limit.
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
const LIMIT_BIG2
Fast query, apihighlimits limit.
This manages continuation state.
This is the main API class, used for both external and internal processing.
static handleApiBeforeMainException( $e)
Handle an exception from the ApiBeforeMain hook.
getExamplesMessages()
Returns usage examples for this module.Return value has query strings as keys, with values being eith...
setRequestExpectations(ApiBase $module)
Set database connection, query, and write expectations given this module request.
getAllowedParams()
See ApiBase for description.
getSensitiveParams()
Get the request parameters that should be considered sensitive.
getPrinter()
Get the result formatter object.
logRequest( $time, $e=null)
Log the preceding request.
static $Modules
List of available modules: action name => module class.
sendCacheHeaders( $isError)
Send caching headers.
encodeRequestLogValue( $s)
Encode a value in a format suitable for a space-separated log line.
markParamsUsed( $params)
Mark parameters as used.
executeActionWithErrorHandling()
Execute an action, and in case of an error, erase whatever partial results have been accumulated,...
createPrinterByName( $format)
Create an instance of an output formatter by its name.
setCacheMaxAge( $maxage)
Set how long the response should be cached.
static $mRights
List of user roles that are specifically relevant to the API.
getResult()
Get the ApiResult object associated with current request.
createErrorPrinter()
Create the printer for error output.
executeAction()
Execute the actual module, without any error handling.
getErrorFormatter()
Get the ApiErrorFormatter object associated with current request.
checkMaxLag( $module, $params)
Check the max lag if necessary.
checkAsserts( $params)
Check asserts of the user's rights.
setupExternalResponse( $module, $params)
Check POST for external response and setup result printer.
static matchRequestedHeaders( $requestedHeaders)
Attempt to validate the value of Access-Control-Request-Headers against a list of headers that we all...
bool null $lacksSameOriginSecurity
Cached return value from self::lacksSameOriginSecurity()
setCacheControl( $directives)
Set directives (key/value pairs) for the Cache-Control header.
static wildcardToRegex( $wildcard)
Helper function to convert wildcard string into a regex '*' => '.
setContinuationManager(ApiContinuationManager $manager=null)
Set the continuation manager.
setCacheMode( $mode)
Set the type of caching headers which will be sent.
setupModule()
Set up the module for response.
markParamsSensitive( $params)
Mark parameters as sensitive.
setupExecuteAction()
Set up for the execution.
checkConditionalRequestHeaders( $module)
Check selected RFC 7232 precondition headers.
checkBotReadOnly()
Check whether we are readonly for bots.
handleException( $e)
Handle an exception as an API response.
getUserAgent()
Fetches the user agent used for this request.
const API_DEFAULT_USELANG
When no uselang parameter is given, this language will be used.
static matchOrigin( $value, $rules, $exceptions)
Attempt to match an Origin header against a set of rules and a set of exceptions.
getModule()
Get the API module object.
__construct( $context=null, $enableWrite=false)
Constructs an instance of ApiMain that utilizes the module and format specified by $request.
isInternalMode()
Return true if the API was started by other PHP code using FauxRequest.
addRequestedFields( $force=[])
Add requested fields to the result.
modifyHelp(array &$help, array $options, array &$tocData)
Called from ApiHelp before the pieces are joined together and returned.This exists mainly for ApiMain...
checkReadOnly( $module)
Check if the DB is read-only for this user.
getCheck( $name)
Get a boolean request value, and register the fact that the parameter was used, for logging.
getContinuationManager()
Get the continuation manager.
printResult( $httpCode=0)
Print results using the current printer.
reportUnusedParams()
Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,...
lacksSameOriginSecurity()
Get the security flag for the current request.
getVal( $name, $default=null)
Get a request value, and register the fact that it was used, for logging.
getParamsUsed()
Get the request parameters used in the course of the preceding execute() request.
getModuleManager()
Overrides to return this instance's module manager.
ApiContinuationManager null $mContinuationManager
substituteResultWithError( $e)
Replace the result data with the information about an exception.
const API_DEFAULT_FORMAT
When no format parameter is given, this format will be used.
checkExecutePermissions( $module)
Check for sufficient permissions to execute.
getUpload( $name)
Get a request upload, and register the fact that it was used, for logging.
canApiHighLimits()
Check whether the current user is allowed to use high limits.
static $Formats
List of available formats: format name => format class.
errorMessagesFromException( $e, $type='error')
Create an error message for the given exception.
handleCORS()
Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
execute()
Execute api request.
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
This class holds a list of modules and handles instantiation.
This class represents the result of the API operations.
Exception used to abort API execution with an error.
getModulePath()
Fetch the responsible module name.
getStatusValue()
Fetch the error status.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
setContext(IContextSource $context)
An IContextSource implementation which will inherit context from another source but allow individual ...
WebRequest clone which takes values from a provided array.
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Library for creating and parsing MW-style timestamps.
static listParam(array $list, $type='text')
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
static newUUIDv4( $flags=0)
Return an RFC4122 compliant v4 UUID.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Interface for MediaWiki-localized exceptions.
if(PHP_SAPI !=='cli' &&PHP_SAPI !=='phpdbg' $chars)