CorsWrapper::gatherCorsMiddleware()   A
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 10
c 1
b 0
f 0
nc 8
nop 1
dl 0
loc 22
ccs 0
cts 14
cp 0
crap 30
rs 9.6111
1
<?php
2
3
namespace ShiftOneLabs\LaravelCors\Http\Middleware;
4
5
use Closure;
6
use Throwable;
7
use Illuminate\Routing\Route;
8
use Illuminate\Routing\Router;
9
use Illuminate\Routing\Pipeline;
10
use Illuminate\Container\Container;
11
use Illuminate\Contracts\Http\Kernel;
12
use ShiftOneLabs\LaravelCors\CorsService;
13
use Symfony\Component\HttpFoundation\Response;
14
15
class CorsWrapper
16
{
17
    /** @var \Illuminate\Container\Container $container */
18
    protected $container;
19
20
    /** @var \Illuminate\Foundation\Http\Kernel $kernel */
21
    protected $kernel;
22
23
    /** @var \Illuminate\Routing\Router $router */
24
    protected $router;
25
26
    /** @var \ShiftOneLabs\LaravelCors\CorsService $cors */
27
    protected $cors;
28
29
    /**
30
     * Create a new middleware instance.
31
     *
32
     * @param  \Illuminate\Foundation\Http\Kernel  $kernel
33
     * @param  \Illuminate\Routing\Router  $router
34
     *
35
     * @return void
36
     */
37 46
    public function __construct(Container $container, Kernel $kernel, Router $router, CorsService $cors)
38
    {
39 46
        $this->container = $container;
40 46
        $this->kernel = $kernel;
0 ignored issues
show
Documentation Bug introduced by
$kernel is of type Illuminate\Contracts\Http\Kernel, but the property $kernel was declared to be of type Illuminate\Foundation\Http\Kernel. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof 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 given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
41 46
        $this->router = $router;
42 46
        $this->cors = $cors;
43 46
    }
44
45
    /**
46
     * Handle an incoming request.
47
     *
48
     * This middleware is intended to be a global middleware that wraps around
49
     * the entire stack. The "before" part will be the first middleware to
50
     * take action, and the "after" part will be the last.
51
     *
52
     * The "before" middleware section is responsible for making sure this
53
     * middleware only handles CORS requests, as well as short-circuiting
54
     * the call stack for CORS preflight requests.
55
     *
56
     * This "after" middleware section is responsible for acting as a fallback
57
     * to handle CORS requests that run into errors. For example, if an error
58
     * occurs before a CORS policy can be applied, the returned error won't
59
     * include the proper CORS headers, and the client won't be able to
60
     * access the error message. This middleware would handle that and
61
     * apply the proper headers.
62
     *
63
     * @param  \Illuminate\Http\Request  $request
64
     * @param  \Closure  $next
65
     *
66
     * @return mixed
67
     */
68 46
    public function handle($request, Closure $next)
69
    {
70
        // Skip if it is not a CORS request.
71 46
        if (!$this->cors->isCorsRequest($request)) {
72 46
            return $next($request);
73
        }
74
75
        // Handle CORS preflight requests and short circuit the call stack.
76
        if ($this->cors->isPreflightRequest($request)) {
77
            return $this->handlePreflightRequest($request, $next);
78
        }
79
80
        // Run the call stack.
81
        $response = $next($request);
82
83
        // Ensure we are working with a Response object.
84
        $response = Router::toResponse($request, $response);
85
86
        // Skip if the CORS middleware has already processed the request. If
87
        // the request is forbidden or has a bad method/header, the normal
88
        // CORS headers aren't added, so we check for a custom one.
89
        if ($response->headers->has('X-S1L-CORS-HANDLED')) {
90
            return $this->prepareResponse($response);
91
        }
92
93
        // If the response is not an error, there were no exceptions to
94
        // get in the way of all the middleware getting processed.
95
        if ($response->getStatusCode() < 400 && empty($response->exception)) {
96
            return $response;
97
        }
98
99
        $route = $request->route();
100
101
        // The route won't be set if it wasn't found or if there was an error in
102
        // a global before middleware. See if we can find the route to get the
103
        // route specific middleware. If not, global will still apply.
104
        if (empty($route)) {
105
            try {
106
                $route = $this->router->getRoutes()->match($request);
107
            } catch (Throwable $e) {
108
                $route = null;
109
            }
110
        }
111
112
        $corsMiddleware = $this->gatherCorsMiddleware($route);
0 ignored issues
show
Bug introduced by
It seems like $route can also be of type string; however, parameter $route of ShiftOneLabs\LaravelCors...:gatherCorsMiddleware() does only seem to accept Illuminate\Routing\Route|null, 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

112
        $corsMiddleware = $this->gatherCorsMiddleware(/** @scrutinizer ignore-type */ $route);
Loading history...
113
114
        // Skip if there are no CORS middleware to run the request through.
115
        if (empty($corsMiddleware)) {
116
            return $response;
117
        }
118
119
        // Process all the CORS middleware for the request.
120
        return $this->prepareResponse(
121
            (new Pipeline($this->container))
122
                ->send($request)
123
                ->through($corsMiddleware)
124
                ->then(function ($request) use ($response) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

124
                ->then(function (/** @scrutinizer ignore-unused */ $request) use ($response) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
125
                    return $response;
126
                })
127
        );
128
    }
129
130
    /**
131
     * Handle a CORS preflight request.
132
     *
133
     * @param  \Illuminate\Http\Request  $request
134
     * @param  \Closure  $next
135
     *
136
     * @return \Symfony\Component\HttpFoundation\Response
137
     */
138
    protected function handlePreflightRequest($request, Closure $next)
139
    {
140
        $corsMiddleware = $this->gatherCorsPreflightMiddleware($request);
141
142
        // Skip if there are no CORS middleware to run the request through.
143
        if (empty($corsMiddleware)) {
144
            return $next($request);
145
        }
146
147
        // Process all the CORS middleware for the request.
148
        return $this->prepareResponse(
149
            (new Pipeline($this->container))
150
                ->send($request)
151
                ->through($corsMiddleware)
152
                ->then(function ($request) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

152
                ->then(function (/** @scrutinizer ignore-unused */ $request) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
153
                    return 'CORS preflight onion core.';
154
                })
155
        );
156
    }
157
158
    /**
159
     * Get all the CORS middleware applied to a route, including the global
160
     * middleware.
161
     *
162
     * @param  \Illuminate\Routing\Route  $route
163
     *
164
     * @return array
165
     */
166
    protected function gatherCorsMiddleware(Route $route = null)
167
    {
168
        $corsMiddleware = [];
169
170
        // Get the global CORS policy middleware.
171
        if ($this->kernel->hasMiddleware(ApplyCorsPolicy::class)) {
172
            $corsMiddleware[] = ApplyCorsPolicy::class;
173
        }
174
175
        if (empty($route)) {
176
            return $corsMiddleware;
177
        }
178
179
        // Gather all the route CORS policy middlewares.
180
        foreach ($this->router->gatherRouteMiddleware($route) as $routeMiddleware) {
181
            list($class, $parameters) = array_pad(explode(':', $routeMiddleware, 2), 2, '');
182
            if ($class == ApplyCorsPolicy::class) {
183
                $corsMiddleware[] = $routeMiddleware;
184
            }
185
        }
186
187
        return $corsMiddleware;
188
    }
189
190
    /**
191
     * Get all the CORS middleware applied to a route specified by a preflight
192
     * request.
193
     *
194
     * @param  \Illuminate\Http\Request  $request
195
     *
196
     * @return array
197
     */
198
    protected function gatherCorsPreflightMiddleware($request)
199
    {
200
        // The preflight request uses the OPTIONS method. We need to temporarily
201
        // replace this method with the preflight target method in order for
202
        // the router to be able to find the intended actual route.
203
204
        // Save off the original method.
205
        $originalMethod = $request->getMethod();
206
207
        // Update the request with the intended method.
208
        $method = $request->headers->get('Access-Control-Request-Method');
209
        $request->setMethod($method);
210
211
        // Find the route with the proper request.
212
        try {
213
            $route = $this->router->getRoutes()->match($request);
214
        } catch (Throwable $e) {
215
            $route = null;
216
        }
217
218
        // Reset the method on the request with the original method.
219
        $request->setMethod($originalMethod);
220
221
        return $this->gatherCorsMiddleware($route);
222
    }
223
224
    /**
225
     * Prepare the response that has been handled by the CORS middleware.
226
     *
227
     * @param  \Symfony\Component\HttpFoundation\Response  $response
228
     *
229
     * @return \Symfony\Component\HttpFoundation\Response
230
     */
231
    protected function prepareResponse(Response $response)
232
    {
233
        $response->headers->remove('X-S1L-CORS-HANDLED');
234
235
        return $response;
236
    }
237
}
238