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  'AllowCrossOrigin',
17  'RestAllowCrossOriginCookieAuth',
18  'CanonicalServer',
19  'CrossSiteAJAXdomains',
20  'CrossSiteAJAXdomainExceptions',
21  ];
22 
24  private $options;
25 
28 
30  private $user;
31 
37  public function __construct(
41  ) {
42  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
43  $this->options = $options;
44  $this->responseFactory = $responseFactory;
45  $this->user = $user;
46  }
47 
56  public function authorize( RequestInterface $request, Handler $handler ) {
57  // Handlers that need write access are by definition a cache-miss, therefore there is no
58  // need to vary by the origin.
59  if (
60  $handler->needsWriteAccess()
61  && $request->hasHeader( 'Origin' )
62  && !$this->user->isRegistered()
63  ) {
64  $origin = Origin::parseHeaderList( $request->getHeader( 'Origin' ) );
65 
66  if ( !$this->allowOrigin( $origin ) ) {
67  return 'rest-cross-origin-anon-write';
68  }
69  }
70 
71  return null;
72  }
73 
78  private function allowOrigin( Origin $origin ) : bool {
79  $allowed = array_merge( [ $this->getCanonicalDomain() ], $this->options->get( 'CrossSiteAJAXdomains' ) );
80  $excluded = $this->options->get( 'CrossSiteAJAXdomainExceptions' );
81 
82  return $origin->match( $allowed, $excluded );
83  }
84 
88  private function getCanonicalDomain() : string {
89  [
90  'host' => $host,
91  ] = wfParseUrl( $this->options->get( 'CanonicalServer' ) );
92 
93  return $host;
94  }
95 
106  public function modifyResponse( RequestInterface $request, ResponseInterface $response ) : ResponseInterface {
107  if ( !$this->options->get( 'AllowCrossOrigin' ) ) {
108  return $response;
109  }
110 
111  $allowedOrigin = '*';
112 
113  if ( $this->options->get( 'RestAllowCrossOriginCookieAuth' ) ) {
114  // @TODO Since we only Vary the response if (1) the method is OPTIONS or (2) the user is
115  // registered, it is safe to only add the Vary: Origin when those two conditions
116  // are met since a response to a logged-in user's request is not cachable.
117  // Therefore, logged out users should always get `Access-Control-Allow-Origin: *`
118  // on all non-OPTIONS request and logged-in users *may* get
119  // `Access-Control-Allow-Origin: <requested origin>`
120 
121  // Vary All Requests by the Origin header.
122  $response->addHeader( 'Vary', 'Origin' );
123 
124  // If the Origin header is missing, there is nothing to check against.
125  if ( $request->hasHeader( 'Origin' ) ) {
126  $origin = Origin::parseHeaderList( $request->getHeader( 'Origin' ) );
127  if ( $this->allowOrigin( $origin ) ) {
128  // Only set the allowed origin for preflight requests, or for main requests where a registered
129  // user is authenticated. This prevents having to Vary all requests by the Origin.
130  // Anonymous users will always get '*', registered users *may* get the requested origin back.
131  if ( $request->getMethod() === 'OPTIONS' || $this->user->isRegistered() ) {
132  $allowedOrigin = $origin->getSingleOrigin();
133  }
134  }
135  }
136  }
137 
138  // If the Origin was determined to be something other than *any* allow the session
139  // cookies to be sent with the main request. If this is the main request, allow the
140  // response to be read.
141  //
142  // If the client includes the credentials on a simple request (HEAD, GET, etc.), but
143  // they do not pass this check, the browser will refuse to allow the client to read the
144  // response. The client may resolve this by repeating the request without the
145  // credentials.
146  if ( $allowedOrigin !== '*' ) {
147  $response->setHeader( 'Access-Control-Allow-Credentials', 'true' );
148  }
149 
150  $response->setHeader( 'Access-Control-Allow-Origin', $allowedOrigin );
151 
152  return $response;
153  }
154 
161  public function createPreflightResponse( array $allowedMethods ) : ResponseInterface {
162  $response = $this->responseFactory->createNoContent();
163 
164  // Authorization header must be explicitly listed which prevent the use of '*'
165  $response->setHeader( 'Access-Control-Allow-Headers', [
166  'Authorization',
167  'Content-Type',
168  'If-Mach',
169  'If-None-Match',
170  'If-Modified-Since',
171  ] );
172 
173  $response->setHeader( 'Access-Control-Allow-Methods', $allowedMethods );
174 
175  return $response;
176  }
177 }
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:30
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:34
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:797
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:27
MediaWiki\Rest\CorsUtils\getCanonicalDomain
getCanonicalDomain()
Definition: CorsUtils.php:88
MediaWiki\Rest\CorsUtils\$options
ServiceOptions $options
Definition: CorsUtils.php:14
MediaWiki\Rest\CorsUtils\allowOrigin
allowOrigin(Origin $origin)
Definition: CorsUtils.php:78
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:106
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:37
MediaWiki\Rest\CorsUtils\createPreflightResponse
createPreflightResponse(array $allowedMethods)
Create a CORS preflight response.
Definition: CorsUtils.php:161
MediaWiki\Rest\ResponseInterface\addHeader
addHeader( $name, $value)
Append the given value to the specified header.
MediaWiki\Rest\CorsUtils\$responseFactory
ResponseFactory $responseFactory
Definition: CorsUtils.php:27
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:56
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:66