1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
4
|
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
5
|
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
6
|
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
7
|
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
8
|
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
9
|
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
10
|
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
11
|
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
12
|
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
13
|
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
14
|
|
|
* |
15
|
|
|
* This software consists of voluntary contributions made by many individuals |
16
|
|
|
* and is licensed under the MIT license. |
17
|
|
|
*/ |
18
|
|
|
|
19
|
|
|
namespace ZfrCors\Service; |
20
|
|
|
|
21
|
|
|
use Zend\Mvc\Router\Http\RouteMatch as DeprecatedRouteMatch; |
22
|
|
|
use Zend\Router\Http\RouteMatch; |
23
|
|
|
use Zend\Http\Header; |
24
|
|
|
use Zend\Uri\UriFactory; |
25
|
|
|
use ZfrCors\Exception\DisallowedOriginException; |
26
|
|
|
use ZfrCors\Exception\InvalidOriginException; |
27
|
|
|
use ZfrCors\Options\CorsOptions; |
28
|
|
|
use Zend\Http\Request as HttpRequest; |
29
|
|
|
use Zend\Http\Response as HttpResponse; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Service that offers a simple mechanism to handle CORS requests |
33
|
|
|
* |
34
|
|
|
* This service closely follow the specification here: https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS |
35
|
|
|
* |
36
|
|
|
* @license MIT |
37
|
|
|
* @author Florent Blaison <[email protected]> |
38
|
|
|
*/ |
39
|
|
|
class CorsService |
40
|
|
|
{ |
41
|
|
|
/** |
42
|
|
|
* @var CorsOptions |
43
|
|
|
*/ |
44
|
|
|
protected $options; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @param CorsOptions $options |
48
|
|
|
*/ |
49
|
|
|
public function __construct(CorsOptions $options) |
50
|
|
|
{ |
51
|
|
|
$this->options = $options; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Check if the HTTP request is a CORS request by checking if the Origin header is present and that the |
56
|
|
|
* request URI is not the same as the one in the Origin |
57
|
|
|
* |
58
|
|
|
* @param HttpRequest $request |
59
|
|
|
* @return bool |
60
|
|
|
*/ |
61
|
|
|
public function isCorsRequest(HttpRequest $request) |
62
|
|
|
{ |
63
|
|
|
$headers = $request->getHeaders(); |
64
|
|
|
|
65
|
|
|
if (! $headers->has('Origin')) { |
66
|
|
|
return false; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
$origin = $headers->get('Origin'); |
70
|
|
|
|
71
|
|
|
if (! $origin instanceof Header\Origin) { |
72
|
|
|
throw InvalidOriginException::fromInvalidHeaderValue(); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
$originUri = UriFactory::factory($origin->getFieldValue()); |
76
|
|
|
$requestUri = $request->getUri(); |
77
|
|
|
|
78
|
|
|
// According to the spec (http://tools.ietf.org/html/rfc6454#section-4), we should check host, port and scheme |
79
|
|
|
|
80
|
|
|
return (! ($originUri->getHost() === $requestUri->getHost()) |
81
|
|
|
|| ! ($originUri->getPort() === $requestUri->getPort()) |
82
|
|
|
|| ! ($originUri->getScheme() === $requestUri->getScheme()) |
83
|
|
|
); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Check if the CORS request is a preflight request |
88
|
|
|
* |
89
|
|
|
* @param HttpRequest $request |
90
|
|
|
* @return bool |
91
|
|
|
*/ |
92
|
|
|
public function isPreflightRequest(HttpRequest $request) |
93
|
|
|
{ |
94
|
|
|
return $this->isCorsRequest($request) |
95
|
|
|
&& strtoupper($request->getMethod()) === 'OPTIONS' |
96
|
|
|
&& $request->getHeaders()->has('Access-Control-Request-Method'); |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Create a preflight response by adding the corresponding headers |
101
|
|
|
* |
102
|
|
|
* @param HttpRequest $request |
103
|
|
|
* @return HttpResponse |
104
|
|
|
*/ |
105
|
|
|
public function createPreflightCorsResponse(HttpRequest $request) |
106
|
|
|
{ |
107
|
|
|
$response = new HttpResponse(); |
108
|
|
|
$response->setStatusCode(200); |
109
|
|
|
|
110
|
|
|
$headers = $response->getHeaders(); |
111
|
|
|
|
112
|
|
|
$headers->addHeaderLine('Access-Control-Allow-Origin', $this->getAllowedOriginValue($request)); |
113
|
|
|
$headers->addHeaderLine('Access-Control-Allow-Methods', implode(', ', $this->options->getAllowedMethods())); |
114
|
|
|
$headers->addHeaderLine('Access-Control-Allow-Headers', implode(', ', $this->options->getAllowedHeaders())); |
115
|
|
|
$headers->addHeaderLine('Access-Control-Max-Age', $this->options->getMaxAge()); |
116
|
|
|
$headers->addHeaderLine('Content-Length', 0); |
117
|
|
|
|
118
|
|
|
if ($this->options->getAllowedCredentials()) { |
119
|
|
|
$headers->addHeaderLine('Access-Control-Allow-Credentials', 'true'); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
return $response; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Create a preflight response by adding the correspoding headers which are merged with per-route configuration |
127
|
|
|
* |
128
|
|
|
* @param HttpRequest $request |
129
|
|
|
* @param RouteMatch|DeprecatedRouteMatch|null $routeMatch |
130
|
|
|
* |
131
|
|
|
* @return HttpResponse |
132
|
|
|
*/ |
133
|
|
|
public function createPreflightCorsResponseWithRouteOptions(HttpRequest $request, $routeMatch = null) |
134
|
|
|
{ |
135
|
|
|
$options = $this->options; |
136
|
|
|
if ($routeMatch instanceof RouteMatch || $routeMatch instanceof DeprecatedRouteMatch) { |
|
|
|
|
137
|
|
|
$options->setFromArray($routeMatch->getParam(CorsOptions::ROUTE_PARAM) ?: []); |
138
|
|
|
} |
139
|
|
|
$response = $this->createPreflightCorsResponse($request); |
140
|
|
|
|
141
|
|
|
return $response; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Populate a simple CORS response |
146
|
|
|
* |
147
|
|
|
* @param HttpRequest $request |
148
|
|
|
* @param HttpResponse $response |
149
|
|
|
* @param null|RouteMatch $routeMatch |
150
|
|
|
* @return HttpResponse |
151
|
|
|
* @throws DisallowedOriginException If the origin is not allowed |
152
|
|
|
*/ |
153
|
|
|
public function populateCorsResponse(HttpRequest $request, HttpResponse $response, $routeMatch = null) |
154
|
|
|
{ |
155
|
|
|
if ($routeMatch instanceof RouteMatch || $routeMatch instanceof DeprecatedRouteMatch) { |
|
|
|
|
156
|
|
|
$this->options->setFromArray($routeMatch->getParam(CorsOptions::ROUTE_PARAM) ?: []); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
$origin = $this->getAllowedOriginValue($request); |
160
|
|
|
|
161
|
|
|
// If $origin is "null", then it means that the origin is not allowed. As this is |
162
|
|
|
// a simple request, it is useless to continue the processing as it will be refused |
163
|
|
|
// by the browser anyway, so we throw an exception |
164
|
|
|
if ($origin === 'null') { |
165
|
|
|
$origin = $request->getHeader('Origin'); |
166
|
|
|
$originHeader = $origin ? $origin->getFieldValue() : ''; |
|
|
|
|
167
|
|
|
throw new DisallowedOriginException( |
168
|
|
|
sprintf( |
169
|
|
|
'The origin "%s" is not authorized', |
170
|
|
|
$originHeader |
171
|
|
|
) |
172
|
|
|
); |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
$headers = $response->getHeaders(); |
176
|
|
|
$headers->addHeaderLine('Access-Control-Allow-Origin', $origin); |
177
|
|
|
$headers->addHeaderLine('Access-Control-Expose-Headers', implode(', ', $this->options->getExposedHeaders())); |
178
|
|
|
|
179
|
|
|
$headers = $this->ensureVaryHeader($response); |
180
|
|
|
|
181
|
|
|
if ($this->options->getAllowedCredentials()) { |
182
|
|
|
$headers->addHeaderLine('Access-Control-Allow-Credentials', 'true'); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
$response->setHeaders($headers); |
186
|
|
|
|
187
|
|
|
return $response; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Get a single value for the "Access-Control-Allow-Origin" header |
192
|
|
|
* |
193
|
|
|
* According to the spec, it is not valid to set multiple origins separated by commas. Only accepted |
194
|
|
|
* value are wildcard ("*"), an exact domain or a null string. |
195
|
|
|
* |
196
|
|
|
* @link http://www.w3.org/TR/cors/#access-control-allow-origin-response-header |
197
|
|
|
* @param HttpRequest $request |
198
|
|
|
* @return string |
199
|
|
|
*/ |
200
|
|
|
protected function getAllowedOriginValue(HttpRequest $request) |
201
|
|
|
{ |
202
|
|
|
$allowedOrigins = $this->options->getAllowedOrigins(); |
203
|
|
|
|
204
|
|
|
$origin = $request->getHeader('Origin'); |
205
|
|
|
|
206
|
|
|
if ($origin) { |
207
|
|
|
$origin = $origin->getFieldValue(); |
|
|
|
|
208
|
|
|
if (in_array('*', $allowedOrigins)) { |
209
|
|
|
return $origin; |
210
|
|
|
} |
211
|
|
|
foreach ($allowedOrigins as $allowedOrigin) { |
212
|
|
|
if (fnmatch($allowedOrigin, $origin)) { |
213
|
|
|
return $origin; |
214
|
|
|
} |
215
|
|
|
} |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
return 'null'; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Ensure that the Vary header is set. |
223
|
|
|
* |
224
|
|
|
* |
225
|
|
|
* @link http://www.w3.org/TR/cors/#resource-implementation |
226
|
|
|
* @param HttpResponse $response |
227
|
|
|
* @return \Zend\Http\Headers |
228
|
|
|
*/ |
229
|
|
|
public function ensureVaryHeader(HttpResponse $response) |
230
|
|
|
{ |
231
|
|
|
$headers = $response->getHeaders(); |
232
|
|
|
// If the origin is not "*", we should add the "Origin" value to the "Vary" header |
233
|
|
|
// See more: http://www.w3.org/TR/cors/#resource-implementation |
234
|
|
|
$allowedOrigins = $this->options->getAllowedOrigins(); |
235
|
|
|
|
236
|
|
|
if (in_array('*', $allowedOrigins)) { |
237
|
|
|
return $headers; |
238
|
|
|
} |
239
|
|
|
if ($headers->has('Vary')) { |
240
|
|
|
$varyHeader = $headers->get('Vary'); |
241
|
|
|
$varyValue = $varyHeader->getFieldValue() . ', Origin'; |
|
|
|
|
242
|
|
|
|
243
|
|
|
$headers->removeHeader($varyHeader); |
|
|
|
|
244
|
|
|
$headers->addHeaderLine('Vary', $varyValue); |
245
|
|
|
} else { |
246
|
|
|
$headers->addHeaderLine('Vary', 'Origin'); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
return $headers; |
250
|
|
|
} |
251
|
|
|
} |
252
|
|
|
|
This error could be the result of:
1. Missing dependencies
PHP Analyzer uses your
composer.json
file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects thecomposer.json
to be in the root folder of your repository.Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the
require
orrequire-dev
section?2. Missing use statement
PHP does not complain about undefined classes in
ìnstanceof
checks. For example, the following PHP code will work perfectly fine:If you have not tested against this specific condition, such errors might go unnoticed.