MediaWiki  master
CorsUtils.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Rest;
4 
9 
15  public const CONSTRUCTOR_OPTIONS = [
16  'AllowedCorsHeaders',
17  'AllowCrossOrigin',
18  'RestAllowCrossOriginCookieAuth',
19  'CanonicalServer',
20  'CrossSiteAJAXdomains',
21  'CrossSiteAJAXdomainExceptions',
22  ];
23 
25  private $options;
26 
29 
31  private $user;
32 
38  public function __construct(
42  ) {
43  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
44  $this->options = $options;
45  $this->responseFactory = $responseFactory;
46  $this->user = $user;
47  }
48 
57  public function authorize( RequestInterface $request, Handler $handler ) {
58  // Handlers that need write access are by definition a cache-miss, therefore there is no
59  // need to vary by the origin.
60  if (
61  $handler->needsWriteAccess()
62  && $request->hasHeader( 'Origin' )
63  && !$this->user->isRegistered()
64  ) {
65  $origin = Origin::parseHeaderList( $request->getHeader( 'Origin' ) );
66 
67  if ( !$this->allowOrigin( $origin ) ) {
68  return 'rest-cross-origin-anon-write';
69  }
70  }
71 
72  return null;
73  }
74 
79  private function allowOrigin( Origin $origin ): bool {
80  $allowed = array_merge( [ $this->getCanonicalDomain() ], $this->options->get( 'CrossSiteAJAXdomains' ) );
81  $excluded = $this->options->get( 'CrossSiteAJAXdomainExceptions' );
82 
83  return $origin->match( $allowed, $excluded );
84  }
85 
89  private function getCanonicalDomain(): string {
90  [
91  'host' => $host,
92  ] = wfParseUrl( $this->options->get( 'CanonicalServer' ) );
93 
94  return $host;
95  }
96 
107  public function modifyResponse( RequestInterface $request, ResponseInterface $response ): ResponseInterface {
108  if ( !$this->options->get( 'AllowCrossOrigin' ) ) {
109  return $response;
110  }
111 
112  $allowedOrigin = '*';
113 
114  if ( $this->options->get( 'RestAllowCrossOriginCookieAuth' ) ) {
115  // @TODO Since we only Vary the response if (1) the method is OPTIONS or (2) the user is
116  // registered, it is safe to only add the Vary: Origin when those two conditions
117  // are met since a response to a logged-in user's request is not cachable.
118  // Therefore, logged out users should always get `Access-Control-Allow-Origin: *`
119  // on all non-OPTIONS request and logged-in users *may* get
120  // `Access-Control-Allow-Origin: <requested origin>`
121 
122  // Vary All Requests by the Origin header.
123  $response->addHeader( 'Vary', 'Origin' );
124 
125  // If the Origin header is missing, there is nothing to check against.
126  if ( $request->hasHeader( 'Origin' ) ) {
127  $origin = Origin::parseHeaderList( $request->getHeader( 'Origin' ) );
128  if ( $this->allowOrigin( $origin ) ) {
129  // Only set the allowed origin for preflight requests, or for main requests where a registered
130  // user is authenticated. This prevents having to Vary all requests by the Origin.
131  // Anonymous users will always get '*', registered users *may* get the requested origin back.
132  if ( $request->getMethod() === 'OPTIONS' || $this->user->isRegistered() ) {
133  $allowedOrigin = $origin->getSingleOrigin();
134  }
135  }
136  }
137  }
138 
139  // If the Origin was determined to be something other than *any* allow the session
140  // cookies to be sent with the main request. If this is the main request, allow the
141  // response to be read.
142  //
143  // If the client includes the credentials on a simple request (HEAD, GET, etc.), but
144  // they do not pass this check, the browser will refuse to allow the client to read the
145  // response. The client may resolve this by repeating the request without the
146  // credentials.
147  if ( $allowedOrigin !== '*' ) {
148  $response->setHeader( 'Access-Control-Allow-Credentials', 'true' );
149  }
150 
151  $response->setHeader( 'Access-Control-Allow-Origin', $allowedOrigin );
152 
153  return $response;
154  }
155 
162  public function createPreflightResponse( array $allowedMethods ): ResponseInterface {
163  $response = $this->responseFactory->createNoContent();
164  $response->setHeader( 'Access-Control-Allow-Methods', $allowedMethods );
165 
166  $allowedHeaders = $this->options->get( 'AllowedCorsHeaders' );
167  $allowedHeaders = array_merge( $allowedHeaders, array_diff( [
168  // Authorization header must be explicitly listed which prevent the use of '*'
169  'Authorization',
170  // REST must allow Content-Type to be operational
171  'Content-Type',
172  // REST relies on conditional requests for some endpoints
173  'If-Mach',
174  'If-None-Match',
175  'If-Modified-Since',
176  ], $allowedHeaders ) );
177  $response->setHeader( 'Access-Control-Allow-Headers', $allowedHeaders );
178 
179  return $response;
180  }
181 }
MediaWiki\Rest\ResponseFactory
Generates standardized response objects.
Definition: ResponseFactory.php:17
MediaWiki\Rest\CorsUtils
Definition: CorsUtils.php:13
MediaWiki\Rest\RequestInterface\getHeader
getHeader( $name)
Retrieves a message header value by the given case-insensitive name.
MediaWiki\Rest\CorsUtils\$user
UserIdentity $user
Definition: CorsUtils.php:31
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
MediaWiki\Rest\Handler
Base class for REST route handlers.
Definition: Handler.php:17
wfParseUrl
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
Definition: GlobalFunctions.php:776
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:27
MediaWiki\Rest\CorsUtils\getCanonicalDomain
getCanonicalDomain()
Definition: CorsUtils.php:89
MediaWiki\Rest\CorsUtils\$options
ServiceOptions $options
Definition: CorsUtils.php:14
MediaWiki\Rest\CorsUtils\allowOrigin
allowOrigin(Origin $origin)
Definition: CorsUtils.php:79
MediaWiki\Rest\ResponseInterface\setHeader
setHeader( $name, $value)
Set or replace the specified header.
MediaWiki\Rest\HeaderParser\Origin\parseHeaderList
static parseHeaderList(array $headerList)
Parse an Origin header list as returned by RequestInterface::getHeader().
Definition: Origin.php:28
MediaWiki\Rest\RequestInterface\hasHeader
hasHeader( $name)
Checks if a header exists by the given case-insensitive name.
MediaWiki\Rest\RequestInterface\getMethod
getMethod()
Retrieves the HTTP method of the request.
MediaWiki\Rest\CorsUtils\modifyResponse
modifyResponse(RequestInterface $request, ResponseInterface $response)
Modify response to allow for CORS.
Definition: CorsUtils.php:107
MediaWiki\Rest
MediaWiki\Rest\ResponseInterface
An interface similar to PSR-7's ResponseInterface, the primary difference being that it is mutable.
Definition: ResponseInterface.php:41
MediaWiki\Rest\Handler\needsWriteAccess
needsWriteAccess()
Indicates whether this route requires write access.
Definition: Handler.php:395
MediaWiki\Rest\RequestInterface
A request interface similar to PSR-7's ServerRequestInterface.
Definition: RequestInterface.php:39
MediaWiki\Rest\CorsUtils\__construct
__construct(ServiceOptions $options, ResponseFactory $responseFactory, UserIdentity $user)
Definition: CorsUtils.php:38
MediaWiki\Rest\CorsUtils\createPreflightResponse
createPreflightResponse(array $allowedMethods)
Create a CORS preflight response.
Definition: CorsUtils.php:162
MediaWiki\Rest\ResponseInterface\addHeader
addHeader( $name, $value)
Append the given value to the specified header.
MediaWiki\Rest\CorsUtils\$responseFactory
ResponseFactory $responseFactory
Definition: CorsUtils.php:28
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
MediaWiki\Rest\CorsUtils\authorize
authorize(RequestInterface $request, Handler $handler)
Only allow registered users to make unsafe cross-origin requests.
Definition: CorsUtils.php:57
MediaWiki\Rest\HeaderParser\Origin\match
match(array $allowList, array $excludeList)
Check whether all the origins match at least one of the rules in $allowList.
Definition: Origin.php:77
MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface
An interface used by Router to ensure that the client has "basic" access, i.e.
Definition: BasicAuthorizerInterface.php:14
MediaWiki\Config\ServiceOptions\assertRequiredOptions
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Definition: ServiceOptions.php:71