Cors   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 209
Duplicated Lines 0 %

Test Coverage

Coverage 70.27%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 80
c 1
b 0
f 0
dl 0
loc 209
ccs 52
cts 74
cp 0.7027
rs 9.52
wmc 36

8 Methods

Rating   Name   Duplication   Size   Complexity  
A beforeAction() 0 18 5
A addCorsHeaders() 0 6 3
A headerize() 0 7 1
A overrideDefaultSettings() 0 10 4
F prepareHeaders() 0 46 14
A headerizeToPhp() 0 3 1
A prepareAllowHeaders() 0 14 4
A extractHeaders() 0 12 4
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();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->request ?: Yii::app->getRequest() can also be of type yii\console\Request. However, the property $request is declared as type null|yii\web\Request. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
103 3
        $this->response = $this->response ?: Yii::$app->getResponse();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->response ?: Yii::app->getResponse() can also be of type yii\console\Response. However, the property $response is declared as type null|yii\web\Response. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $this->response can also be of type yii\console\Response; however, parameter $response of yii\filters\Cors::addCorsHeaders() does only seem to accept yii\web\Response, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

109
        $this->addCorsHeaders(/** @scrutinizer ignore-type */ $this->response, $responseCorsHeaders);
Loading history...
110
111 3
        if ($this->request->isOptions && $this->request->headers->has('Access-Control-Request-Method')) {
0 ignored issues
show
Bug Best Practice introduced by
The property headers does not exist on yii\console\Request. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property isOptions does not exist on yii\console\Request. Since you implemented __get, consider adding a @property annotation.
Loading history...
112
            // it is CORS preflight request, respond with 200 OK without further processing
113 1
            $this->response->setStatusCode(200);
0 ignored issues
show
Bug introduced by
The method setStatusCode() does not exist on yii\console\Response. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

113
            $this->response->/** @scrutinizer ignore-call */ 
114
                             setStatusCode(200);
Loading history...
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()) {
0 ignored issues
show
Bug introduced by
The method getIsOptions() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

195
        if (isset($this->cors['Access-Control-Max-Age']) && $this->request->/** @scrutinizer ignore-call */ getIsOptions()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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