'rsd' => 'ApiRsd',
'compare' => 'ApiComparePages',
'tokens' => 'ApiTokens',
+ 'checktoken' => 'ApiCheckToken',
// Write modules
'purge' => 'ApiPurge',
'options' => 'ApiOptions',
'imagerotate' => 'ApiImageRotate',
'revisiondelete' => 'ApiRevisionDelete',
+ 'managetags' => 'ApiManageTags',
);
/**
// Remove all modules other than login
global $wgUser;
- if ( $this->getVal( 'callback' ) !== null ) {
- // JSON callback allows cross-site reads.
- // For safety, strip user credentials.
- wfDebug( "API: stripping user credentials for JSON callback\n" );
+ if ( $this->lacksSameOriginSecurity() ) {
+ // If we're in a mode that breaks the same-origin policy, strip
+ // user credentials for security.
+ wfDebug( "API: stripping user credentials when the same-origin policy is not applied\n" );
$wgUser = new User();
$this->getContext()->setUser( $wgUser );
}
$uselang = $this->getParameter( 'uselang' );
if ( $uselang === 'user' ) {
- $uselang = $this->getUser()->getOption( 'language' );
- $uselang = RequestContext::sanitizeLangCode( $uselang );
- wfRunHooks( 'UserGetLanguageObject', array( $this->getUser(), &$uselang, $this ) );
- } elseif ( $uselang === 'content' ) {
- global $wgContLang;
- $uselang = $wgContLang->getCode();
- }
- $code = RequestContext::sanitizeLangCode( $uselang );
- $this->getContext()->setLanguage( $code );
- if ( !$this->mInternalMode ) {
- global $wgLang;
- $wgLang = $this->getContext()->getLanguage();
- RequestContext::getMain()->setLanguage( $wgLang );
+ // Assume the parent context is going to return the user language
+ // for uselang=user (see T85635).
+ } else {
+ if ( $uselang === 'content' ) {
+ global $wgContLang;
+ $uselang = $wgContLang->getCode();
+ }
+ $code = RequestContext::sanitizeLangCode( $uselang );
+ $this->getContext()->setLanguage( $code );
+ if ( !$this->mInternalMode ) {
+ global $wgLang;
+ $wgLang = $this->getContext()->getLanguage();
+ RequestContext::getMain()->setLanguage( $wgLang );
+ }
}
$config = $this->getConfig();
$this->mModuleMgr->addModules( self::$Formats, 'format' );
$this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' );
+ Hooks::run( 'ApiMain::moduleManager', array( $this->mModuleMgr ) );
+
$this->mResult = new ApiResult( $this );
$this->mEnableWrite = $enableWrite;
* Execute api request. Any errors will be handled if the API was called by the remote client.
*/
public function execute() {
- $this->profileIn();
if ( $this->mInternalMode ) {
$this->executeAction();
} else {
$this->executeActionWithErrorHandling();
}
-
- $this->profileOut();
}
/**
}
// Allow extra cleanup and logging
- wfRunHooks( 'ApiMain::onException', array( $this, $e ) );
+ Hooks::run( 'ApiMain::onException', array( $this, $e ) );
// Log it
if ( !( $e instanceof UsageException ) ) {
// Reset and print just the error message
ob_clean();
- // If the error occurred during printing, do a printer->profileOut()
- $this->mPrinter->safeProfileOut();
$this->printResult( true );
}
*
* @since 1.23
* @param Exception $e
+ * @throws Exception
*/
public static function handleApiBeforeMainException( Exception $e ) {
ob_start();
* If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
* and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
* headers are set.
+ * http://www.w3.org/TR/cors/#resource-requests
+ * http://www.w3.org/TR/cors/#resource-preflight-requests
*
* @return bool False if the caller should abort (403 case), true otherwise (all other cases)
*/
$request = $this->getRequest();
$response = $request->response();
+
// Origin: header is a space-separated list of origins, check all of them
$originHeader = $request->getHeader( 'Origin' );
if ( $originHeader === false ) {
$origins = array();
} else {
- $origins = explode( ' ', $originHeader );
+ $originHeader = trim( $originHeader );
+ $origins = preg_split( '/\s+/', $originHeader );
}
if ( !in_array( $originParam, $origins ) ) {
}
$config = $this->getConfig();
- $matchOrigin = self::matchOrigin(
+ $matchOrigin = count( $origins ) === 1 && self::matchOrigin(
$originParam,
$config->get( 'CrossSiteAJAXdomains' ),
$config->get( 'CrossSiteAJAXdomainExceptions' )
);
if ( $matchOrigin ) {
- $response->header( "Access-Control-Allow-Origin: $originParam" );
+ $requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
+ $preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
+ if ( $preflight ) {
+ // This is a CORS preflight request
+ if ( $requestedMethod !== 'POST' && $requestedMethod !== 'GET' ) {
+ // If method is not a case-sensitive match, do not set any additional headers and terminate.
+ return true;
+ }
+ // We allow the actual request to send the following headers
+ $requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
+ if ( $requestedHeaders !== false ) {
+ if ( !self::matchRequestedHeaders( $requestedHeaders ) ) {
+ return true;
+ }
+ $response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
+ }
+
+ // We only allow the actual request to be GET or POST
+ $response->header( 'Access-Control-Allow-Methods: POST, GET' );
+ }
+
+ $response->header( "Access-Control-Allow-Origin: $originHeader" );
$response->header( 'Access-Control-Allow-Credentials: true' );
- $response->header( 'Access-Control-Allow-Headers: Api-User-Agent' );
- $this->getOutput()->addVaryHeader( 'Origin' );
+ $response->header( "Timing-Allow-Origin: $originHeader" ); # http://www.w3.org/TR/resource-timing/#timing-allow-origin
+
+ if ( !$preflight ) {
+ $response->header( 'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag' );
+ }
}
+ $this->getOutput()->addVaryHeader( 'Origin' );
return true;
}
return false;
}
+ /**
+ * Attempt to validate the value of Access-Control-Request-Headers against a list
+ * of headers that we allow the follow up request to send.
+ *
+ * @param string $requestedHeaders Comma seperated list of HTTP headers
+ * @return bool True if all requested headers are in the list of allowed headers
+ */
+ protected static function matchRequestedHeaders( $requestedHeaders ) {
+ if ( trim( $requestedHeaders ) === '' ) {
+ return true;
+ }
+ $requestedHeaders = explode( ',', $requestedHeaders );
+ $allowedAuthorHeaders = array_flip( array(
+ /* simple headers (see spec) */
+ 'accept',
+ 'accept-language',
+ 'content-language',
+ 'content-type',
+ /* non-authorable headers in XHR, which are however requested by some UAs */
+ 'accept-encoding',
+ 'dnt',
+ 'origin',
+ /* MediaWiki whitelist */
+ 'api-user-agent',
+ ) );
+ foreach ( $requestedHeaders as $rHeader ) {
+ $rHeader = strtolower( trim( $rHeader ) );
+ if ( !isset( $allowedAuthorHeaders[$rHeader] ) ) {
+ wfDebugLog( 'api', 'CORS preflight failed on requested header: ' . $rHeader );
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Helper function to convert wildcard string into a regex
* '*' => '.*?'
$wildcard
);
- return "/https?:\/\/$wildcard/";
+ return "/^https?:\/\/$wildcard$/";
}
protected function sendCacheHeaders() {
$out->addVaryHeader( 'X-Forwarded-Proto' );
}
+ // The logic should be:
+ // $this->mCacheControl['max-age'] is set?
+ // Use it, the module knows better than our guess.
+ // !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
+ // Use 0 because we can guess caching is probably the wrong thing to do.
+ // Use $this->getParameter( 'maxage' ), which already defaults to 0.
+ $maxage = 0;
+ if ( isset( $this->mCacheControl['max-age'] ) ) {
+ $maxage = $this->mCacheControl['max-age'];
+ } elseif ( ( $this->mModule && !$this->mModule->isWriteMode() ) ||
+ $this->mCacheMode !== 'private'
+ ) {
+ $maxage = $this->getParameter( 'maxage' );
+ }
+ $privateCache = 'private, must-revalidate, max-age=' . $maxage;
+
if ( $this->mCacheMode == 'private' ) {
- $response->header( 'Cache-Control: private' );
+ $response->header( "Cache-Control: $privateCache" );
return;
}
$response->header( $out->getXVO() );
if ( $out->haveCacheVaryCookies() ) {
// Logged in, mark this request private
- $response->header( 'Cache-Control: private' );
+ $response->header( "Cache-Control: $privateCache" );
return;
}
// Logged out, send normal public headers below
} elseif ( session_id() != '' ) {
// Logged in or otherwise has session (e.g. anonymous users who have edited)
// Mark request private
- $response->header( 'Cache-Control: private' );
+ $response->header( "Cache-Control: $privateCache" );
return;
} // else no XVO and anonymous, send public headers below
// Public cache not requested
// Sending a Vary header in this case is harmless, and protects us
// against conditional calls of setCacheMaxAge().
- $response->header( 'Cache-Control: private' );
+ $response->header( "Cache-Control: $privateCache" );
return;
}
// Printer may not be able to handle errors. This is particularly
// likely if the module returns something for getCustomPrinter().
if ( !$this->mPrinter->canPrintErrors() ) {
- $this->mPrinter->safeProfileOut();
$this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
}
$errMessage = array(
'code' => 'internal_api_error_' . get_class( $e ),
- 'info' => $info,
- );
- ApiResult::setContent(
- $errMessage,
- $config->get( 'ShowExceptionDetails' ) ? "\n\n{$e->getTraceAsString()}\n\n" : ''
+ 'info' => '[' . MWExceptionHandler::getLogId( $e ) . '] ' . $info,
);
+ if ( $config->get( 'ShowExceptionDetails' ) ) {
+ ApiResult::setContent(
+ $errMessage,
+ MWExceptionHandler::getRedactedTraceAsString( $e )
+ );
+ }
}
// Remember all the warnings to re-add them later
/**
* Set up the module for response
* @return ApiBase The module that will handle this action
+ * @throws MWException
+ * @throws UsageException
*/
protected function setupModule() {
// Instantiate the module requested by the user
// Allow extensions to stop execution for arbitrary reasons.
$message = false;
- if ( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) {
+ if ( !Hooks::run( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) {
$this->dieUsageMsg( $message );
}
}
$this->checkAsserts( $params );
// Execute
- $module->profileIn();
$module->execute();
- wfRunHooks( 'APIAfterExecute', array( &$module ) );
- $module->profileOut();
+ Hooks::run( 'APIAfterExecute', array( &$module ) );
$this->reportUnusedParams();
$this->getResult()->cleanUpUTF8();
$printer = $this->mPrinter;
- $printer->profileIn();
$printer->initPrinter( false );
-
$printer->execute();
$printer->closePrinter();
- $printer->profileOut();
}
/**