Passed
Push — develop ( 36c2b4...1d782e )
by nguereza
03:02
created

CorsMiddleware::shouldBeProcessed()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 16
rs 10
1
<?php
2
3
/**
4
 * Platine Framework
5
 *
6
 * Platine Framework is a lightweight, high-performance, simple and elegant PHP
7
 * Web framework
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Framework
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
/**
33
 *  @file CorsMiddleware.php
34
 *
35
 *  The CORS middleware class is used to check the CORS policies
36
 *
37
 *  @package    Platine\Framework\Http\Middleware
38
 *  @author Platine Developers Team
39
 *  @copyright  Copyright (c) 2020
40
 *  @license    http://opensource.org/licenses/MIT  MIT License
41
 *  @link   http://www.iacademy.cf
42
 *  @version 1.0.0
43
 *  @filesource
44
 */
45
46
declare(strict_types=1);
47
48
namespace Platine\Framework\Http\Middleware;
49
50
use Platine\Config\Config;
51
use Platine\Http\Handler\MiddlewareInterface;
52
use Platine\Http\Handler\RequestHandlerInterface;
53
use Platine\Http\Response;
54
use Platine\Http\ResponseInterface;
55
use Platine\Http\ServerRequestInterface;
56
use Platine\Logger\LoggerInterface;
57
use Platine\Route\Route;
58
use Platine\Stdlib\Helper\Str;
59
60
/**
61
 * @class CorsMiddleware
62
 * @package Platine\Framework\Http\Middleware
63
 * @template T
64
 */
65
class CorsMiddleware implements MiddlewareInterface
66
{
67
68
    /**
69
     * The configuration instance
70
     * @var Config<T>
71
     */
72
    protected Config $config;
73
74
    /**
75
     * The logger instance
76
     * @var LoggerInterface
77
     */
78
    protected LoggerInterface $logger;
79
80
    /**
81
     * The server request to use
82
     * @var ServerRequestInterface
83
     */
84
    protected ServerRequestInterface $request;
85
86
    /**
87
     * The response to return
88
     * @var ResponseInterface
89
     */
90
    protected ResponseInterface $response;
91
92
    /**
93
     * Create new instance
94
     * @param LoggerInterface $logger
95
     * @param Config<T> $config
96
     */
97
    public function __construct(
98
        LoggerInterface $logger,
99
        Config $config
100
    ) {
101
        $this->config = $config;
102
        $this->logger = $logger;
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108
    public function process(
109
        ServerRequestInterface $request,
110
        RequestHandlerInterface $handler
111
    ): ResponseInterface {
112
        if (!$this->shouldBeProcessed($request)) {
113
            return $handler->handle($request);
114
        }
115
116
        $this->request = $request;
117
118
        if ($this->isPreflight()) {
119
            $response = new Response(204);
120
121
            $this->logger->info(
122
                'CORS Preflight Request for {method}:{url}',
123
                [
124
                    'method' => $request->getMethod(),
125
                    'url' => (string) $request->getUri(),
126
                ]
127
            );
128
        } else {
129
            $response = $handler->handle($request);
130
        }
131
132
        $this->response = $response;
133
134
        $this->setCorsHeaders();
135
136
        return $this->response;
137
    }
138
139
    /**
140
     * Whether we can process this request
141
     * @param ServerRequestInterface $request
142
     * @return bool
143
     */
144
    protected function shouldBeProcessed(ServerRequestInterface $request): bool
145
    {
146
        //If no route has been match no need check for CORS
147
        /** @var ?Route $route */
148
        $route = $request->getAttribute(Route::class);
149
        if (!$route) {
150
            return false;
151
        }
152
153
        //Check if the path match
154
        $path = $this->config->get('security.cors.path', '/');
155
        if (!preg_match('~^' . $path . '~', $route->getPattern())) {
156
            return false;
157
        }
158
159
        return true;
160
    }
161
162
    /**
163
     * Check if the current request is preflight
164
     * @return bool
165
     */
166
    protected function isPreflight(): bool
167
    {
168
        return $this->request->getMethod() === 'OPTIONS';
169
    }
170
171
    /**
172
     * Set the CORS headers
173
     * @return void
174
     */
175
    protected function setCorsHeaders(): void
176
    {
177
        if ($this->isPreflight()) {
178
            $this->setOrigin()
179
                 ->setMaxAge()
180
                 ->setAllowCredentials()
181
                 ->setAllowMethods()
182
                 ->setAllowHeaders();
183
        } else {
184
            $this->setOrigin()
185
                 ->setExposedHeaders()
186
                 ->setAllowCredentials();
187
        }
188
    }
189
190
    /**
191
     * Set Origin header
192
     * @return $this
193
     */
194
    protected function setOrigin(): self
195
    {
196
        $origins = $this->config->get('security.cors.origins', ['*']);
197
        // default to the first allowed origin
198
        $origin = reset($origins);
199
200
        foreach ($origins as $ori) {
201
            if ($ori === $this->request->getHeaderLine('Origin')) {
202
                $origin = $ori;
203
                break;
204
            }
205
        }
206
207
        $this->response = $this->response
208
                                ->withHeader(
209
                                    'Access-Control-Allow-Origin',
210
                                    $origin
211
                                );
212
213
        return $this;
214
    }
215
216
    /**
217
     * Set expose headers
218
     * @return $this
219
     */
220
    protected function setExposedHeaders(): self
221
    {
222
223
        $headers = $this->config->get('security.cors.expose_headers', []);
224
225
        if (!empty($headers)) {
226
            $this->response = $this->response
227
                                ->withHeader(
228
                                    'Access-Control-Expose-Headers',
229
                                    implode(', ', $headers)
230
                                );
231
        }
232
233
        return $this;
234
    }
235
236
    /**
237
     * Set max age header
238
     * @return $this
239
     */
240
    protected function setMaxAge(): self
241
    {
242
        $maxAge = $this->config->get('security.cors.max_age', 1800);
243
244
        $this->response = $this->response
245
                            ->withHeader(
246
                                'Access-Control-Max-Age',
247
                                Str::stringify($maxAge)
248
                            );
249
250
        return $this;
251
    }
252
253
    /**
254
     * Set Allow credentials header
255
     * @return $this
256
     */
257
    protected function setAllowCredentials(): self
258
    {
259
        $allowCredentials = $this->config->get('security.cors.allow_credentials', false);
260
261
        if ($allowCredentials) {
262
            $this->response = $this->response
263
                                ->withHeader(
264
                                    'Access-Control-Allow-Credential',
265
                                    Str::stringify($allowCredentials)
266
                                );
267
        }
268
269
        return $this;
270
    }
271
272
    /**
273
     * Set allow methods header
274
     * @return $this
275
     */
276
    protected function setAllowMethods(): self
277
    {
278
        $methods = $this->config->get('security.cors.allow_methods', []);
279
280
        if (!empty($methods)) {
281
            $this->response = $this->response
282
                                ->withHeader(
283
                                    'Access-Control-Allow-Methods',
284
                                    implode(', ', $methods)
285
                                );
286
        }
287
288
        return $this;
289
    }
290
291
    /**
292
     * Set Allow headers
293
     * @return $this
294
     */
295
    protected function setAllowHeaders(): self
296
    {
297
298
        $headers = $this->config->get('security.cors.allow_headers', []);
299
300
        if (empty($headers)) {
301
            //use request headers
302
            $requestHeaders = $this->request
303
                                    ->getHeaderLine('Access-Control-Request-Headers');
304
305
            if (!empty($requestHeaders)) {
306
                $headers = $requestHeaders;
307
            }
308
        }
309
        if (!empty($headers)) {
310
            if (is_array($headers)) {
311
                $headers = implode(', ', $headers);
312
            }
313
314
            $this->response = $this->response
315
                                ->withHeader(
316
                                    'Access-Control-Allow-Headers',
317
                                    $headers
318
                                );
319
        }
320
321
        return $this;
322
    }
323
}
324