Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of fruitcake/php-cors and was originally part of asm89/stack-cors
5
 *
6
 * (c) Alexander <iam.asm89@gmail.com>
7
 * (c) Barryvdh <barryvdh@gmail.com>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
 
13
namespace Fruitcake\Cors;
14
 
15
use Fruitcake\Cors\Exceptions\InvalidOptionException;
16
use Symfony\Component\HttpFoundation\Request;
17
use Symfony\Component\HttpFoundation\Response;
18
 
19
/**
20
 * @phpstan-type CorsInputOptions array{
21
 *  'allowedOrigins'?: string[],
22
 *  'allowedOriginsPatterns'?: string[],
23
 *  'supportsCredentials'?: bool,
24
 *  'allowedHeaders'?: string[],
25
 *  'allowedMethods'?: string[],
26
 *  'exposedHeaders'?: string[]|false,
27
 *  'maxAge'?: int|bool|null,
28
 *  'allowed_origins'?: string[],
29
 *  'allowed_origins_patterns'?: string[],
30
 *  'supports_credentials'?: bool,
31
 *  'allowed_headers'?: string[],
32
 *  'allowed_methods'?: string[],
33
 *  'exposed_headers'?: string[]|false,
34
 *  'max_age'?: int|bool|null
35
 * }
36
 *
37
 */
38
class CorsService
39
{
40
    /** @var string[]  */
41
    private array $allowedOrigins = [];
42
    /** @var string[] */
43
    private array $allowedOriginsPatterns = [];
44
    /** @var string[] */
45
    private array $allowedMethods = [];
46
    /** @var string[] */
47
    private array $allowedHeaders = [];
48
    /** @var string[] */
49
    private array $exposedHeaders = [];
50
    private bool $supportsCredentials = false;
51
    private ?int $maxAge = 0;
52
 
53
    private bool $allowAllOrigins = false;
54
    private bool $allowAllMethods = false;
55
    private bool $allowAllHeaders = false;
56
 
57
    /**
58
     * @param CorsInputOptions $options
59
     */
60
    public function __construct(array $options = [])
61
    {
62
        if ($options) {
63
            $this->setOptions($options);
64
        }
65
    }
66
 
67
    /**
68
     * @param CorsInputOptions $options
69
     */
70
    public function setOptions(array $options): void
71
    {
72
        $this->allowedOrigins = $options['allowedOrigins'] ?? $options['allowed_origins'] ?? $this->allowedOrigins;
73
        $this->allowedOriginsPatterns =
74
            $options['allowedOriginsPatterns'] ?? $options['allowed_origins_patterns'] ?? $this->allowedOriginsPatterns;
75
        $this->allowedMethods = $options['allowedMethods'] ?? $options['allowed_methods'] ?? $this->allowedMethods;
76
        $this->allowedHeaders = $options['allowedHeaders'] ?? $options['allowed_headers'] ?? $this->allowedHeaders;
77
        $this->supportsCredentials =
78
            $options['supportsCredentials'] ?? $options['supports_credentials'] ?? $this->supportsCredentials;
79
 
80
        $maxAge = $this->maxAge;
81
        if (array_key_exists('maxAge', $options)) {
82
            $maxAge = $options['maxAge'];
83
        } elseif (array_key_exists('max_age', $options)) {
84
            $maxAge = $options['max_age'];
85
        }
86
        $this->maxAge = $maxAge === null ? null : (int)$maxAge;
87
 
88
        $exposedHeaders = $options['exposedHeaders'] ?? $options['exposed_headers'] ?? $this->exposedHeaders;
89
        $this->exposedHeaders = $exposedHeaders === false ? [] : $exposedHeaders;
90
 
91
        $this->normalizeOptions();
92
    }
93
 
94
    private function normalizeOptions(): void
95
    {
96
        // Normalize case
97
        $this->allowedHeaders = array_map('strtolower', $this->allowedHeaders);
98
        $this->allowedMethods = array_map('strtoupper', $this->allowedMethods);
99
 
100
        // Normalize ['*'] to true
101
        $this->allowAllOrigins = in_array('*', $this->allowedOrigins);
102
        $this->allowAllHeaders = in_array('*', $this->allowedHeaders);
103
        $this->allowAllMethods = in_array('*', $this->allowedMethods);
104
 
105
        // Transform wildcard pattern
106
        if (!$this->allowAllOrigins) {
107
            foreach ($this->allowedOrigins as $origin) {
108
                if (strpos($origin, '*') !== false) {
109
                    $this->allowedOriginsPatterns[] = $this->convertWildcardToPattern($origin);
110
                }
111
            }
112
        }
113
    }
114
 
115
    /**
116
     * Create a pattern for a wildcard, based on Str::is() from Laravel
117
     *
118
     * @see https://github.com/laravel/framework/blob/5.5/src/Illuminate/Support/Str.php
119
     * @param string $pattern
120
     * @return string
121
     */
122
    private function convertWildcardToPattern($pattern)
123
    {
124
        $pattern = preg_quote($pattern, '#');
125
 
126
        // Asterisks are translated into zero-or-more regular expression wildcards
127
        // to make it convenient to check if the strings starts with the given
128
        // pattern such as "*.example.com", making any string check convenient.
129
        $pattern = str_replace('\*', '.*', $pattern);
130
 
131
        return '#^' . $pattern . '\z#u';
132
    }
133
 
134
    public function isCorsRequest(Request $request): bool
135
    {
136
        return $request->headers->has('Origin');
137
    }
138
 
139
    public function isPreflightRequest(Request $request): bool
140
    {
141
        return $request->getMethod() === 'OPTIONS' && $request->headers->has('Access-Control-Request-Method');
142
    }
143
 
144
    public function handlePreflightRequest(Request $request): Response
145
    {
146
        $response = new Response();
147
 
148
        $response->setStatusCode(204);
149
 
150
        return $this->addPreflightRequestHeaders($response, $request);
151
    }
152
 
153
    public function addPreflightRequestHeaders(Response $response, Request $request): Response
154
    {
155
        $this->configureAllowedOrigin($response, $request);
156
 
157
        if ($response->headers->has('Access-Control-Allow-Origin')) {
158
            $this->configureAllowCredentials($response, $request);
159
 
160
            $this->configureAllowedMethods($response, $request);
161
 
162
            $this->configureAllowedHeaders($response, $request);
163
 
164
            $this->configureMaxAge($response, $request);
165
        }
166
 
167
        return $response;
168
    }
169
 
170
    public function isOriginAllowed(Request $request): bool
171
    {
172
        if ($this->allowAllOrigins === true) {
173
            return true;
174
        }
175
 
176
        $origin = (string) $request->headers->get('Origin');
177
 
178
        if (in_array($origin, $this->allowedOrigins)) {
179
            return true;
180
        }
181
 
182
        foreach ($this->allowedOriginsPatterns as $pattern) {
183
            if (preg_match($pattern, $origin)) {
184
                return true;
185
            }
186
        }
187
 
188
        return false;
189
    }
190
 
191
    public function addActualRequestHeaders(Response $response, Request $request): Response
192
    {
193
        $this->configureAllowedOrigin($response, $request);
194
 
195
        if ($response->headers->has('Access-Control-Allow-Origin')) {
196
            $this->configureAllowCredentials($response, $request);
197
 
198
            $this->configureExposedHeaders($response, $request);
199
        }
200
 
201
        return $response;
202
    }
203
 
204
    private function configureAllowedOrigin(Response $response, Request $request): void
205
    {
206
        if ($this->allowAllOrigins === true && !$this->supportsCredentials) {
207
            // Safe+cacheable, allow everything
208
            $response->headers->set('Access-Control-Allow-Origin', '*');
209
        } elseif ($this->isSingleOriginAllowed()) {
210
            // Single origins can be safely set
211
            $response->headers->set('Access-Control-Allow-Origin', array_values($this->allowedOrigins)[0]);
212
        } else {
213
            // For dynamic headers, set the requested Origin header when set and allowed
214
            if ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) {
215
                $response->headers->set('Access-Control-Allow-Origin', (string) $request->headers->get('Origin'));
216
            }
217
 
218
            $this->varyHeader($response, 'Origin');
219
        }
220
    }
221
 
222
    private function isSingleOriginAllowed(): bool
223
    {
224
        if ($this->allowAllOrigins === true || count($this->allowedOriginsPatterns) > 0) {
225
            return false;
226
        }
227
 
228
        return count($this->allowedOrigins) === 1;
229
    }
230
 
231
    private function configureAllowedMethods(Response $response, Request $request): void
232
    {
233
        if ($this->allowAllMethods === true) {
234
            $allowMethods = strtoupper((string) $request->headers->get('Access-Control-Request-Method'));
235
            $this->varyHeader($response, 'Access-Control-Request-Method');
236
        } else {
237
            $allowMethods = implode(', ', $this->allowedMethods);
238
        }
239
 
240
        $response->headers->set('Access-Control-Allow-Methods', $allowMethods);
241
    }
242
 
243
    private function configureAllowedHeaders(Response $response, Request $request): void
244
    {
245
        if ($this->allowAllHeaders === true) {
246
            $allowHeaders = (string) $request->headers->get('Access-Control-Request-Headers');
247
            $this->varyHeader($response, 'Access-Control-Request-Headers');
248
        } else {
249
            $allowHeaders = implode(', ', $this->allowedHeaders);
250
        }
251
        $response->headers->set('Access-Control-Allow-Headers', $allowHeaders);
252
    }
253
 
254
    private function configureAllowCredentials(Response $response, Request $request): void
255
    {
256
        if ($this->supportsCredentials) {
257
            $response->headers->set('Access-Control-Allow-Credentials', 'true');
258
        }
259
    }
260
 
261
    private function configureExposedHeaders(Response $response, Request $request): void
262
    {
263
        if ($this->exposedHeaders) {
264
            $response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->exposedHeaders));
265
        }
266
    }
267
 
268
    private function configureMaxAge(Response $response, Request $request): void
269
    {
270
        if ($this->maxAge !== null) {
271
            $response->headers->set('Access-Control-Max-Age', (string) $this->maxAge);
272
        }
273
    }
274
 
275
    public function varyHeader(Response $response, string $header): Response
276
    {
277
        if (!$response->headers->has('Vary')) {
278
            $response->headers->set('Vary', $header);
279
        } elseif (!in_array($header, explode(', ', (string) $response->headers->get('Vary')))) {
280
            $response->headers->set('Vary', ((string) $response->headers->get('Vary')) . ', ' . $header);
281
        }
282
 
283
        return $response;
284
    }
285
}