Passed
Push — main ( d0ed98...da6fcc )
by Dimitri
13:43
created

CorsBuilder::isCorsRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 2
rs 10
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Http;
13
14
use Psr\Http\Message\ResponseInterface;
15
use Psr\Http\Message\ServerRequestInterface;
16
17
/**
18
 * @credit CodeIgniter4 Cors <a href="https://github.com/agungsugiarto/codeigniter4-cors">Fluent\Cors\ServiceCors</a>
19
 */
20
class CorsBuilder
21
{
22
    protected array $options = [];
23
24
    public function __construct(array $options = [])
25
    {
26 4
        $this->options = $this->normalizeOptions($options);
27
    }
28
29
    protected function normalizeOptions(array $options = []): array
30
    {
31
        $options = array_merge([
32
            'allowedOrigins'         => [],
33
            'allowedOriginsPatterns' => [],
34
            'supportsCredentials'    => false,
35
            'allowedHeaders'         => [],
36
            'exposedHeaders'         => [],
37
            'allowedMethods'         => [],
38
            'maxAge'                 => 0,
39 4
        ], $options);
40
41
        // Normaliser la casse
42 4
        $options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']);
43
44
        // normalizer ['*'] en true
45
        if (in_array('*', $options['allowedOrigins'], true)) {
46 4
            $options['allowedOrigins'] = true;
47
        }
48
        if (in_array('*', $options['allowedHeaders'], true)) {
49 4
            $options['allowedHeaders'] = true;
50
        }
51
        if (in_array('*', $options['allowedMethods'], true)) {
52 2
            $options['allowedMethods'] = true;
53
        }
54
55 4
        return $options;
56
    }
57
58
    public function isCorsRequest(ServerRequestInterface $request): bool
59
    {
60 4
        return $request->hasHeader('Origin') && ! $this->isSameHost($request);
61
    }
62
63
    public function isPreflightRequest(ServerRequestInterface $request): bool
64
    {
65 4
        return strtoupper($request->getMethod()) === 'OPTIONS' && $request->hasHeader('Access-Control-Request-Method');
66
    }
67
68
    public function handlePreflightRequest(ServerRequestInterface $request): ResponseInterface
69
    {
70 4
        $response = new Response();
71
72 4
        $response = $response->withStatus(204);
73
74 4
        return $this->addPreflightRequestHeaders($request, $response);
75
    }
76
77
    public function addPreflightRequestHeaders(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
78
    {
79 4
        $response = $this->configureAllowedOrigin($request, $response);
80
81
        if ($response->hasHeader('Access-Control-Allow-Origin')) {
82 4
            $response = $this->configureAllowCredentials($request, $response);
83 4
            $response = $this->configureAllowedMethods($request, $response);
84 4
            $response = $this->configureAllowedHeaders($request, $response);
85 4
            $response = $this->configureMaxAge($request, $response);
86
        }
87
88 4
        return $response;
89
    }
90
91
    public function isOriginAllowed(ServerRequestInterface $request): bool
92
    {
93
        if ($this->options['allowedOrigins'] === true) {
94 2
            return true;
95
        }
96
97
        if (! $request->hasHeader('Origin')) {
98
            return false;
99
        }
100
101
        $origin = $request->getHeaderLine('Origin');
102
103
        if (in_array($origin, $this->options['allowedOrigins'], true)) {
104
            return true;
105
        }
106
107
        foreach ($this->options['allowedOriginsPatterns'] as $pattern) {
108
            if (preg_match($pattern, $origin)) {
109
                return true;
110
            }
111
        }
112
113
        return false;
114
    }
115
116
    public function addActualRequestHeaders(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
117
    {
118 4
        $response = $this->configureAllowedOrigin($request, $response);
119
120
        if ($response->hasHeader('Access-Control-Allow-Origin')) {
121 4
            $response = $this->configureAllowCredentials($request, $response);
122 4
            $response = $this->configureExposedHeaders($request, $response);
123
        }
124
125 4
        return $response;
126
    }
127
128
    public function varyHeader(ResponseInterface $response, $header): ResponseInterface
129
    {
130
        if (! $response->hasHeader('Vary')) {
131 4
            $response = $response->withHeader('Vary', $header);
132
        } elseif (! in_array($header, explode(', ', $response->getHeaderLine('Vary')), true)) {
133 4
            $response = $response->withHeader('Vary', $response->getHeaderLine('Vary') . ', ' . $header);
134
        }
135
136 4
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response could return the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
137
    }
138
139
    protected function configureAllowedOrigin(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
140
    {
141
        if ($this->options['allowedOrigins'] === true && ! $this->options['supportsCredentials']) {
142
            // Sûr+cacheable, tout autoriser
143 2
            $response = $response->withHeader('Access-Control-Allow-Origin', '*');
144
        } elseif ($this->isSingleOriginAllowed()) {
145
            // Les origines uniques peuvent être définies en toute sécurité
146 4
            $response = $response->withHeader('Access-Control-Allow-Origin', array_values($this->options['allowedOrigins'])[0]);
147
        } else {
148
            // Pour les en-têtes dynamiques, définir l'en-tête Origin demandé lorsqu'il est défini et autorisé.
149
            if ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) {
150 2
                $response = $response->withHeader('Access-Control-Allow-Origin', (string) $request->getHeaderLine('Origin'));
151
            }
152
153 2
            $response = $this->varyHeader($response, 'Origin');
154
        }
155
156 4
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response could return the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
157
    }
158
159
    protected function isSingleOriginAllowed(): bool
160
    {
161
        if ($this->options['allowedOrigins'] === true || ! empty($this->options['allowedOriginsPatterns'])) {
162 2
            return false;
163
        }
164
165 4
        return count($this->options['allowedOrigins']) === 1;
166
    }
167
168
    protected function configureAllowedMethods(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
169
    {
170
        if ($this->options['allowedMethods'] === true) {
171 2
            $allowMethods = strtoupper($request->getHeaderLine('Access-Control-Request-Method'));
172 2
            $response     = $this->varyHeader($response, 'Access-Control-Request-Method');
173
        } else {
174 2
            $allowMethods = implode(', ', $this->options['allowedMethods']);
175
        }
176
177 4
        return $response->withHeader('Access-Control-Allow-Methods', $allowMethods);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response->withHe...ethods', $allowMethods) returns the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface.
Loading history...
178
    }
179
180
    protected function configureAllowedHeaders(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
181
    {
182
        if ($this->options['allowedHeaders'] === true) {
183 4
            $allowHeaders = $request->getHeaderLine('Access-Control-Request-Headers');
184 4
            $response     = $this->varyHeader($response, 'Access-Control-Request-Headers');
185
        } else {
186 2
            $allowHeaders = implode(', ', $this->options['allowedHeaders']);
187
        }
188
189 4
        return $response->withHeader('Access-Control-Allow-Headers', $allowHeaders);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response->withHe...eaders', $allowHeaders) returns the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface.
Loading history...
190
    }
191
192
    protected function configureAllowCredentials(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
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

192
    protected function configureAllowCredentials(/** @scrutinizer ignore-unused */ ServerRequestInterface $request, ResponseInterface $response): ResponseInterface

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...
193
    {
194
        if ($this->options['supportsCredentials']) {
195 2
            $response = $response->withHeader('Access-Control-Allow-Credentials', 'true');
196
        }
197
198 4
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response could return the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
199
    }
200
201
    protected function configureExposedHeaders(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
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

201
    protected function configureExposedHeaders(/** @scrutinizer ignore-unused */ ServerRequestInterface $request, ResponseInterface $response): ResponseInterface

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...
202
    {
203
        if ($this->options['exposedHeaders']) {
204 2
            $response = $response->withHeader('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders']));
205
        }
206
207 4
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response could return the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
208
    }
209
210
    protected function configureMaxAge(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
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

210
    protected function configureMaxAge(/** @scrutinizer ignore-unused */ ServerRequestInterface $request, ResponseInterface $response): ResponseInterface

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...
211
    {
212
        if ($this->options['maxAge'] !== null) {
213 4
            $response = $response->withHeader('Access-Control-Max-Age', (string) $this->options['maxAge']);
214
        }
215
216 4
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response could return the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
217
    }
218
219
    protected function isSameHost(ServerRequestInterface $request): bool
220
    {
221 4
        return $request->getHeaderLine('Origin') === config('app.base_url');
222
    }
223
}
224