CorsMiddleware::getAllowedHeaders()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 4
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Koded\Framework\Middleware;
4
5
use Koded\Http\Interfaces\HttpStatus;
6
use Koded\Http\ServerResponse;
7
use Koded\Stdlib\Configuration;
8
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface};
9
use Psr\Http\Server\{MiddlewareInterface, RequestHandlerInterface};
10
use function join;
11
use function str_contains;
12
use function strtoupper;
13
use function trim;
14
15
/**
16
 * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
17
 * @link https://fetch.spec.whatwg.org/#cors-protocol-and-credentials
18
 */
19
class CorsMiddleware implements MiddlewareInterface
20
{
21
    private readonly bool $disabled;
22
    private readonly string $origin;
23
    private readonly string $methods;
24
    private readonly string $headers;
25
    private readonly string $expose;
26
    private readonly int $maxAge;
27
28
    /**
29
     * The configuration is used to force/override the CORS header values.
30
     * By default, most of them are not predefined or have a commonly used values.
31
     *
32
     * Configuration directives:
33
     *
34
     *      cors.disable
35
     *      cors.origin
36
     *      cors.headers
37
     *      cors.methods
38
     *      cors.maxAge
39
     *      cors.expose
40
     *
41
     * @param Configuration $config
42
     */
43 34
    public function __construct(Configuration $config)
44
    {
45 34
        $this->disabled = (bool)$config->get('cors.disable');
0 ignored issues
show
Bug introduced by
The property disabled is declared read-only in Koded\Framework\Middleware\CorsMiddleware.
Loading history...
46 34
        $this->origin = trim($config->get('cors.origin'));
0 ignored issues
show
Bug introduced by
The property origin is declared read-only in Koded\Framework\Middleware\CorsMiddleware.
Loading history...
47 34
        $this->methods = strtoupper(trim($config->get('cors.methods'))) ?: '*';
0 ignored issues
show
Bug introduced by
The property methods is declared read-only in Koded\Framework\Middleware\CorsMiddleware.
Loading history...
48 34
        $this->headers = trim($config->get('cors.headers')) ?: '*';
0 ignored issues
show
Bug introduced by
The property headers is declared read-only in Koded\Framework\Middleware\CorsMiddleware.
Loading history...
49 34
        $this->expose = trim($config->get('cors.expose')) ?: '*';
0 ignored issues
show
Bug introduced by
The property expose is declared read-only in Koded\Framework\Middleware\CorsMiddleware.
Loading history...
50 34
        $this->maxAge = (int)$config->get('cors.maxAge');
0 ignored issues
show
Bug introduced by
The property maxAge is declared read-only in Koded\Framework\Middleware\CorsMiddleware.
Loading history...
51
    }
52
53 34
    public function process(
54
        ServerRequestInterface $request,
55
        RequestHandlerInterface $handler
56
    ): ResponseInterface {
57 34
        $response = $handler->handle($request);
58 33
        if ($this->skipProcess($request)) {
59 15
            return $response;
60
        }
61 18
        if ($this->isPreFlightRequest($request)) {
62 7
            return $this->responseForPreFlightRequest($request);
63
        }
64 11
        return $this->responseForActualRequest($request, $response);
65
    }
66
67 18
    private function isPreFlightRequest(ServerRequestInterface $request): bool
68
    {
69
        // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests
70 18
        return 'OPTIONS' === $request->getMethod()
71 18
            && $request->hasHeader('Access-Control-Request-Method');
72
    }
73
74
    /**
75
     * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS#preflighted_requests_in_cors
76
     * @param ServerRequestInterface $request
77
     * @return ResponseInterface
78
     */
79 7
    private function responseForPreFlightRequest(ServerRequestInterface $request): ResponseInterface
80
    {
81 7
        if ($this->disabled) {
82
            // https://fetch.spec.whatwg.org/#http-responses
83 1
            return new ServerResponse('CORS is disabled', HttpStatus::FORBIDDEN);
84
        }
85 6
        $response = $this
86 6
            ->addOriginToResponse($request, new ServerResponse('', HttpStatus::NO_CONTENT))
87 6
            ->withHeader('Access-Control-Allow-Methods', $this->getAllowedMethods($request))
88 6
            ->withHeader('Access-Control-Allow-Credentials', 'true')
89 6
            ->withHeader('Content-Type', 'text/plain')
90 6
            ->withAddedHeader('Vary', 'Origin')
91 6
            ->withoutHeader('Cache-Control')
92 6
            ->withoutHeader('Allow');
93
94 6
        if ($headers = $this->getAllowedHeaders($request)) {
95 2
            $response = $response->withHeader('Access-Control-Allow-Headers', $headers);
96
        }
97 6
        if ($expose = $this->getExposedHeaders($request)) {
98 1
            $response = $response->withHeader('Access-Control-Expose-Headers', $expose);
99
        }
100 6
        if ($this->maxAge > 0) {
101 1
            $response = $response->withHeader('Access-Control-Max-Age', (string)$this->maxAge);
102
        }
103 6
        return $response;
104
    }
105
106 17
    private function addOriginToResponse(
107
        ServerRequestInterface $request,
108
        ResponseInterface $response
109
    ): ResponseInterface {
110 17
        if ($this->disabled) {
111
            // https://fetch.spec.whatwg.org/#http-responses
112 2
            return $response->withStatus(HttpStatus::FORBIDDEN);
113
        }
114 15
        $origin = $this->origin ?: $request->getHeaderLine('Origin') ?: '*';
115 15
        if ($request->hasHeader('Cookie')) {// || false === str_contains($origin, '*')) {
116 8
            $response = $response
117 8
                ->withHeader('Access-Control-Allow-Credentials', 'true')
118 8
                ->withAddedHeader('Vary', 'Origin');
119
        }
120 15
        return $response->withHeader('Access-Control-Allow-Origin', $origin);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response->withHe...Allow-Origin', $origin) returns the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface.
Loading history...
121
    }
122
123 11
    private function responseForActualRequest(
124
        ServerRequestInterface $request,
125
        ResponseInterface $response
126
    ): ResponseInterface {
127 11
        if ($expose = $this->getExposedHeaders($request)) {
128 2
            $response = $response->withHeader('Access-Control-Expose-Headers', $expose);
129
        }
130 11
        return $this->addOriginToResponse($request, $response);
131
    }
132
133 6
    private function getAllowedMethods(ServerRequestInterface $request): string
134
    {
135 6
        return match (true) {
136 6
            !empty($this->methods) && !str_contains('*', $this->methods) => $this->methods,
137 6
            !empty($allowed = $request->getAttribute('@http_methods')) => join(',', $allowed),
138 6
            default => $request->getHeaderLine('Access-Control-Request-Method') ?: 'HEAD,OPTIONS'
139 6
        };
140
    }
141
142 6
    private function getAllowedHeaders(ServerRequestInterface $request): string
143
    {
144 6
        return ($this->headers && false === str_contains($this->headers, '*'))
145 1
            ? $this->headers
146 6
            : $request->getHeaderLine('Access-Control-Request-Headers');
147
    }
148
149 17
    private function getExposedHeaders(ServerRequestInterface $request): string
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

149
    private function getExposedHeaders(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): string

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...
150
    {
151 17
        return ($this->expose && false === str_contains($this->expose, '*'))
152 3
            ? $this->expose
153 17
            : '';
154
    }
155
156 33
    private function skipProcess(ServerRequestInterface $request): bool
157
    {
158 33
        if (false === $request->hasHeader('Origin')) {
159 14
            return true;
160
        }
161
//        if ('OPTIONS' !== $request->getMethod()) {
162
//            return true;
163
//        }
164
165
        // Same origin?
166 19
        return $request->getHeaderLine('Origin') === $request->getUri()->getScheme() . '://' . $request->getUri()->getHost();
167
    }
168
}
169