MediaWiki REL1_37
CorsUtils.php
Go to the documentation of this file.
1<?php
2
3namespace 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}
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.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,...
__construct(ServiceOptions $options, ResponseFactory $responseFactory, UserIdentity $user)
Definition CorsUtils.php:38
ResponseFactory $responseFactory
Definition CorsUtils.php:28
allowOrigin(Origin $origin)
Definition CorsUtils.php:79
ServiceOptions $options
Definition CorsUtils.php:25
createPreflightResponse(array $allowedMethods)
Create a CORS preflight response.
authorize(RequestInterface $request, Handler $handler)
Only allow registered users to make unsafe cross-origin requests.
Definition CorsUtils.php:57
modifyResponse(RequestInterface $request, ResponseInterface $response)
Modify response to allow for CORS.
Base class for REST route handlers.
Definition Handler.php:17
needsWriteAccess()
Indicates whether this route requires write access.
Definition Handler.php:395
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
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.