Test Failed
Push — master ( c128ed...0e28b1 )
by Mihail
11:58
created

CorsMiddleware::addOriginToResponse()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 8
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 15
ccs 6
cts 6
cp 1
crap 5
rs 9.6111
1
<?php
2
3
namespace Koded\Framework\Middleware;
4
5
use Koded\Http\Interfaces\{HttpStatus, Request};
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
    public function __construct(Configuration $config)
44
    {
45
        $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
        $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
        $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
        $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
        $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
        $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
    public function process(
54
        ServerRequestInterface $request,
55
        RequestHandlerInterface $handler
56
    ): ResponseInterface {
57
        $response = $handler->handle($request);
58
        if ($this->skipProcess($request)) {
59
            return $response;
60
        }
61
        if ($this->isPreFlightRequest($request)) {
62 17
            return $this->responseForPreFlightRequest($request);
63
        }
64 17
        return $this->responseForActualRequest($request, $response);
65 17
    }
66 17
67 17
    private function isPreFlightRequest(ServerRequestInterface $request): bool
68 17
    {
69 17
        // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests
70
        return Request::OPTIONS === $request->getMethod()
71
            && $request->hasHeader('Access-Control-Request-Method');
72 17
    }
73
74
    /**
75
     * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS#preflighted_requests_in_cors
76 17
     * @param ServerRequestInterface $request
77 14
     * @return ResponseInterface
78 1
     */
79
    private function responseForPreFlightRequest(ServerRequestInterface $request): ResponseInterface
80 13
    {
81 5
        if ($this->disabled) {
82
            // https://fetch.spec.whatwg.org/#http-responses
83 8
            return new ServerResponse('CORS is disabled', HttpStatus::FORBIDDEN);
84 6
        }
85
        $response = $this
86 2
            ->addOriginToResponse($request, new ServerResponse('', HttpStatus::NO_CONTENT))
87
            ->withHeader('Content-Type', 'text/plain')
88
            ->withHeader('Access-Control-Allow-Methods', $this->getAllowedMethods($request));
89 8
90
        if ($headers = $this->getAllowedHeaders($request)) {
91
            $response = $response->withHeader('Access-Control-Allow-Headers', $headers);
92 8
        }
93 2
        if ($expose = $this->getExposedHeaders($request)) {
94
            $response = $response->withHeader('Access-Control-Expose-Headers', $expose);
95 6
        }
96 6
        if ($this->maxAge > 0) {
97
            $response = $response->withHeader('Access-Control-Max-Age', (string)$this->maxAge);
98
        }
99
        return $response
100
            ->withoutHeader('Cache-Control')
101
            ->withoutHeader('Allow');
102
    }
103
104
    private function addOriginToResponse(
105 13
        ServerRequestInterface $request,
106
        ResponseInterface $response
107
    ): ResponseInterface {
108 13
        if ($this->disabled) {
109 13
            // https://fetch.spec.whatwg.org/#http-responses
110
            return $response->withStatus(HttpStatus::FORBIDDEN);
111
        }
112 12
        $origin = $this->origin ?: $request->getHeaderLine('Origin') ?: '*';
113
        if ($request->hasHeader('Cookie')) {// || false === str_contains($origin, '*')) {
114
            $response = $response
115
                ->withHeader('Access-Control-Allow-Credentials', 'true')
116 12
                ->withAddedHeader('Vary', 'Origin');
117
        }
118 2
        return $response->withHeader('Access-Control-Allow-Origin', $origin);
119
    }
120
121 10
    private function responseForActualRequest(
122 10
        ServerRequestInterface $request,
123 10
        ResponseInterface $response
124
    ): ResponseInterface {
125 10
        if ($expose = $this->getExposedHeaders($request)) {
126 10
            $response = $response->withHeader('Access-Control-Expose-Headers', $expose);
127
        }
128 10
        return $this->addOriginToResponse($request, $response);
129
    }
130
131 2
    private function getAllowedMethods(ServerRequestInterface $request): string
132
    {
133
        return match (true) {
134
            !empty($this->methods) && !str_contains('*', $this->methods) => $this->methods,
135 2
            !empty($allowed = $request->getAttribute('@http_methods')) => join(',', $allowed),
136 2
            default => $request->getHeaderLine('Access-Control-Request-Method') ?: 'HEAD,OPTIONS'
137 2
        };
138
    }
139 2
140
    private function getAllowedHeaders(ServerRequestInterface $request): string
141
    {
142
        return ($this->headers && false === str_contains($this->headers, '*'))
143
            ? $this->headers
144
            : $request->getHeaderLine('Access-Control-Request-Headers');
145
    }
146
147
    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

147
    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...
148 5
    {
149
        return ($this->expose && false === str_contains($this->expose, '*'))
150
            ? $this->expose
151
            : '';
152 5
    }
153
154 1
    private function skipProcess(ServerRequestInterface $request): bool
155
    {
156 4
        if (false === $request->hasHeader('Origin')) {
157 4
            return true;
158
        }
159 4
        // Same origin?
160
        return $request->getHeaderLine('Origin') === $request->getUri()->getScheme() . '://' . $request->getUri()->getHost();
161 4
    }
162
}
163