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