Cors::prepareHeaders()   F
last analyzed

Complexity

Conditions 14
Paths 338

Size

Total Lines 46
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 16.0138

Importance

Changes 0
Metric Value
cc 14
eloc 24
nc 338
nop 1
dl 0
loc 46
ccs 18
cts 23
cp 0.7826
crap 16.0138
rs 3.7083
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\filters;
10
11
use Yii;
12
use yii\base\ActionFilter;
13
use yii\base\InvalidConfigException;
14
use yii\web\Request;
15
use yii\web\Response;
16
17
/**
18
 * Cors filter implements [Cross Origin Resource Sharing](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing).
19
 *
20
 * Make sure to read carefully what CORS does and does not. CORS do not secure your API,
21
 * but allow the developer to grant access to third party code (ajax calls from external domain).
22
 *
23
 * You may use CORS filter by attaching it as a behavior to a controller or module, like the following,
24
 *
25
 * ```php
26
 * public function behaviors()
27
 * {
28
 *     return [
29
 *         'corsFilter' => [
30
 *             'class' => \yii\filters\Cors::class,
31
 *         ],
32
 *     ];
33
 * }
34
 * ```
35
 *
36
 * The CORS filter can be specialized to restrict parameters, like this,
37
 * [MDN CORS Information](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS)
38
 *
39
 * ```php
40
 * public function behaviors()
41
 * {
42
 *     return [
43
 *         'corsFilter' => [
44
 *             'class' => \yii\filters\Cors::class,
45
 *             'cors' => [
46
 *                 // restrict access to
47
 *                 'Origin' => ['http://www.myserver.com', 'https://www.myserver.com'],
48
 *                 // Allow only POST and PUT methods
49
 *                 'Access-Control-Request-Method' => ['POST', 'PUT'],
50
 *                 // Allow only headers 'X-Wsse'
51
 *                 'Access-Control-Request-Headers' => ['X-Wsse'],
52
 *                 // Allow credentials (cookies, authorization headers, etc.) to be exposed to the browser
53
 *                 'Access-Control-Allow-Credentials' => true,
54
 *                 // Allow OPTIONS caching
55
 *                 'Access-Control-Max-Age' => 3600,
56
 *                 // Allow the X-Pagination-Current-Page header to be exposed to the browser.
57
 *                 'Access-Control-Expose-Headers' => ['X-Pagination-Current-Page'],
58
 *             ],
59
 *
60
 *         ],
61
 *     ];
62
 * }
63
 * ```
64
 *
65
 * For more information on how to add the CORS filter to a controller, see
66
 * the [Guide on REST controllers](guide:rest-controllers#cors).
67
 *
68
 * @author Philippe Gaultier <[email protected]>
69
 * @since 2.0
70
 */
71
class Cors extends ActionFilter
72
{
73
    /**
74
     * @var Request|null the current request. If not set, the `request` application component will be used.
75
     */
76
    public $request;
77
    /**
78
     * @var Response|null the response to be sent. If not set, the `response` application component will be used.
79
     */
80
    public $response;
81
    /**
82
     * @var array define specific CORS rules for specific actions
83
     */
84
    public $actions = [];
85
    /**
86
     * @var array Basic headers handled for the CORS requests.
87
     */
88
    public $cors = [
89
        'Origin' => ['*'],
90
        'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
91
        'Access-Control-Request-Headers' => ['*'],
92
        'Access-Control-Allow-Credentials' => null,
93
        'Access-Control-Max-Age' => 86400,
94
        'Access-Control-Expose-Headers' => [],
95
    ];
96
97
98
    /**
99
     * {@inheritdoc}
100
     */
101 3
    public function beforeAction($action)
102
    {
103 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...
104 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...
105
106 3
        $this->overrideDefaultSettings($action);
107
108 3
        $requestCorsHeaders = $this->extractHeaders();
109 3
        $responseCorsHeaders = $this->prepareHeaders($requestCorsHeaders);
110 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

110
        $this->addCorsHeaders(/** @scrutinizer ignore-type */ $this->response, $responseCorsHeaders);
Loading history...
111
112 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...
113
            // it is CORS preflight request, respond with 200 OK without further processing
114 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

114
            $this->response->/** @scrutinizer ignore-call */ 
115
                             setStatusCode(200);
Loading history...
115 1
            return false;
116
        }
117
118 3
        return true;
119
    }
120
121
    /**
122
     * Override settings for specific action.
123
     * @param \yii\base\Action $action the action settings to override
124
     */
125 3
    public function overrideDefaultSettings($action)
126
    {
127 3
        if (isset($this->actions[$action->id])) {
128
            $actionParams = $this->actions[$action->id];
129
            $actionParamsKeys = array_keys($actionParams);
130
            foreach ($this->cors as $headerField => $headerValue) {
131
                if (in_array($headerField, $actionParamsKeys)) {
132
                    $this->cors[$headerField] = $actionParams[$headerField];
133
                }
134
            }
135
        }
136
    }
137
138
    /**
139
     * Extract CORS headers from the request.
140
     * @return array CORS headers to handle
141
     */
142 3
    public function extractHeaders()
143
    {
144 3
        $headers = [];
145 3
        foreach (array_keys($this->cors) as $headerField) {
146 3
            $serverField = $this->headerizeToPhp($headerField);
147 3
            $headerData = isset($_SERVER[$serverField]) ? $_SERVER[$serverField] : null;
148 3
            if ($headerData !== null) {
149 2
                $headers[$headerField] = $headerData;
150
            }
151
        }
152
153 3
        return $headers;
154
    }
155
156
    /**
157
     * For each CORS headers create the specific response.
158
     * @param array $requestHeaders CORS headers we have detected
159
     * @return array CORS headers ready to be sent
160
     */
161 3
    public function prepareHeaders($requestHeaders)
162
    {
163 3
        $responseHeaders = [];
164
        // handle Origin
165 3
        if (isset($requestHeaders['Origin'], $this->cors['Origin'])) {
166 2
            if (in_array($requestHeaders['Origin'], $this->cors['Origin'], true)) {
167
                $responseHeaders['Access-Control-Allow-Origin'] = $requestHeaders['Origin'];
168
            }
169
170 2
            if (in_array('*', $this->cors['Origin'], true)) {
171
                // Per CORS standard (https://fetch.spec.whatwg.org), wildcard origins shouldn't be used together with credentials
172 2
                if (isset($this->cors['Access-Control-Allow-Credentials']) && $this->cors['Access-Control-Allow-Credentials']) {
173
                    if (YII_DEBUG) {
174
                        throw new InvalidConfigException("Allowing credentials for wildcard origins is insecure. Please specify more restrictive origins or set 'credentials' to false in your CORS configuration.");
175
                    } else {
176
                        Yii::error("Allowing credentials for wildcard origins is insecure. Please specify more restrictive origins or set 'credentials' to false in your CORS configuration.", __METHOD__);
177
                    }
178
                } else {
179 2
                    $responseHeaders['Access-Control-Allow-Origin'] = '*';
180
                }
181
            }
182
        }
183
184 3
        $this->prepareAllowHeaders('Headers', $requestHeaders, $responseHeaders);
185
186 3
        if (isset($requestHeaders['Access-Control-Request-Method'])) {
187
            $responseHeaders['Access-Control-Allow-Methods'] = implode(', ', $this->cors['Access-Control-Request-Method']);
188
        }
189
190 3
        if (isset($this->cors['Access-Control-Allow-Credentials'])) {
191 1
            $responseHeaders['Access-Control-Allow-Credentials'] = $this->cors['Access-Control-Allow-Credentials'] ? 'true' : 'false';
192
        }
193
194 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

194
        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...
195 1
            $responseHeaders['Access-Control-Max-Age'] = $this->cors['Access-Control-Max-Age'];
196
        }
197
198 3
        if (isset($this->cors['Access-Control-Expose-Headers'])) {
199 1
            $responseHeaders['Access-Control-Expose-Headers'] = implode(', ', $this->cors['Access-Control-Expose-Headers']);
200
        }
201
202 3
        if (isset($this->cors['Access-Control-Allow-Headers'])) {
203 1
            $responseHeaders['Access-Control-Allow-Headers'] = implode(', ', $this->cors['Access-Control-Allow-Headers']);
204
        }
205
206 3
        return $responseHeaders;
207
    }
208
209
    /**
210
     * Handle classic CORS request to avoid duplicate code.
211
     * @param string $type the kind of headers we would handle
212
     * @param array $requestHeaders CORS headers request by client
213
     * @param array $responseHeaders CORS response headers sent to the client
214
     */
215 3
    protected function prepareAllowHeaders($type, $requestHeaders, &$responseHeaders)
216
    {
217 3
        $requestHeaderField = 'Access-Control-Request-' . $type;
218 3
        $responseHeaderField = 'Access-Control-Allow-' . $type;
219 3
        if (!isset($requestHeaders[$requestHeaderField], $this->cors[$requestHeaderField])) {
220 3
            return;
221
        }
222
        if (in_array('*', $this->cors[$requestHeaderField])) {
223
            $responseHeaders[$responseHeaderField] = $this->headerize($requestHeaders[$requestHeaderField]);
224
        } else {
225
            $requestedData = preg_split('/[\\s,]+/', $requestHeaders[$requestHeaderField], -1, PREG_SPLIT_NO_EMPTY);
226
            $acceptedData = array_uintersect($requestedData, $this->cors[$requestHeaderField], 'strcasecmp');
227
            if (!empty($acceptedData)) {
228
                $responseHeaders[$responseHeaderField] = implode(', ', $acceptedData);
229
            }
230
        }
231
    }
232
233
    /**
234
     * Adds the CORS headers to the response.
235
     * @param Response $response
236
     * @param array $headers CORS headers which have been computed
237
     */
238 3
    public function addCorsHeaders($response, $headers)
239
    {
240 3
        if (empty($headers) === false) {
241 3
            $responseHeaders = $response->getHeaders();
242 3
            foreach ($headers as $field => $value) {
243 3
                $responseHeaders->set($field, $value);
244
            }
245
        }
246
    }
247
248
    /**
249
     * Convert any string (including php headers with HTTP prefix) to header format.
250
     *
251
     * Example:
252
     *  - X-PINGOTHER -> X-Pingother
253
     *  - X_PINGOTHER -> X-Pingother
254
     * @param string $string string to convert
255
     * @return string the result in "header" format
256
     */
257
    protected function headerize($string)
258
    {
259
        $headers = preg_split('/[\\s,]+/', $string, -1, PREG_SPLIT_NO_EMPTY);
260
        $headers = array_map(function ($element) {
261
            return str_replace(' ', '-', ucwords(strtolower(str_replace(['_', '-'], [' ', ' '], $element))));
262
        }, $headers);
263
        return implode(', ', $headers);
264
    }
265
266
    /**
267
     * Convert any string (including php headers with HTTP prefix) to header format.
268
     *
269
     * Example:
270
     *  - X-Pingother -> HTTP_X_PINGOTHER
271
     *  - X PINGOTHER -> HTTP_X_PINGOTHER
272
     * @param string $string string to convert
273
     * @return string the result in "php $_SERVER header" format
274
     */
275 3
    protected function headerizeToPhp($string)
276
    {
277 3
        return 'HTTP_' . strtoupper(str_replace([' ', '-'], ['_', '_'], $string));
278
    }
279
}
280