* @defgroup API API
*/
+use MediaWiki\Logger\LoggerFactory;
+
/**
* This is the main API class, used for both external and internal processing.
* When executed, it will create the requested formatter object,
'compare' => 'ApiComparePages',
'tokens' => 'ApiTokens',
'checktoken' => 'ApiCheckToken',
+ 'cspreport' => 'ApiCSPReport',
// Write modules
'purge' => 'ApiPurge',
// @codingStandardsIgnoreStart String contenation on "msg" not allowed to break long line
/**
* List of user roles that are specifically relevant to the API.
- * array( 'right' => array ( 'msg' => 'Some message with a $1',
- * 'params' => array ( $someVarToSubst ) ),
- * );
+ * [ 'right' => [ 'msg' => 'Some message with a $1',
+ * 'params' => [ $someVarToSubst ] ],
+ * ];
*/
private static $mRights = [
'writeapi' => [
*/
private $mPrinter;
- private $mModuleMgr, $mResult, $mErrorFormatter, $mContinuationManager;
+ private $mModuleMgr, $mResult, $mErrorFormatter;
+ /** @var ApiContinuationManager|null */
+ private $mContinuationManager;
private $mAction;
private $mEnableWrite;
private $mInternalMode, $mSquidMaxage;
if ( isset( $request ) ) {
$this->getContext()->setRequest( $request );
+ } else {
+ $request = $this->getRequest();
}
- $this->mInternalMode = ( $this->getRequest() instanceof FauxRequest );
+ $this->mInternalMode = ( $request instanceof FauxRequest );
// Special handling for the main module: $parent === $this
parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
+ $config = $this->getConfig();
+
if ( !$this->mInternalMode ) {
- // Impose module restrictions.
- // If the current user cannot read,
- // Remove all modules other than login
- global $wgUser;
+ // Log if a request with a non-whitelisted Origin header is seen
+ // with session cookies.
+ $originHeader = $request->getHeader( 'Origin' );
+ if ( $originHeader === false ) {
+ $origins = [];
+ } else {
+ $originHeader = trim( $originHeader );
+ $origins = preg_split( '/\s+/', $originHeader );
+ }
+ $sessionCookies = array_intersect(
+ array_keys( $_COOKIE ),
+ MediaWiki\Session\SessionManager::singleton()->getVaryCookies()
+ );
+ if ( $origins && $sessionCookies && (
+ count( $origins ) !== 1 || !self::matchOrigin(
+ $origins[0],
+ $config->get( 'CrossSiteAJAXdomains' ),
+ $config->get( 'CrossSiteAJAXdomainExceptions' )
+ )
+ ) ) {
+ LoggerFactory::getInstance( 'cors' )->warning(
+ 'Non-whitelisted CORS request with session cookies', [
+ 'origin' => $originHeader,
+ 'cookies' => $sessionCookies,
+ 'ip' => $request->getIP(),
+ 'userAgent' => $this->getUserAgent(),
+ 'wiki' => wfWikiID(),
+ ]
+ );
+ }
+ // If we're in a mode that breaks the same-origin policy, strip
+ // user credentials for security.
if ( $this->lacksSameOriginSecurity() ) {
- // If we're in a mode that breaks the same-origin policy, strip
- // user credentials for security.
+ global $wgUser;
wfDebug( "API: stripping user credentials when the same-origin policy is not applied\n" );
$wgUser = new User();
$this->getContext()->setUser( $wgUser );
}
}
- $config = $this->getConfig();
$this->mModuleMgr = new ApiModuleManager( $this );
$this->mModuleMgr->addModules( self::$Modules, 'action' );
$this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
return true;
}
+ // Anonymous CORS
+ if ( $request->getVal( 'origin' ) === '*' ) {
+ $this->lacksSameOriginSecurity = true;
+ return true;
+ }
+
// Header to be used from XMLHTTPRequest when the request might
// otherwise be used for XSS.
if ( $request->getHeader( 'Treat-as-Untrusted' ) !== false ) {
$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 = [];
+ $matchOrigin = false;
+ $allowTiming = false;
+ $varyOrigin = true;
+
+ if ( $originParam === '*' ) {
+ // Request for anonymous CORS
+ $matchOrigin = true;
+ $allowOrigin = '*';
+ $allowCredentials = 'false';
+ $varyOrigin = false; // No need to vary
} else {
- $originHeader = trim( $originHeader );
- $origins = preg_split( '/\s+/', $originHeader );
- }
+ // Non-anonymous CORS, check we allow the domain
- if ( !in_array( $originParam, $origins ) ) {
- // origin parameter set but incorrect
- // Send a 403 response
- $response->statusHeader( 403 );
- $response->header( 'Cache-Control: no-cache' );
- echo "'origin' parameter does not match Origin header\n";
+ // Origin: header is a space-separated list of origins, check all of them
+ $originHeader = $request->getHeader( 'Origin' );
+ if ( $originHeader === false ) {
+ $origins = [];
+ } else {
+ $originHeader = trim( $originHeader );
+ $origins = preg_split( '/\s+/', $originHeader );
+ }
- return false;
- }
+ if ( !in_array( $originParam, $origins ) ) {
+ // origin parameter set but incorrect
+ // Send a 403 response
+ $response->statusHeader( 403 );
+ $response->header( 'Cache-Control: no-cache' );
+ echo "'origin' parameter does not match Origin header\n";
- $config = $this->getConfig();
- $matchOrigin = count( $origins ) === 1 && self::matchOrigin(
- $originParam,
- $config->get( 'CrossSiteAJAXdomains' ),
- $config->get( 'CrossSiteAJAXdomainExceptions' )
- );
+ return false;
+ }
+
+ $config = $this->getConfig();
+ $matchOrigin = count( $origins ) === 1 && self::matchOrigin(
+ $originParam,
+ $config->get( 'CrossSiteAJAXdomains' ),
+ $config->get( 'CrossSiteAJAXdomainExceptions' )
+ );
+
+ $allowOrigin = $originHeader;
+ $allowCredentials = 'true';
+ $allowTiming = $originHeader;
+ }
if ( $matchOrigin ) {
$requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
$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-Origin: $allowOrigin" );
+ $response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
// http://www.w3.org/TR/resource-timing/#timing-allow-origin
- $response->header( "Timing-Allow-Origin: $originHeader" );
+ if ( $allowTiming !== false ) {
+ $response->header( "Timing-Allow-Origin: $allowTiming" );
+ }
if ( !$preflight ) {
$response->header(
}
}
- $this->getOutput()->addVaryHeader( 'Origin' );
+ if ( $varyOrigin ) {
+ $this->getOutput()->addVaryHeader( 'Origin' );
+ }
+
return true;
}
$this->dieUsageMsg( [ 'missingparam', 'token' ] );
}
- if ( !$this->getConfig()->get( 'DebugAPI' ) &&
- array_key_exists(
- $module->encodeParamName( 'token' ),
- $this->getRequest()->getQueryValues()
- )
- ) {
- $this->dieUsage(
- "The '{$module->encodeParamName( 'token' )}' parameter was " .
- 'found in the query string, but must be in the POST body',
- 'mustposttoken'
- );
- }
+ $module->requirePostedParameters( [ 'token' ] );
if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
$this->dieUsageMsg( 'sessionfailure' );
}
if ( $module->isWriteMode()
- && in_array( 'bot', $this->getUser()->getGroups() )
+ && $this->getUser()->isBot()
&& wfGetLB()->getServerCount() > 1
) {
$this->checkBotReadOnly();
}
}
- // If a majority of slaves are too lagged then disallow writes
- $slaveCount = wfGetLB()->getServerCount() - 1;
- if ( $numLagged >= ceil( $slaveCount / 2 ) ) {
+ // If a majority of replica DBs are too lagged then disallow writes
+ $replicaCount = wfGetLB()->getServerCount() - 1;
+ if ( $numLagged >= ceil( $replicaCount / 2 ) ) {
$laggedServers = implode( ', ', $laggedServers );
wfDebugLog(
'api-readonly',
protected function setRequestExpectations( ApiBase $module ) {
$limits = $this->getConfig()->get( 'TrxProfilerLimits' );
$trxProfiler = Profiler::instance()->getTransactionProfiler();
+ $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
if ( $this->getRequest()->hasSafeMethod() ) {
$trxProfiler->setExpectations( $limits['GET'], __METHOD__ );
} elseif ( $this->getRequest()->wasPosted() && !$module->isWriteMode() ) {
parent::__construct( $message, $code );
$this->mCodestr = $codestr;
$this->mExtraData = $extradata;
+
+ // This should never happen, so throw an exception about it that will
+ // hopefully get logged with a backtrace (T138585)
+ if ( !is_string( $codestr ) || $codestr === '' ) {
+ throw new InvalidArgumentException( 'Invalid $codestr, was ' .
+ ( $codestr === '' ? 'empty string' : gettype( $codestr ) )
+ );
+ }
}
/**