Passed
Push — develop ( ec03b4...36c2b4 )
by nguereza
02:46
created

CorsMiddleware::setAllowCredentials()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 0
dl 0
loc 13
rs 10
c 0
b 0
f 0
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 no route has been match no need check for CORS
113
        /** @var ?Route $route */
114
        $route = $request->getAttribute(Route::class);
115
        if (!$route) {
116
            return $handler->handle($request);
117
        }
118
119
        //Check if the path match
120
        $path = $this->config->get('security.cors.path', '/');
121
        if (!preg_match('~^' . $path . '~', $route->getPattern())) {
122
            return $handler->handle($request);
123
        }
124
125
        $this->request = $request;
126
127
        if ($this->isPreflight()) {
128
            $response = new Response(204);
129
130
            $this->logger->info(
131
                'CORS Preflight Request for {method}:{url}',
132
                [
133
                    'method' => $request->getMethod(),
134
                    'url' => (string) $request->getUri(),
135
                ]
136
            );
137
        } else {
138
            $response = $handler->handle($request);
139
        }
140
141
        $this->response = $response;
142
143
        $this->setCorsHeaders();
144
145
        return $this->response;
146
    }
147
148
    /**
149
     * Check if the current request is preflight
150
     * @return bool
151
     */
152
    protected function isPreflight(): bool
153
    {
154
        return $this->request->getMethod() === 'OPTIONS';
155
    }
156
157
    /**
158
     * Set the CORS headers
159
     * @return void
160
     */
161
    protected function setCorsHeaders(): void
162
    {
163
        if ($this->isPreflight()) {
164
            $this->setOrigin()
165
                 ->setMaxAge()
166
                 ->setAllowCredentials()
167
                 ->setAllowMethods()
168
                 ->setAllowHeaders();
169
        } else {
170
            $this->setOrigin()
171
                 ->setExposedHeaders()
172
                 ->setAllowCredentials();
173
        }
174
    }
175
176
    /**
177
     * Set Origin header
178
     * @return $this
179
     */
180
    protected function setOrigin(): self
181
    {
182
        $origins = $this->config->get('security.cors.origins', ['*']);
183
        // default to the first allowed origin
184
        $origin = reset($origins);
185
186
        foreach ($origins as $ori) {
187
            if ($ori === $this->request->getHeaderLine('Origin')) {
188
                $origin = $ori;
189
                break;
190
            }
191
        }
192
193
        $this->response = $this->response
194
                                ->withHeader(
195
                                    'Access-Control-Allow-Origin',
196
                                    $origin
197
                                );
198
199
        return $this;
200
    }
201
202
    /**
203
     * Set expose headers
204
     * @return $this
205
     */
206
    protected function setExposedHeaders(): self
207
    {
208
209
        $headers = $this->config->get('security.cors.expose_headers', []);
210
211
        if (!empty($headers)) {
212
            $this->response = $this->response
213
                                ->withHeader(
214
                                    'Access-Control-Expose-Headers',
215
                                    implode(', ', $headers)
216
                                );
217
        }
218
219
        return $this;
220
    }
221
222
    /**
223
     * Set max age header
224
     * @return $this
225
     */
226
    protected function setMaxAge(): self
227
    {
228
        $maxAge = $this->config->get('security.cors.max_age', 1800);
229
230
        $this->response = $this->response
231
                            ->withHeader(
232
                                'Access-Control-Max-Age',
233
                                Str::stringify($maxAge)
234
                            );
235
236
        return $this;
237
    }
238
239
    /**
240
     * Set Allow credentials header
241
     * @return $this
242
     */
243
    protected function setAllowCredentials(): self
244
    {
245
        $allowCredentials = $this->config->get('security.cors.allow_credentials', false);
246
247
        if ($allowCredentials) {
248
            $this->response = $this->response
249
                                ->withHeader(
250
                                    'Access-Control-Allow-Credential',
251
                                    Str::stringify($allowCredentials)
252
                                );
253
        }
254
255
        return $this;
256
    }
257
258
    /**
259
     * Set allow methods header
260
     * @return $this
261
     */
262
    protected function setAllowMethods(): self
263
    {
264
        $methods = $this->config->get('security.cors.allow_methods', []);
265
266
        if (!empty($methods)) {
267
            $this->response = $this->response
268
                                ->withHeader(
269
                                    'Access-Control-Allow-Methods',
270
                                    implode(', ', $methods)
271
                                );
272
        }
273
274
        return $this;
275
    }
276
277
    /**
278
     * Set Allow headers
279
     * @return $this
280
     */
281
    protected function setAllowHeaders(): self
282
    {
283
284
        $headers = $this->config->get('security.cors.allow_headers', []);
285
286
        if (empty($headers)) {
287
            //use request headers
288
            $requestHeaders = $this->request
289
                                    ->getHeaderLine('Access-Control-Request-Headers');
290
291
            if (!empty($requestHeaders)) {
292
                $headers = $requestHeaders;
293
            }
294
        }
295
        if (!empty($headers)) {
296
            if (is_array($headers)) {
297
                $headers = implode(', ', $headers);
298
            }
299
300
            $this->response = $this->response
301
                                ->withHeader(
302
                                    'Access-Control-Allow-Headers',
303
                                    $headers
304
                                );
305
        }
306
307
        return $this;
308
    }
309
}
310