26 use 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',
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(),
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();
254 if ( !$this->mInternalMode ) {
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();
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;
330 if ( $request->getCheck(
'callback' ) ) {
336 if ( $request->getVal(
'origin' ) ===
'*' ) {
343 if ( $request->getHeader(
'Treat-as-Untrusted' ) !== false ) {
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;
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" );
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;
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 ) {
562 while ( ob_get_level() > $obLevel ) {
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();
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";
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' ) ) {
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();
1039 $msg = Message::newFromSpecifier( $e );
1065 $errors = $result->getResultData( [
'errors' ] );
1066 $warnings = $result->getResultData( [
'warnings' ] );
1068 if ( $warnings !==
null ) {
1071 if ( $errors !==
null ) {
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',
1127 )->inLanguage( $formatter->getLanguage() )->text()
1134 return array_keys( $errorCodes );
1146 if ( $requestid !==
null ) {
1150 if ( $this->
getConfig()->
get(
'ShowHostnames' ) && (
1151 in_array(
'servedby', $force,
true ) || $this->
getParameter(
'servedby' )
1161 $result->addValue(
null,
'uselang', $this->
getLanguage()->getCode(),
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 ) {
1246 $jobQueueLag = $totalJobs / (float)$jobQueueLagFactor;
1247 if ( $jobQueueLag > $lagInfo[
'lag'] ) {
1250 'lag' => $jobQueueLag,
1251 'type' =>
'jobqueue',
1252 'jobs' => $totalJobs,
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(
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' )
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();
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()
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'] ) ) {
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' && (
1559 $request->getSession()->shouldForceHTTPS() ||
1560 ( $this->
getUser()->isLoggedIn() &&
1561 $this->
getUser()->requiresHTTPS() )
1563 $this->
addDeprecation(
'apiwarn-deprecation-httpsexpected',
'https-expected' );
1578 $this->mModule = $module;
1580 if ( !$this->mInternalMode ) {
1594 if ( !$this->mInternalMode ) {
1599 Hooks::run(
'APIAfterExecute', [ &$module ] );
1603 if ( !$this->mInternalMode ) {
1615 $limits = $this->
getConfig()->get(
'TrxProfilerLimits' );
1617 $trxProfiler->setLogger( LoggerFactory::getInstance(
'DBPerformance' ) );
1618 if ( $this->
getRequest()->hasSafeMethod() ) {
1619 $trxProfiler->setExpectations( $limits[
'GET'], __METHOD__ );
1621 $trxProfiler->setExpectations( $limits[
'POST-nonwrite'], __METHOD__ );
1624 $trxProfiler->setExpectations( $limits[
'POST'], __METHOD__ );
1638 '$schema' =>
'/mediawiki/api/request/0.0.1',
1643 'domain' => $this->
getConfig()->get(
'ServerName' ),
1646 'stream' =>
'mediawiki.api-request'
1649 'method' => $request->getMethod(),
1650 'client_ip' => $request->getIP()
1653 'backend_time_ms' => (int)round( $time * 1000 ),
1657 $httpRequestHeadersToLog = [
'accept-language',
'referer',
'user-agent' ];
1658 foreach ( $httpRequestHeadersToLog as
$header ) {
1659 if ( $request->getHeader(
$header ) ) {
1661 $logCtx[
'http'][
'request_headers'][
$header] = $request->getHeader(
$header );
1666 $logCtx[
'api_error_codes'] = [];
1668 $logCtx[
'api_error_codes'][] = $msg->getApiCode();
1673 $msg =
"API {$request->getMethod()} " .
1675 " {$logCtx['http']['client_ip']} " .
1676 "T={$logCtx['backend_time_ms']}ms";
1680 $value = $request->getVal( $name );
1681 if ( $value ===
null ) {
1685 if ( isset( $sensitive[$name] ) ) {
1686 $value =
'[redacted]';
1687 $encValue =
'[redacted]';
1688 } elseif ( strlen( $value ) > 256 ) {
1689 $value = substr( $value, 0, 256 );
1695 $logCtx[
'params'][$name] = $value;
1696 $msg .=
" {$name}={$encValue}";
1703 wfDebugLog(
'api-request',
'',
'private', $logCtx );
1715 $numChars = strlen(
$chars );
1716 for ( $i = 0; $i < $numChars; $i++ ) {
1721 return strtr( rawurlencode(
$s ), $table );
1729 return array_keys( $this->mParamsUsed );
1737 $this->mParamsUsed += array_fill_keys( (array)$params,
true );
1746 return array_keys( $this->mParamsSensitive );
1755 $this->mParamsSensitive += array_fill_keys( (array)$params,
true );
1764 public function getVal( $name, $default =
null ) {
1765 $this->mParamsUsed[$name] =
true;
1768 if ( $ret ===
null ) {
1769 if ( $this->
getRequest()->getArray( $name ) !==
null ) {
1772 $this->
addWarning( [
'apiwarn-unsupportedarray', $name ] );
1786 return $this->
getVal( $name,
null ) !==
null;
1797 $this->mParamsUsed[$name] =
true;
1799 return $this->
getRequest()->getUpload( $name );
1808 $allParams = $this->
getRequest()->getValueNames();
1810 if ( !$this->mInternalMode ) {
1812 $printerParams = $this->mPrinter->encodeParamName(
1813 array_keys( $this->mPrinter->getFinalParams() ?: [] )
1815 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
1817 $unusedParams = array_diff( $allParams, $paramsUsed );
1820 if ( count( $unusedParams ) ) {
1822 'apierror-unrecognizedparams',
1823 Message::listParam( array_map(
'wfEscapeWikiText', $unusedParams ),
'comma' ),
1824 count( $unusedParams )
1835 if ( $this->
getConfig()->
get(
'DebugAPI' ) !==
false ) {
1842 $printer->setHttpStatus( $httpCode );
1844 $printer->execute();
1845 $printer->closePrinter();
1887 'requestid' =>
null,
1888 'servedby' =>
false,
1889 'curtimestamp' =>
false,
1890 'responselanginfo' =>
false,
1902 'errorsuselocal' => [
1912 =>
'apihelp-help-example-main',
1913 'action=help&recursivesubmodules=1'
1914 =>
'apihelp-help-example-recursive',
1927 foreach ( $oldHelp as $k => $v ) {
1928 if ( $k ===
'submodules' ) {
1929 $help[
'permissions'] =
'';
1933 $help[
'datatypes'] =
'';
1934 $help[
'templatedparams'] =
'';
1935 $help[
'credits'] =
'';
1938 $help[
'permissions'] .= Html::openElement(
'div',
1939 [
'class' =>
'apihelp-block apihelp-permissions' ] );
1940 $m = $this->
msg(
'api-help-permissions' );
1941 if ( !$m->isDisabled() ) {
1942 $help[
'permissions'] .= Html::rawElement(
'div', [
'class' =>
'apihelp-block-head' ],
1943 $m->numParams( count( self::$mRights ) )->parse()
1946 $help[
'permissions'] .= Html::openElement(
'dl' );
1947 foreach ( self::$mRights as $right => $rightMsg ) {
1948 $help[
'permissions'] .= Html::element(
'dt',
null, $right );
1950 $rightMsg = $this->
msg( $rightMsg[
'msg'], $rightMsg[
'params'] )->parse();
1951 $help[
'permissions'] .= Html::rawElement(
'dd',
null, $rightMsg );
1953 $groups = array_map(
function ( $group ) {
1954 return $group ==
'*' ?
'all' : $group;
1957 $help[
'permissions'] .= Html::rawElement(
'dd',
null,
1958 $this->
msg(
'api-help-permissions-granted-to' )
1959 ->numParams( count( $groups ) )
1960 ->params( Message::listParam( $groups ) )
1964 $help[
'permissions'] .= Html::closeElement(
'dl' );
1965 $help[
'permissions'] .= Html::closeElement(
'div' );
1968 if ( empty( $options[
'nolead'] ) ) {
1969 $level = $options[
'headerlevel'];
1970 $tocnumber = &$options[
'tocnumber'];
1972 $header = $this->
msg(
'api-help-datatypes-header' )->parse();
1974 $id = Sanitizer::escapeIdForAttribute(
'main/datatypes', Sanitizer::ID_PRIMARY );
1975 $idFallback = Sanitizer::escapeIdForAttribute(
'main/datatypes', Sanitizer::ID_FALLBACK );
1977 ' class="apihelp-header">',
1984 if ( $id !==
'main/datatypes' && $idFallback !==
'main/datatypes' ) {
1985 $headline =
'<div id="main/datatypes"></div>' . $headline;
1987 $help[
'datatypes'] .= $headline;
1988 $help[
'datatypes'] .= $this->
msg(
'api-help-datatypes' )->parseAsBlock();
1989 if ( !isset( $tocData[
'main/datatypes'] ) ) {
1990 $tocnumber[$level]++;
1991 $tocData[
'main/datatypes'] = [
1992 'toclevel' => count( $tocnumber ),
1994 'anchor' =>
'main/datatypes',
1996 'number' => implode(
'.', $tocnumber ),
2001 $header = $this->
msg(
'api-help-templatedparams-header' )->parse();
2003 $id = Sanitizer::escapeIdForAttribute(
'main/templatedparams', Sanitizer::ID_PRIMARY );
2004 $idFallback = Sanitizer::escapeIdForAttribute(
'main/templatedparams', Sanitizer::ID_FALLBACK );
2006 ' class="apihelp-header">',
2013 if ( $id !==
'main/templatedparams' && $idFallback !==
'main/templatedparams' ) {
2014 $headline =
'<div id="main/templatedparams"></div>' . $headline;
2016 $help[
'templatedparams'] .= $headline;
2017 $help[
'templatedparams'] .= $this->
msg(
'api-help-templatedparams' )->parseAsBlock();
2018 if ( !isset( $tocData[
'main/templatedparams'] ) ) {
2019 $tocnumber[$level]++;
2020 $tocData[
'main/templatedparams'] = [
2021 'toclevel' => count( $tocnumber ),
2023 'anchor' =>
'main/templatedparams',
2025 'number' => implode(
'.', $tocnumber ),
2030 $header = $this->
msg(
'api-credits-header' )->parse();
2031 $id = Sanitizer::escapeIdForAttribute(
'main/credits', Sanitizer::ID_PRIMARY );
2032 $idFallback = Sanitizer::escapeIdForAttribute(
'main/credits', Sanitizer::ID_FALLBACK );
2034 ' class="apihelp-header">',
2041 if ( $id !==
'main/credits' && $idFallback !==
'main/credits' ) {
2042 $headline =
'<div id="main/credits"></div>' . $headline;
2044 $help[
'credits'] .= $headline;
2045 $help[
'credits'] .= $this->
msg(
'api-credits' )->useDatabase(
false )->parseAsBlock();
2046 if ( !isset( $tocData[
'main/credits'] ) ) {
2047 $tocnumber[$level]++;
2048 $tocData[
'main/credits'] = [
2049 'toclevel' => count( $tocnumber ),
2051 'anchor' =>
'main/credits',
2053 'number' => implode(
'.', $tocnumber ),
2067 if ( !isset( $this->mCanApiHighLimits ) ) {
2069 ->userHasRight( $this->
getUser(),
'apihighlimits' );
2093 $this->
getRequest()->getHeader(
'Api-user-agent' ) .
' ' .
2094 $this->
getRequest()->getHeader(
'User-agent' )