CorsService::handlePreflightRequest()   B
last analyzed

Complexity

Conditions 11
Paths 130

Size

Total Lines 48
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 11.0044

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 25
c 1
b 0
f 0
nc 130
nop 1
dl 0
loc 48
ccs 29
cts 30
cp 0.9667
crap 11.0044
rs 7.0666

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
namespace ShiftOneLabs\LaravelCors;
4
5
use Symfony\Component\HttpFoundation\Request;
6
use Symfony\Component\HttpFoundation\Response;
7
8
/**
9
 * Cors Service
10
 *
11
 * A lot of this code is derived from the asm89/stack-cors package. There were
12
 * a few things that needed tweaking, though, and the package's service class
13
 * uses private visibility, so I couldn't extend and override.
14
 *
15
 * @see https://github.com/asm89/stack-cors
16
 */
17
class CorsService
18
{
19
    /** @var array $options */
20
    protected $options;
21
22
    /**
23
     * Create a new service instance.
24
     *
25
     * @param  array  $options
26
     *
27
     * @return void
28
     */
29 598
    public function __construct(array $options = [])
30
    {
31 598
        $this->options = $this->normalizeOptions($options);
32 598
    }
33
34
    /**
35
     * Normalize the options into something more usable for the library.
36
     *
37
     * @param  array  $options
38
     *
39
     * @return array
40
     */
41 598
    protected function normalizeOptions(array $options = [])
42
    {
43
        $options += [
44 598
            'allowedOrigins' => [],
45 65
            'allowedOriginsPatterns' => [],
46 65
            'supportsCredentials' => false,
47 65
            'allowedHeaders' => [],
48 65
            'exposedHeaders' => [],
49 65
            'allowedMethods' => [],
50 65
            'maxAge' => 0,
51
        ];
52
53 598
        if (in_array('*', $options['allowedOrigins'])) {
54 368
            $options['allowedOrigins'] = true;
55 40
        }
56
57 598
        if (in_array('*', $options['allowedHeaders'])) {
58 184
            $options['allowedHeaders'] = true;
59 20
        } else {
60 506
            $options['allowedHeaders'] = array_map('strtolower', $options['allowedHeaders']);
61
        }
62
63 598
        if (in_array('*', $options['allowedMethods'])) {
64 276
            $options['allowedMethods'] = true;
65 30
        } else {
66 506
            $options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']);
67
        }
68
69 598
        return $options;
70
    }
71
72
    /**
73
     * Check if the request is a valid Cors request.
74
     *
75
     * @param  \Symfony\Component\HttpFoundation\Request  $request
76
     *
77
     * @return bool
78
     */
79 276
    public function isCorsRequest(Request $request)
80
    {
81 276
        return $request->headers->has('Origin') && $this->isCrossOrigin($request);
82
    }
83
84
    /**
85
     * Check if the request is a valid Cors preflight request.
86
     *
87
     * @param  \Symfony\Component\HttpFoundation\Request  $request
88
     *
89
     * @return bool
90
     */
91 184
    public function isPreflightRequest(Request $request)
92
    {
93 184
        return $this->isCorsRequest($request)
94 184
            && $request->getMethod() === 'OPTIONS'
95 184
            && $request->headers->has('Access-Control-Request-Method');
96
    }
97
98
    /**
99
     * Check the request origin to determine if the request is allowed.
100
     *
101
     * For the base CORS service, the request method is only checked during
102
     * preflight requests. If an invalid method is used for an actual
103
     * request, that should be handled at the application level.
104
     *
105
     * @param  \Symfony\Component\HttpFoundation\Request  $request
106
     *
107
     * @return bool
108
     */
109 92
    public function isActualRequestAllowed(Request $request)
110
    {
111 92
        return $this->isOriginAllowed($request);
112
    }
113
114
    /**
115
     * Add the valid Cors headers to the response.
116
     *
117
     * @param  \Symfony\Component\HttpFoundation\Response  $response
118
     * @param  \Symfony\Component\HttpFoundation\Request  $request
119
     *
120
     * @return \Symfony\Component\HttpFoundation\Response
121
     */
122 46
    public function addActualRequestHeaders(Response $response, Request $request)
123
    {
124 46
        if (!$this->isActualRequestAllowed($request)) {
125 46
            return $response;
126
        }
127
128 46
        if (!$response->headers->has('Vary')) {
129 46
            $response->headers->set('Vary', 'Origin');
130 5
        } else {
131
            $response->headers->set('Vary', $response->headers->get('Vary') . ', Origin');
132
        }
133
134 46
        $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
135
136 46
        if ($this->options['supportsCredentials']) {
137 46
            $response->headers->set('Access-Control-Allow-Credentials', 'true');
138 5
        }
139
140 46
        if ($this->options['exposedHeaders']) {
141 46
            $response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders']));
142 5
        }
143
144 46
        return $response;
145
    }
146
147
    /**
148
     * Get the full response for a Cors preflight request.
149
     *
150
     * This method deviates from the official preflight request algorithm. In the
151
     * official algorithm, if any of the origin, method, or headers is invalid,
152
     * no allow headers are added. So, if the origin is valid, but the header
153
     * is not supported, the allow origin header would not be added, and the
154
     * client would report an origin error, even though the origin is valid.
155
     *
156
     * This algorithm will only not add headers if the origin is not allowed (to
157
     * prevent leaking information). If the origin is allowed, then add all the
158
     * CORS response headers so the client can validate if the response is
159
     * valid and give the appropriate error message if not.
160
     *
161
     * The only exception to this algorithm is when the request method is not
162
     * allowed and it is a simple request method. For simple request methods,
163
     * clients do not validate the method against the allowed method header,
164
     * so we need to remove the allowed origin header to reject the request.
165
     *
166
     * @see https://www.w3.org/TR/cors/#resource-preflight-requests
167
     *
168
     * @param  \Symfony\Component\HttpFoundation\Request  $request
169
     *
170
     * @return \Symfony\Component\HttpFoundation\Response
171
     */
172 46
    public function handlePreflightRequest(Request $request)
173
    {
174
        // Preflight responses, even rejected ones, should return a 204
175
        // no body response.
176 46
        $response = $this->createResponse(null, 204);
177
178 46
        if (!$response->headers->has('Vary')) {
179 46
            $response->headers->set('Vary', 'Origin');
180 5
        } else {
181
            $response->headers->set('Vary', $response->headers->get('Vary') . ', Origin');
182
        }
183
184 46
        if (!$this->isOriginAllowed($request)) {
185 46
            return $response;
186
        }
187
188 46
        $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
189
190
        // Clients ignore the Access-Control-Allow-Methods header for simple
191
        // request methods. In order to reject requests for simple methods
192
        // that aren't allowed, we disallow the origin.
193 46
        if (!$this->isMethodAllowed($request) && $this->isSimpleMethod($request)) {
194 46
            $response->headers->remove('Access-Control-Allow-Origin');
195 5
        }
196
197 46
        $allowMethods = $this->options['allowedMethods'] === true
198 46
            ? strtoupper($request->headers->get('Access-Control-Request-Method'))
199 46
            : implode(', ', $this->options['allowedMethods']);
200 46
        $response->headers->set('Access-Control-Allow-Methods', $allowMethods);
201
202 46
        $allowHeaders = $this->options['allowedHeaders'] === true
203 46
            ? strtoupper($request->headers->get('Access-Control-Request-Headers'))
204 46
            : implode(', ', $this->options['allowedHeaders']);
205 46
        $response->headers->set('Access-Control-Allow-Headers', $allowHeaders);
206
207 46
        if ($this->options['supportsCredentials']) {
208 46
            $response->headers->set('Access-Control-Allow-Credentials', 'true');
209 5
        }
210
211 46
        if ($this->options['maxAge']) {
212 46
            $response->headers->set('Access-Control-Max-Age', $this->options['maxAge']);
213 5
        }
214
215 46
        if ($this->isMethodAllowed($request) && $this->isHeadersAllowed($request)) {
216 46
            $response->headers->set('X-CORS-PREFLIGHT-SUCCESS', 'true');
217 5
        }
218
219 46
        return $response;
220
    }
221
222
    /**
223
     * Check if a given response is a successful preflight response.
224
     *
225
     * @param  \Symfony\Component\HttpFoundation\Response  $response
226
     *
227
     * @return bool
228
     */
229 46
    public function isPreflightSuccessful(Response $response)
230
    {
231
        // Only successful preflight responses will have this header.
232 46
        return $response->headers->has('X-CORS-PREFLIGHT-SUCCESS');
233
    }
234
235
    /**
236
     * Check if a given response is a rejected preflight response.
237
     *
238
     * @param  \Symfony\Component\HttpFoundation\Response  $response
239
     *
240
     * @return bool
241
     */
242 46
    public function isPreflightRejected(Response $response)
243
    {
244 46
        return !$this->isPreflightSuccessful($response);
245
    }
246
247
    /**
248
     * Check if the request is actually cross origin (the origin isn't the same
249
     * as the request host).
250
     *
251
     * @param \Symfony\Component\HttpFoundation\Request $request
252
     *
253
     * @return bool
254
     */
255 230
    protected function isCrossOrigin(Request $request)
256
    {
257 230
        return $request->headers->get('Origin') !== $request->getSchemeAndHttpHost();
258
    }
259
260
    /**
261
     * Check if the request origin is allowed by the CORS configuration.
262
     *
263
     * @param  \Symfony\Component\HttpFoundation\Request  $request
264
     *
265
     * @return bool
266
     */
267 230
    protected function isOriginAllowed(Request $request)
268
    {
269 230
        if ($this->options['allowedOrigins'] === true) {
270 230
            return true;
271
        }
272
273 230
        $origin = $request->headers->get('Origin');
274
275 230
        if (in_array($origin, $this->options['allowedOrigins'])) {
276 92
            return true;
277
        }
278
279 230
        foreach ($this->options['allowedOriginsPatterns'] as $pattern) {
280
            if (preg_match($pattern, $origin)) {
281
                return true;
282
            }
283 25
        }
284
285 230
        return false;
286
    }
287
288
    /**
289
     * Check if the request method is allowed by the CORS configuration.
290
     *
291
     * @param  \Symfony\Component\HttpFoundation\Request  $request
292
     *
293
     * @return bool
294
     */
295 138
    protected function isMethodAllowed(Request $request)
296
    {
297 138
        if ($this->options['allowedMethods'] === true) {
298 138
            return true;
299
        }
300
301 138
        $method = $this->getActualRequestMethod($request);
302
303 138
        return in_array($method, $this->options['allowedMethods']);
304
    }
305
306
    /**
307
     * Check if the request headers are allowed by the CORS configuration.
308
     *
309
     * @param  \Symfony\Component\HttpFoundation\Request  $request
310
     *
311
     * @return bool
312
     */
313 46
    protected function isHeadersAllowed(Request $request)
314
    {
315 46
        if ($this->options['allowedHeaders'] === true || !$request->headers->has('Access-Control-Request-Headers')) {
316 46
            return true;
317
        }
318
319 46
        $headers = strtolower($request->headers->get('Access-Control-Request-Headers'));
320 46
        $headers = array_filter(array_map('trim', explode(',', $headers)));
321
322 46
        return empty(array_diff($headers, $this->options['allowedHeaders']));
323
    }
324
325
    /**
326
     * Check if this a simple request method.
327
     *
328
     * @see https://www.w3.org/TR/cors/#simple-method
329
     *
330
     * @param  \Symfony\Component\HttpFoundation\Request  $request
331
     *
332
     * @return bool
333
     */
334 46
    protected function isSimpleMethod(Request $request)
335
    {
336 46
        $method = $this->getActualRequestMethod($request);
337
338 46
        return in_array($method, ['GET', 'HEAD', 'POST']);
339
    }
340
341
    /**
342
     * Get the request method for the actual request, even if the current
343
     * request is a preflight request.
344
     *
345
     * @param  \Symfony\Component\HttpFoundation\Request  $request
346
     *
347
     * @return string
348
     */
349 138
    protected function getActualRequestMethod(Request $request)
350
    {
351 138
        return $this->isPreflightRequest($request)
352 56
            ? strtoupper($request->headers->get('Access-Control-Request-Method'))
353 138
            : $request->getMethod();
354
    }
355
356
    /**
357
     * Create a response object.
358
     *
359
     * @param  string|null  $content
360
     * @param  int  $status
361
     * @param  array  $headers
362
     *
363
     * @return \Symfony\Component\HttpFoundation\Response
364
     */
365 46
    protected function createResponse($content = '', $status = 200, $headers = [])
366
    {
367 46
        return new Response($content, $status, $headers);
368
    }
369
}
370