Completed
Push — v3 ( 4ecdc9...abfb2c )
by Mihail
06:27
created

ServerRequest.php (2 issues)

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 Koded\Http\Interfaces\Request;
16
use Psr\Http\Message\ServerRequestInterface;
17
18
19
class ServerRequest extends ClientRequest implements Request
20
{
21
    use CookieTrait, FilesTrait, ValidatableTrait;
22
23
    protected string $serverSoftware = '';
24
    protected array  $attributes     = [];
25
    protected array  $queryParams    = [];
26
27
    protected object|array|null $parsedBody = null;
0 ignored issues
show
The type Koded\Http\null was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
28
29
    /**
30
     * ServerRequest constructor.
31
     *
32
     * @param array $attributes
33
     */
34
    public function __construct(array $attributes = [])
35
    {
36
        parent::__construct($_SERVER['REQUEST_METHOD'] ?? Request::GET, $this->buildUri());
37
        $this->attributes = $attributes;
38
        $this->extractHttpHeaders($_SERVER);
39
        $this->extractServerData($_SERVER);
40
    }
41
42
    public function getServerParams(): array
43
    {
44
        return $_SERVER;
45
    }
46
47
    public function getQueryParams(): array
48
    {
49
        return $this->queryParams;
50
    }
51
52
    public function withQueryParams(array $query): static
53
    {
54
        $instance              = clone $this;
55
        $instance->queryParams = \array_merge($instance->queryParams, $query);
56
        return $instance;
57
    }
58
59
    public function getParsedBody(): object|array|null
60
    {
61
        if ($this->useOnlyPost()) {
62
            return $_POST;
63
        }
64
        if (false === empty($_POST)) {
65
            return $_POST;
66
        }
67
        return $this->parsedBody;
68
    }
69
70
    public function withParsedBody($data): static
71
    {
72
        $instance = clone $this;
73
        if ($this->useOnlyPost()) {
74
            $instance->parsedBody = $_POST;
75
            return $instance;
76
        }
77
        // If nothing is available for the body
78
        if (null === $data) {
79
            $instance->parsedBody = null;
80
            return $instance;
81
        }
82
        // Supports array or iterable object
83
        if (\is_iterable($data)) {
84
            $instance->parsedBody = \is_array($data) ? $data : \iterator_to_array($data);
85
            return $instance;
86
        }
87
        if (\is_object($data)) {
88
            $instance->parsedBody = $data;
89
            return $instance;
90
        }
91
        throw new \InvalidArgumentException(
92
            \sprintf('Unsupported data provided (%s), Expects NULL, array or iterable', gettype($data))
93
        );
94
    }
95
96
    public function getAttributes(): array
97
    {
98
        return $this->attributes;
99
    }
100
101
    public function getAttribute($name, $default = null): mixed
102
    {
103
        return $this->attributes[$name] ?? $default;
104
    }
105
106
    public function withAttribute($name, $value): static
107
    {
108
        $instance                    = clone $this;
109
        $instance->attributes[$name] = $value;
110
        return $instance;
111
    }
112
113
    public function withoutAttribute($name): static
114
    {
115
        $instance = clone $this;
116
        unset($instance->attributes[$name]);
117
        return $instance;
118
    }
119
120
    public function withAttributes(array $attributes): static
121
    {
122
        $instance = clone $this;
123
        foreach ($attributes as $name => $value) {
124
            $instance->attributes[$name] = $value;
125
        }
126
        return $instance;
127
    }
128
129
    public function isXHR(): bool
130
    {
131
        return 'XMLHTTPREQUEST' === \strtoupper($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '');
132
    }
133
134
    protected function buildUri(): Uri
135
    {
136
        if (\strpos($_SERVER['REQUEST_URI'] ?? '', '://')) {
137
            return new Uri($_SERVER['REQUEST_URI']);
138
        }
139
        if ($host = $_SERVER['SERVER_NAME'] ?? $_SERVER['SERVER_ADDR'] ?? '') {
140
            return new Uri('http' . ($_SERVER['HTTPS'] ?? $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? false ? 's' : '')
141
                . '://' . $host
142
                . ':' . ($_SERVER['SERVER_PORT'] ?? 80)
143
                . ($_SERVER['REQUEST_URI'] ?? '')
144
            );
145
        }
146
        return new Uri($_SERVER['REQUEST_URI'] ?? '');
147
    }
148
149
    protected function extractHttpHeaders(array $server): void
150
    {
151
        foreach ($server as $k => $v) {
152
            // Calisthenics :)
153
            \str_starts_with($k, 'HTTP_') && $this->normalizeHeader(\str_replace('HTTP_', '', $k), $v, false);
154
        }
155
        unset($this->headers['X-Forwarded-For'], $this->headers['X-Forwarded-Proto']);
156
        unset($this->headersMap['x-forwarded-for'], $this->headersMap['x-forwarded-proto']);
157
        if (isset($server['HTTP_IF_NONE_MATCH'])) {
158
            // ETag workaround for various broken Apache2 versions
159
            $this->headers['ETag']    = \str_replace('-gzip', '', $server['HTTP_IF_NONE_MATCH']);
160
            $this->headersMap['etag'] = 'ETag';
161
        }
162
        if (isset($server['CONTENT_TYPE'])) {
163
            $this->headers['Content-Type']    = \strtolower($server['CONTENT_TYPE']);
164
            $this->headersMap['content-type'] = 'Content-Type';
165
        }
166
        $this->setHost();
167
    }
168
169
    protected function extractServerData(array $server): void
170
    {
171
        $this->protocolVersion = \str_ireplace('HTTP/', '', $server['SERVER_PROTOCOL'] ?? $this->protocolVersion);
172
        $this->serverSoftware  = $server['SERVER_SOFTWARE'] ?? '';
173
        $this->queryParams     = $_GET;
174
        $this->cookieParams    = $_COOKIE;
175
        if (false === $this->isSafeMethod()) {
176
            $this->parseInput();
177
        }
178
        if ($_FILES) {
179
            $this->uploadedFiles = $this->parseUploadedFiles($_FILES);
180
        }
181
    }
182
183
    /**
184
     * Per recommendation:
185
     *
186
     * @return bool If the request Content-Type is either
187
     * application/x-www-form-urlencoded or multipart/form-data
188
     * and the request method is POST,
189
     * then it MUST return the contents of $_POST
190
     * @see ServerRequestInterface::withParsedBody()
191
     *
192
     * @see ServerRequestInterface::getParsedBody()
193
     */
194
    protected function useOnlyPost(): bool
195
    {
196
        if (empty($contentType = $this->getHeaderLine('Content-Type'))) {
197
            return false;
198
        }
199
        return $this->method === self::POST && (
200
            \str_contains('application/x-www-form-urlencoded', $contentType) ||
201
            \str_contains('multipart/form-data', $contentType));
202
    }
203
204
    /**
205
     * Try to unserialize a JSON string or form encoded request body.
206
     * Very useful if JavaScript app stringify objects in AJAX requests.
207
     */
208
    protected function parseInput(): void
209
    {
210
        if (empty($input = $this->getRawInput())) {
211
            return;
212
        }
213
        // Try JSON deserialization
214
        $this->parsedBody = \json_decode($input, true, 512, JSON_BIGINT_AS_STRING);
215
        if (null === $this->parsedBody) {
216
            \parse_str($input, $this->parsedBody);
0 ignored issues
show
It seems like $this->parsedBody can also be of type Koded\Http\null and object; however, parameter $result of parse_str() does only seem to accept array, 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

216
            \parse_str($input, /** @scrutinizer ignore-type */ $this->parsedBody);
Loading history...
217
        }
218
    }
219
220
    protected function getRawInput(): string
221
    {
222
        return \file_get_contents('php://input') ?: '';
223
    }
224
}
225