Passed
Pull Request — master (#17)
by Mihail
15:10
created

ServerRequest.php (1 issue)

Labels
Severity
1
<?php
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 *
11
 */
12
13
namespace Koded\Http;
14
15
use InvalidArgumentException;
16
use Koded\Http\Interfaces\HttpMethod;
17
use Koded\Http\Interfaces\Request;
18
use Koded\Stdlib\Serializer\XmlSerializer;
19
use Psr\Http\Message\ServerRequestInterface;
20
use function array_merge;
21
use function file_get_contents;
22
use function gettype;
23
use function is_array;
24
use function is_iterable;
25
use function is_object;
26
use function iterator_to_array;
27
use function json_decode;
28
use function parse_str;
29
use function sprintf;
30
use function str_contains;
31
use function str_ireplace;
32
use function str_replace;
33
use function str_starts_with;
34
use function strpos;
35
use function strtolower;
36
37
class ServerRequest extends ClientRequest implements Request
38
{
39
    use CookieTrait, FilesTrait, ValidatableTrait;
40
41 52
    protected string $serverSoftware = '';
42
    protected array  $attributes     = [];
43 52
    protected array  $queryParams    = [];
44 52
45 52
    protected object|array|null $parsedBody = null;
46 52
47 52
    /**
48
     * ServerRequest constructor.
49 3
     *
50
     * @param array $attributes
51 3
     */
52
    public function __construct(array $attributes = [])
53
    {
54 3
        parent::__construct(
55
            HttpMethod::tryFrom($_SERVER['REQUEST_METHOD'] ?? 'GET'),
0 ignored issues
show
It seems like Koded\Http\Interfaces\Ht...UEST_METHOD'] ?? 'GET') can also be of type null; however, parameter $method of Koded\Http\ClientRequest::__construct() does only seem to accept Koded\Http\Interfaces\HttpMethod, 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

55
            /** @scrutinizer ignore-type */ HttpMethod::tryFrom($_SERVER['REQUEST_METHOD'] ?? 'GET'),
Loading history...
56 3
            $this->buildUri()
57
        );
58
        $this->attributes = $attributes;
59 2
        $this->extractHttpHeaders($_SERVER);
60
        $this->extractServerData($_SERVER);
61 2
    }
62 2
63
    public function getServerParams(): array
64 2
    {
65
        return $_SERVER;
66
    }
67 17
68
    public function getQueryParams(): array
69 17
    {
70 2
        return $this->queryParams;
71
    }
72
73 15
    public function withQueryParams(array $query): static
74 5
    {
75
        $instance              = clone $this;
76
        $instance->queryParams = array_merge($instance->queryParams, $query);
77 10
        return $instance;
78
    }
79
80 13
    public function getParsedBody(): object|array|null
81
    {
82 13
        if ($this->useOnlyPost()) {
83
            return $_POST;
84 13
        }
85 1
        if (false === empty($_POST)) {
86
            return $_POST;
87 1
        }
88
        return $this->parsedBody;
89
    }
90
91 12
    public function withParsedBody($data): static
92 2
    {
93
        $instance = clone $this;
94 2
        if ($this->useOnlyPost()) {
95
            $instance->parsedBody = $_POST;
96
            return $instance;
97
        }
98 10
        // If nothing is available for the body
99 4
        if (null === $data) {
100
            $instance->parsedBody = null;
101 4
            return $instance;
102
        }
103
        // Supports array or iterable object
104 6
        if (is_iterable($data)) {
105 1
            $instance->parsedBody = is_array($data) ? $data : iterator_to_array($data);
106
            return $instance;
107 1
        }
108
        if (is_object($data)) {
109
            $instance->parsedBody = $data;
110 5
            return $instance;
111 5
        }
112
        throw new InvalidArgumentException(sprintf(
113
            'Unsupported data provided (%s), Expects NULL, array or iterable',
114
            gettype($data))
115 3
        );
116
    }
117 3
118
    public function getAttributes(): array
119
    {
120 3
        return $this->attributes;
121
    }
122 3
123
    public function getAttribute($name, $default = null): mixed
124
    {
125 4
        return $this->attributes[$name] ?? $default;
126
    }
127 4
128 4
    public function withAttribute($name, $value): static
129
    {
130 4
        $instance                    = clone $this;
131
        $instance->attributes[$name] = $value;
132
        return $instance;
133 2
    }
134
135 2
    public function withoutAttribute($name): static
136 2
    {
137
        $instance = clone $this;
138 2
        unset($instance->attributes[$name]);
139
        return $instance;
140
    }
141 1
142
    public function withAttributes(array $attributes): static
143 1
    {
144
        $instance = clone $this;
145 1
        foreach ($attributes as $name => $value) {
146 1
            $instance->attributes[$name] = $value;
147
        }
148
        return $instance;
149 1
    }
150
151
    public function isXHR(): bool
152 2
    {
153
        return 'XMLHTTPREQUEST' === strtoupper($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '');
154 2
    }
155
156
    protected function buildUri(): Uri
157 52
    {
158
        if (strpos($_SERVER['REQUEST_URI'] ?? '', '://')) {
159 52
            return new Uri($_SERVER['REQUEST_URI']);
160 1
        }
161
        if ($host = $_SERVER['SERVER_NAME'] ?? $_SERVER['SERVER_ADDR'] ?? '') {
162
            return new Uri('http' . ($_SERVER['HTTPS'] ?? $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? false ? 's' : '')
163 52
                . '://' . $host
164 50
                . ':' . ($_SERVER['SERVER_PORT'] ?? 80)
165 50
                . ($_SERVER['REQUEST_URI'] ?? '')
166 50
            );
167 50
        }
168
        return new Uri($_SERVER['REQUEST_URI'] ?? '');
169
    }
170
171 5
    protected function extractHttpHeaders(array $server): void
172
    {
173
        foreach ($server as $k => $v) {
174 52
            // Calisthenics :)
175
            str_starts_with($k, 'HTTP_') && $this->normalizeHeader(str_replace('HTTP_', '', $k), $v, false);
176 52
        }
177
        if (isset($server['HTTP_IF_NONE_MATCH'])) {
178 52
            // ETag workaround for various broken Apache2 versions
179
            $this->headers['ETag']    = str_replace('-gzip', '', $server['HTTP_IF_NONE_MATCH']);
180
            $this->headersMap['etag'] = 'ETag';
181 52
        }
182 52
        if (isset($server['CONTENT_TYPE'])) {
183
            $this->headers['Content-Type']    = strtolower($server['CONTENT_TYPE']);
184 52
            $this->headersMap['content-type'] = 'Content-Type';
185
        }
186 22
        $this->setHost();
187 22
    }
188
189
    protected function extractServerData(array $server): void
190 52
    {
191 1
        $this->protocolVersion = str_ireplace('HTTP/', '', $server['SERVER_PROTOCOL'] ?? $this->protocolVersion);
192 1
        $this->serverSoftware  = $server['SERVER_SOFTWARE'] ?? '';
193
        $this->queryParams     = $_GET;
194
        $this->cookieParams    = $_COOKIE;
195 52
        if (false === $this->isSafeMethod()) {
196 52
            $this->parseInput();
197
        }
198 52
        if ($_FILES) {
199
            $this->uploadedFiles = $this->parseUploadedFiles($_FILES);
200 52
        }
201 52
    }
202 52
203 52
    /**
204
     * [IMPORTANT]  In REST apps PUT and PATCH are essential methods.
205 52
     *              This rule is changed to support them in a same
206 22
     *              way as being a POST method, by not checking for
207
     *              Content-Type value.
208
     *
209 52
     * Per recommendation:
210 4
     *
211
     * @return bool If the request Content-Type is either
212 52
     * application/x-www-form-urlencoded or multipart/form-data
213
     * and the request method is POST,
214
     * then it MUST return the contents of $_POST
215
     *
216
     * @see ServerRequestInterface::withParsedBody()
217
     * @see ServerRequestInterface::getParsedBody()
218
     */
219
    protected function useOnlyPost(): bool
220
    {
221
        if ($this->method === HttpMethod::PUT ||
222
            $this->method === HttpMethod::PATCH ||
223
            empty($contentType = $this->getHeaderLine('Content-Type'))) {
224
            return false;
225 23
        }
226
        return $this->method === HttpMethod::POST && (
227 23
            str_contains($contentType, 'application/x-www-form-urlencoded') ||
228 16
            str_contains($contentType, 'multipart/form-data')
229
        );
230
    }
231 7
232 4
    /**
233 7
     * Try to unserialize a JSON string or form encoded request body.
234
     * Very useful if JavaScript app stringify objects in AJAX requests.
235
     */
236
    protected function parseInput(): void
237
    {
238
        if (empty($input = $this->getRawInput())) {
239
            return;
240 22
        }
241
        // Try XML deserialization
242 22
        if (str_starts_with($input, '<?xml')) {
243 22
            $this->parsedBody = (new XmlSerializer(null))->unserialize($input) ?: [];
244
            return;
245
        }
246
        // Try JSON deserialization
247 2
        $this->parsedBody = json_decode($input, true, 512, JSON_BIGINT_AS_STRING);
248
        if (null === $this->parsedBody) {
249 2
            // Fallback to application/x-www-form-urlencoded
250 1
            parse_str($input, $this->parsedBody);
251
        }
252 2
    }
253
254 22
    protected function getRawInput(): string
255
    {
256 22
        return file_get_contents('php://input') ?: '';
257
    }
258
}
259