Issues (4)

src/CorsService.php (2 issues)

Labels
Severity
1
<?php namespace Nord\Lumen\Cors;
2
3
use Illuminate\Support\Str;
4
use Nord\Lumen\Cors\Contracts\CorsService as CorsServiceContract;
5
use Symfony\Component\HttpFoundation\Request;
6
use Symfony\Component\HttpFoundation\Response;
7
8
class CorsService implements CorsServiceContract
9
{
10
11
    /**
12
     * Allowed request origins.
13
     *
14
     * @var array
15
     */
16
    private $allowOrigins = [];
17
18
    /**
19
     * Allowed HTTP methods.
20
     *
21
     * @var array
22
     */
23
    private $allowMethods = [];
24
25
    /**
26
     * Allowed HTTP headers.
27
     *
28
     * @var array
29
     */
30
    private $allowHeaders = [];
31
32
    /**
33
     * Whether or not the response can be exposed when credentials are present.
34
     *
35
     * @var bool
36
     */
37
    private $allowCredentials = false;
38
39
    /**
40
     * HTTP Headers that are allowed to be exposed to the web browser.
41
     *
42
     * @var array
43
     */
44
    private $exposeHeaders = [];
45
46
    /**
47
     * Indicates how long preflight request can be cached.
48
     *
49
     * @var int
50
     */
51
    private $maxAge = 0;
52
53
54
    /**
55
     * CorsService constructor.
56
     *
57
     * @param array $config
58
     */
59
    public function __construct(array $config = [])
60
    {
61
        if (isset($config['allow_origins'])) {
62
            $this->allowOrigins = $config['allow_origins'];
63
        }
64
65
        if (isset($config['allow_headers'])) {
66
            $this->setAllowHeaders($config['allow_headers']);
67
        }
68
69
        if (isset($config['allow_methods'])) {
70
            $this->setAllowMethods($config['allow_methods']);
71
        }
72
73
        if (isset($config['allow_credentials'])) {
74
            $this->allowCredentials = $config['allow_credentials'];
75
        }
76
77
        if (isset($config['expose_headers'])) {
78
            $this->setExposeHeaders($config['expose_headers']);
79
        }
80
81
        if (isset($config['max_age'])) {
82
            $this->setMaxAge($config['max_age']);
83
        }
84
    }
85
86
87
    /**
88
     * @inheritdoc
89
     */
90
    public function handlePreflightRequest(Request $request): Response
91
    {
92
        $response = new Response();
93
94
        // Do not set any headers if the origin is not allowed
95
        if ($this->isOriginAllowed($request->headers->get('Origin'))) {
96
            $response = $this->setAccessControlAllowOriginHeader($request, $response);
97
98
            if ($this->allowCredentials) {
99
                $response->headers->set('Access-Control-Allow-Credentials', 'true');
100
            }
101
102
            if ($this->maxAge) {
103
                $response->headers->set('Access-Control-Max-Age', (string)$this->maxAge);
104
            }
105
106
            $allowMethods = $this->isAllMethodsAllowed()
107
                ? strtoupper($request->headers->get('Access-Control-Request-Method'))
108
                : implode(', ', $this->allowMethods);
109
110
            $response->headers->set('Access-Control-Allow-Methods', $allowMethods);
111
112
            $allowHeaders = $this->isAllHeadersAllowed()
113
                ? strtolower($request->headers->get('Access-Control-Request-Headers'))
114
                : implode(', ', $this->allowHeaders);
115
116
            $response->headers->set('Access-Control-Allow-Headers', $allowHeaders);
117
        }
118
119
        return $response;
120
    }
121
122
123
    /**
124
     * @inheritdoc
125
     */
126
    public function handleRequest(Request $request, Response $response): Response
127
    {
128
        // Do not set any headers if the origin is not allowed
129
        if ($this->isOriginAllowed($request->headers->get('Origin'))) {
130
            $response = $this->setAccessControlAllowOriginHeader($request, $response);
131
132
            // Set Vary unless all origins are allowed
133
            if (!$this->isAllOriginsAllowed()) {
134
                $vary = $request->headers->has('Vary') ? $request->headers->get('Vary') . ', Origin' : 'Origin';
135
                $response->headers->set('Vary', $vary);
136
            }
137
138
            if ($this->allowCredentials) {
139
                $response->headers->set('Access-Control-Allow-Credentials', 'true');
140
            }
141
142
            if (!empty($this->exposeHeaders)) {
143
                $response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->exposeHeaders));
144
            }
145
        }
146
147
        return $response;
148
    }
149
150
151
    /**
152
     * @inheritdoc
153
     */
154
    public function isCorsRequest(Request $request)
155
    {
156
        return $request->headers->has('Origin');
157
    }
158
159
160
    /**
161
     * @inheritdoc
162
     */
163
    public function isPreflightRequest(Request $request)
164
    {
165
        return $this->isCorsRequest($request) && $request->isMethod('OPTIONS') && $request->headers->has('Access-Control-Request-Method');
166
    }
167
168
    /**
169
     * @param Request  $request
170
     * @param Response $response
171
     *
172
     * @return Response
173
     */
174
    protected function setAccessControlAllowOriginHeader(Request $request, Response $response): Response
175
    {
176
        $origin = $request->headers->get('Origin');
177
178
        if ($this->isAllOriginsAllowed()) {
179
            $response->headers->set('Access-Control-Allow-Origin', '*');
180
        } elseif ($this->isOriginAllowed($origin)) {
181
            $response->headers->set('Access-Control-Allow-Origin', $origin);
182
        }
183
184
        return $response;
185
    }
186
187
188
    /**
189
     * Returns whether or not the origin is allowed.
190
     *
191
     * @param string|null $origin
192
     *
193
     * @return bool
194
     */
195
    protected function isOriginAllowed(?string $origin)
196
    {
197
        if ($this->isAllOriginsAllowed()) {
198
            return true;
199
        }
200
201
        return Str::is($this->allowOrigins, $origin);
202
    }
203
204
205
    /**
206
     * Returns whether or not the method is allowed.
207
     *
208
     * @param string|null $method
209
     *
210
     * @return bool
211
     */
212
    protected function isMethodAllowed(?string $method)
213
    {
214
        if ($this->isAllMethodsAllowed()) {
215
            return true;
216
        }
217
218
        return in_array(strtoupper($method), $this->allowMethods);
0 ignored issues
show
It seems like $method can also be of type null; however, parameter $string of strtoupper() does only seem to accept string, 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

218
        return in_array(strtoupper(/** @scrutinizer ignore-type */ $method), $this->allowMethods);
Loading history...
219
    }
220
221
222
    /**
223
     * Returns whether or not the header is allowed.
224
     *
225
     * @param string|null $header
226
     *
227
     * @return bool
228
     */
229
    protected function isHeaderAllowed(?string $header)
230
    {
231
        if ($this->isAllHeadersAllowed()) {
232
            return true;
233
        }
234
235
        return in_array(strtolower($header), $this->allowHeaders);
0 ignored issues
show
It seems like $header can also be of type null; however, parameter $string of strtolower() does only seem to accept string, 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

235
        return in_array(strtolower(/** @scrutinizer ignore-type */ $header), $this->allowHeaders);
Loading history...
236
    }
237
238
239
    /**
240
     * @return bool
241
     */
242
    protected function isAllOriginsAllowed()
243
    {
244
        return in_array('*', $this->allowOrigins);
245
    }
246
247
248
    /**
249
     * @return bool
250
     */
251
    protected function isAllMethodsAllowed()
252
    {
253
        return in_array('*', $this->allowMethods);
254
    }
255
256
257
    /**
258
     * @return bool
259
     */
260
    protected function isAllHeadersAllowed()
261
    {
262
        return in_array('*', $this->allowHeaders);
263
    }
264
265
    /**
266
     * @param array $allowMethods
267
     *
268
     * @return self
269
     */
270
    protected function setAllowMethods(array $allowMethods): self
271
    {
272
        $this->allowMethods = array_map('strtoupper', $allowMethods);
273
        
274
        return $this;
275
    }
276
277
278
    /**
279
     * @param array $allowHeaders
280
     *
281
     * @return self
282
     */
283
    protected function setAllowHeaders(array $allowHeaders): self
284
    {
285
        $this->allowHeaders = array_map('strtolower', $allowHeaders);
286
        
287
        return $this;
288
    }
289
290
291
    /**
292
     * @param array $exposeHeaders
293
     *
294
     * @return self
295
     */
296
    protected function setExposeHeaders(array $exposeHeaders): self
297
    {
298
        $this->exposeHeaders = array_map('strtolower', $exposeHeaders);
299
        
300
        return $this;
301
    }
302
303
304
    /**
305
     * @param int $maxAge
306
     *
307
     * @return self
308
     */
309
    protected function setMaxAge(int $maxAge): self
310
    {
311
        if ($maxAge < 0) {
312
            throw new \InvalidArgumentException('Max age must be a positive number or zero.');
313
        }
314
315
        $this->maxAge = $maxAge;
316
        
317
        return $this;
318
    }
319
}
320