1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @link https://www.yiiframework.com/ |
4
|
|
|
* @copyright Copyright (c) 2008 Yii Software LLC |
5
|
|
|
* @license https://www.yiiframework.com/license/ |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace yii\filters; |
9
|
|
|
|
10
|
|
|
use Yii; |
11
|
|
|
use yii\base\ActionFilter; |
12
|
|
|
use yii\base\InvalidConfigException; |
13
|
|
|
use yii\web\Request; |
14
|
|
|
use yii\web\Response; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Cors filter implements [Cross Origin Resource Sharing](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing). |
18
|
|
|
* |
19
|
|
|
* Make sure to read carefully what CORS does and does not. CORS do not secure your API, |
20
|
|
|
* but allow the developer to grant access to third party code (ajax calls from external domain). |
21
|
|
|
* |
22
|
|
|
* You may use CORS filter by attaching it as a behavior to a controller or module, like the following, |
23
|
|
|
* |
24
|
|
|
* ```php |
25
|
|
|
* public function behaviors() |
26
|
|
|
* { |
27
|
|
|
* return [ |
28
|
|
|
* 'corsFilter' => [ |
29
|
|
|
* 'class' => \yii\filters\Cors::class, |
30
|
|
|
* ], |
31
|
|
|
* ]; |
32
|
|
|
* } |
33
|
|
|
* ``` |
34
|
|
|
* |
35
|
|
|
* The CORS filter can be specialized to restrict parameters, like this, |
36
|
|
|
* [MDN CORS Information](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) |
37
|
|
|
* |
38
|
|
|
* ```php |
39
|
|
|
* public function behaviors() |
40
|
|
|
* { |
41
|
|
|
* return [ |
42
|
|
|
* 'corsFilter' => [ |
43
|
|
|
* 'class' => \yii\filters\Cors::class, |
44
|
|
|
* 'cors' => [ |
45
|
|
|
* // restrict access to |
46
|
|
|
* 'Origin' => ['http://www.myserver.com', 'https://www.myserver.com'], |
47
|
|
|
* // Allow only POST and PUT methods |
48
|
|
|
* 'Access-Control-Request-Method' => ['POST', 'PUT'], |
49
|
|
|
* // Allow only headers 'X-Wsse' |
50
|
|
|
* 'Access-Control-Request-Headers' => ['X-Wsse'], |
51
|
|
|
* // Allow credentials (cookies, authorization headers, etc.) to be exposed to the browser |
52
|
|
|
* 'Access-Control-Allow-Credentials' => true, |
53
|
|
|
* // Allow OPTIONS caching |
54
|
|
|
* 'Access-Control-Max-Age' => 3600, |
55
|
|
|
* // Allow the X-Pagination-Current-Page header to be exposed to the browser. |
56
|
|
|
* 'Access-Control-Expose-Headers' => ['X-Pagination-Current-Page'], |
57
|
|
|
* ], |
58
|
|
|
* |
59
|
|
|
* ], |
60
|
|
|
* ]; |
61
|
|
|
* } |
62
|
|
|
* ``` |
63
|
|
|
* |
64
|
|
|
* For more information on how to add the CORS filter to a controller, see |
65
|
|
|
* the [Guide on REST controllers](guide:rest-controllers#cors). |
66
|
|
|
* |
67
|
|
|
* @author Philippe Gaultier <[email protected]> |
68
|
|
|
* @since 2.0 |
69
|
|
|
*/ |
70
|
|
|
class Cors extends ActionFilter |
71
|
|
|
{ |
72
|
|
|
/** |
73
|
|
|
* @var Request|null the current request. If not set, the `request` application component will be used. |
74
|
|
|
*/ |
75
|
|
|
public $request; |
76
|
|
|
/** |
77
|
|
|
* @var Response|null the response to be sent. If not set, the `response` application component will be used. |
78
|
|
|
*/ |
79
|
|
|
public $response; |
80
|
|
|
/** |
81
|
|
|
* @var array define specific CORS rules for specific actions |
82
|
|
|
*/ |
83
|
|
|
public $actions = []; |
84
|
|
|
/** |
85
|
|
|
* @var array Basic headers handled for the CORS requests. |
86
|
|
|
*/ |
87
|
|
|
public $cors = [ |
88
|
|
|
'Origin' => ['*'], |
89
|
|
|
'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], |
90
|
|
|
'Access-Control-Request-Headers' => ['*'], |
91
|
|
|
'Access-Control-Allow-Credentials' => null, |
92
|
|
|
'Access-Control-Max-Age' => 86400, |
93
|
|
|
'Access-Control-Expose-Headers' => [], |
94
|
|
|
]; |
95
|
|
|
|
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* {@inheritdoc} |
99
|
|
|
*/ |
100
|
3 |
|
public function beforeAction($action) |
101
|
|
|
{ |
102
|
3 |
|
$this->request = $this->request ?: Yii::$app->getRequest(); |
|
|
|
|
103
|
3 |
|
$this->response = $this->response ?: Yii::$app->getResponse(); |
|
|
|
|
104
|
|
|
|
105
|
3 |
|
$this->overrideDefaultSettings($action); |
106
|
|
|
|
107
|
3 |
|
$requestCorsHeaders = $this->extractHeaders(); |
108
|
3 |
|
$responseCorsHeaders = $this->prepareHeaders($requestCorsHeaders); |
109
|
3 |
|
$this->addCorsHeaders($this->response, $responseCorsHeaders); |
|
|
|
|
110
|
|
|
|
111
|
3 |
|
if ($this->request->isOptions && $this->request->headers->has('Access-Control-Request-Method')) { |
|
|
|
|
112
|
|
|
// it is CORS preflight request, respond with 200 OK without further processing |
113
|
1 |
|
$this->response->setStatusCode(200); |
|
|
|
|
114
|
1 |
|
return false; |
115
|
|
|
} |
116
|
|
|
|
117
|
3 |
|
return true; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Override settings for specific action. |
122
|
|
|
* @param \yii\base\Action $action the action settings to override |
123
|
|
|
*/ |
124
|
3 |
|
public function overrideDefaultSettings($action) |
125
|
|
|
{ |
126
|
3 |
|
$actionId = $this->getActionId($action); |
127
|
|
|
|
128
|
3 |
|
if (isset($this->actions[$actionId])) { |
129
|
|
|
$actionParams = $this->actions[$actionId]; |
130
|
|
|
$actionParamsKeys = array_keys($actionParams); |
131
|
|
|
foreach ($this->cors as $headerField => $headerValue) { |
132
|
|
|
if (in_array($headerField, $actionParamsKeys)) { |
133
|
|
|
$this->cors[$headerField] = $actionParams[$headerField]; |
134
|
|
|
} |
135
|
|
|
} |
136
|
|
|
} |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Extract CORS headers from the request. |
141
|
|
|
* @return array CORS headers to handle |
142
|
|
|
*/ |
143
|
3 |
|
public function extractHeaders() |
144
|
|
|
{ |
145
|
3 |
|
$headers = []; |
146
|
3 |
|
foreach (array_keys($this->cors) as $headerField) { |
147
|
3 |
|
$serverField = $this->headerizeToPhp($headerField); |
148
|
3 |
|
$headerData = isset($_SERVER[$serverField]) ? $_SERVER[$serverField] : null; |
149
|
3 |
|
if ($headerData !== null) { |
150
|
2 |
|
$headers[$headerField] = $headerData; |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
154
|
3 |
|
return $headers; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* For each CORS headers create the specific response. |
159
|
|
|
* @param array $requestHeaders CORS headers we have detected |
160
|
|
|
* @return array CORS headers ready to be sent |
161
|
|
|
*/ |
162
|
3 |
|
public function prepareHeaders($requestHeaders) |
163
|
|
|
{ |
164
|
3 |
|
$responseHeaders = []; |
165
|
|
|
// handle Origin |
166
|
3 |
|
if (isset($requestHeaders['Origin'], $this->cors['Origin'])) { |
167
|
2 |
|
if (in_array($requestHeaders['Origin'], $this->cors['Origin'], true)) { |
168
|
|
|
$responseHeaders['Access-Control-Allow-Origin'] = $requestHeaders['Origin']; |
169
|
|
|
} |
170
|
|
|
|
171
|
2 |
|
if (in_array('*', $this->cors['Origin'], true)) { |
172
|
|
|
// Per CORS standard (https://fetch.spec.whatwg.org), wildcard origins shouldn't be used together with credentials |
173
|
2 |
|
if (isset($this->cors['Access-Control-Allow-Credentials']) && $this->cors['Access-Control-Allow-Credentials']) { |
174
|
|
|
if (YII_DEBUG) { |
175
|
|
|
throw new InvalidConfigException("Allowing credentials for wildcard origins is insecure. Please specify more restrictive origins or set 'credentials' to false in your CORS configuration."); |
176
|
|
|
} else { |
177
|
|
|
Yii::error("Allowing credentials for wildcard origins is insecure. Please specify more restrictive origins or set 'credentials' to false in your CORS configuration.", __METHOD__); |
178
|
|
|
} |
179
|
|
|
} else { |
180
|
2 |
|
$responseHeaders['Access-Control-Allow-Origin'] = '*'; |
181
|
|
|
} |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
|
185
|
3 |
|
$this->prepareAllowHeaders('Headers', $requestHeaders, $responseHeaders); |
186
|
|
|
|
187
|
3 |
|
if (isset($requestHeaders['Access-Control-Request-Method'])) { |
188
|
|
|
$responseHeaders['Access-Control-Allow-Methods'] = implode(', ', $this->cors['Access-Control-Request-Method']); |
189
|
|
|
} |
190
|
|
|
|
191
|
3 |
|
if (isset($this->cors['Access-Control-Allow-Credentials'])) { |
192
|
1 |
|
$responseHeaders['Access-Control-Allow-Credentials'] = $this->cors['Access-Control-Allow-Credentials'] ? 'true' : 'false'; |
193
|
|
|
} |
194
|
|
|
|
195
|
3 |
|
if (isset($this->cors['Access-Control-Max-Age']) && $this->request->getIsOptions()) { |
|
|
|
|
196
|
1 |
|
$responseHeaders['Access-Control-Max-Age'] = $this->cors['Access-Control-Max-Age']; |
197
|
|
|
} |
198
|
|
|
|
199
|
3 |
|
if (isset($this->cors['Access-Control-Expose-Headers'])) { |
200
|
1 |
|
$responseHeaders['Access-Control-Expose-Headers'] = implode(', ', $this->cors['Access-Control-Expose-Headers']); |
201
|
|
|
} |
202
|
|
|
|
203
|
3 |
|
if (isset($this->cors['Access-Control-Allow-Headers'])) { |
204
|
1 |
|
$responseHeaders['Access-Control-Allow-Headers'] = implode(', ', $this->cors['Access-Control-Allow-Headers']); |
205
|
|
|
} |
206
|
|
|
|
207
|
3 |
|
return $responseHeaders; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Handle classic CORS request to avoid duplicate code. |
212
|
|
|
* @param string $type the kind of headers we would handle |
213
|
|
|
* @param array $requestHeaders CORS headers request by client |
214
|
|
|
* @param array $responseHeaders CORS response headers sent to the client |
215
|
|
|
*/ |
216
|
3 |
|
protected function prepareAllowHeaders($type, $requestHeaders, &$responseHeaders) |
217
|
|
|
{ |
218
|
3 |
|
$requestHeaderField = 'Access-Control-Request-' . $type; |
219
|
3 |
|
$responseHeaderField = 'Access-Control-Allow-' . $type; |
220
|
3 |
|
if (!isset($requestHeaders[$requestHeaderField], $this->cors[$requestHeaderField])) { |
221
|
3 |
|
return; |
222
|
|
|
} |
223
|
|
|
if (in_array('*', $this->cors[$requestHeaderField])) { |
224
|
|
|
$responseHeaders[$responseHeaderField] = $this->headerize($requestHeaders[$requestHeaderField]); |
225
|
|
|
} else { |
226
|
|
|
$requestedData = preg_split('/[\\s,]+/', $requestHeaders[$requestHeaderField], -1, PREG_SPLIT_NO_EMPTY); |
227
|
|
|
$acceptedData = array_uintersect($requestedData, $this->cors[$requestHeaderField], 'strcasecmp'); |
228
|
|
|
if (!empty($acceptedData)) { |
229
|
|
|
$responseHeaders[$responseHeaderField] = implode(', ', $acceptedData); |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Adds the CORS headers to the response. |
236
|
|
|
* @param Response $response |
237
|
|
|
* @param array $headers CORS headers which have been computed |
238
|
|
|
*/ |
239
|
3 |
|
public function addCorsHeaders($response, $headers) |
240
|
|
|
{ |
241
|
3 |
|
if (empty($headers) === false) { |
242
|
3 |
|
$responseHeaders = $response->getHeaders(); |
243
|
3 |
|
foreach ($headers as $field => $value) { |
244
|
3 |
|
$responseHeaders->set($field, $value); |
245
|
|
|
} |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Convert any string (including php headers with HTTP prefix) to header format. |
251
|
|
|
* |
252
|
|
|
* Example: |
253
|
|
|
* - X-PINGOTHER -> X-Pingother |
254
|
|
|
* - X_PINGOTHER -> X-Pingother |
255
|
|
|
* @param string $string string to convert |
256
|
|
|
* @return string the result in "header" format |
257
|
|
|
*/ |
258
|
|
|
protected function headerize($string) |
259
|
|
|
{ |
260
|
|
|
$headers = preg_split('/[\\s,]+/', $string, -1, PREG_SPLIT_NO_EMPTY); |
261
|
|
|
$headers = array_map(function ($element) { |
262
|
|
|
return str_replace(' ', '-', ucwords(strtolower(str_replace(['_', '-'], [' ', ' '], $element)))); |
263
|
|
|
}, $headers); |
264
|
|
|
return implode(', ', $headers); |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* Convert any string (including php headers with HTTP prefix) to header format. |
269
|
|
|
* |
270
|
|
|
* Example: |
271
|
|
|
* - X-Pingother -> HTTP_X_PINGOTHER |
272
|
|
|
* - X PINGOTHER -> HTTP_X_PINGOTHER |
273
|
|
|
* @param string $string string to convert |
274
|
|
|
* @return string the result in "php $_SERVER header" format |
275
|
|
|
*/ |
276
|
3 |
|
protected function headerizeToPhp($string) |
277
|
|
|
{ |
278
|
3 |
|
return 'HTTP_' . strtoupper(str_replace([' ', '-'], ['_', '_'], $string)); |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.