Passed
Pull Request — master (#53)
by Sergei
03:04 queued 30s
created

RequestFactory::parseBody()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 11
ccs 9
cts 9
cp 1
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Runner\Http;
6
7
use JsonException;
8
use Psr\Http\Message\ServerRequestFactoryInterface;
9
use Psr\Http\Message\ServerRequestInterface;
10
use Psr\Http\Message\StreamFactoryInterface;
11
use Psr\Http\Message\StreamInterface;
12
use Psr\Http\Message\UploadedFileFactoryInterface;
13
use Psr\Http\Message\UriFactoryInterface;
14
use Psr\Http\Message\UriInterface;
15
use RuntimeException;
16
use Yiisoft\Yii\Runner\Http\Exception\BadRequestException;
17
18
use function array_key_exists;
19
use function explode;
20
use function fopen;
21
use function function_exists;
22
use function getallheaders;
23
use function in_array;
24
use function is_array;
25
use function preg_match;
26
use function str_replace;
27
use function strtolower;
28
use function substr;
29
use function ucwords;
30
31
/**
32
 * `ServerRequestCreator` creates an instance of a server request.
33
 *
34
 * @internal
35
 */
36
final class RequestFactory
37
{
38 50
    public function __construct(
39
        private ServerRequestFactoryInterface $serverRequestFactory,
40
        private UriFactoryInterface $uriFactory,
41
        private UploadedFileFactoryInterface $uploadedFileFactory,
42
        private StreamFactoryInterface $streamFactory,
43
    ) {
44 50
    }
45
46
    /**
47
     * Creates an instance of a server request from custom parameters.
48
     *
49
     * @param false|resource|null $body
50
     *
51
     * @return ServerRequestInterface The server request instance.
52
     */
53 50
    public function create($body = null): ServerRequestInterface
54
    {
55
        // Create base request
56 50
        $method = $_SERVER['REQUEST_METHOD'] ?? null;
57 50
        if ($method === null) {
58 1
            throw new RuntimeException('Unable to determine HTTP request method.');
59
        }
60 49
        $request = $this->serverRequestFactory->createServerRequest($method, $this->createUri(), $_SERVER);
61
62
        // Add headers
63 49
        foreach ($this->getHeaders() as $name => $value) {
64 29
            if ($name === 'Host' && $request->hasHeader('Host')) {
65 20
                continue;
66
            }
67 10
            $request = $request->withAddedHeader($name, $value);
68
        }
69
70
        // Add protocol
71 49
        $protocol = '1.1';
72 49
        if (array_key_exists('SERVER_PROTOCOL', $_SERVER) && $_SERVER['SERVER_PROTOCOL'] !== '') {
73 2
            $protocol = str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']);
74
        }
75 49
        $request = $request->withProtocolVersion($protocol);
76
77
        // Add body
78 49
        $body = $body ?? fopen('php://input', 'rb');
79 49
        if ($body !== false) {
80 47
            $request = $request->withBody(
81 47
                $this->streamFactory->createStreamFromResource($body)
82 47
            );
83
        }
84
85
        // Add query and cookie params
86 49
        $request = $request
87 49
            ->withQueryParams($_GET)
88 49
            ->withCookieParams($_COOKIE);
89
90
        // Add uploaded files
91 49
        $files = [];
92 49
        foreach ($_FILES as $class => $info) {
93 8
            $files[$class] = [];
94 8
            $this->populateUploadedFileRecursive(
95 8
                $files[$class],
96 8
                $info['name'],
97 8
                $info['tmp_name'],
98 8
                $info['type'],
99 8
                $info['size'],
100 8
                $info['error'],
101 8
            );
102
        }
103 49
        $request = $request->withUploadedFiles($files);
104
105 49
        return $request;
106
    }
107
108
    /**
109
     * @throws BadRequestException
110
     */
111 13
    public function parseBody(ServerRequestInterface $request): ServerRequestInterface
112
    {
113 13
        $parsedBody = $this->doParseBody(
114 13
            $request->getMethod(),
115 13
            $request->getHeaderLine('content-type'),
116 13
            $request->getBody(),
117 13
        );
118
119 10
        return $parsedBody === null
120 6
            ? $request
121 10
            : $request->withParsedBody($parsedBody);
122
    }
123
124 49
    private function createUri(): UriInterface
125
    {
126 49
        $uri = $this->uriFactory->createUri();
127
128 49
        if (array_key_exists('HTTPS', $_SERVER) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off') {
129 4
            $uri = $uri->withScheme('https');
130
        } else {
131 45
            $uri = $uri->withScheme('http');
132
        }
133
134 49
        $uri = isset($_SERVER['SERVER_PORT'])
135 2
            ? $uri->withPort((int) $_SERVER['SERVER_PORT'])
136 47
            : $uri->withPort($uri->getScheme() === 'https' ? 443 : 80);
137
138 49
        if (isset($_SERVER['HTTP_HOST'])) {
139 20
            $uri = preg_match('/^(.+):(\d+)$/', $_SERVER['HTTP_HOST'], $matches) === 1
140 6
                ? $uri
141 6
                    ->withHost($matches[1])
142 6
                    ->withPort((int) $matches[2])
143 14
                : $uri->withHost($_SERVER['HTTP_HOST']);
144 29
        } elseif (isset($_SERVER['SERVER_NAME'])) {
145 2
            $uri = $uri->withHost($_SERVER['SERVER_NAME']);
146
        }
147
148 49
        if (isset($_SERVER['REQUEST_URI'])) {
149 2
            $uri = $uri->withPath(explode('?', $_SERVER['REQUEST_URI'])[0]);
150
        }
151
152 49
        if (isset($_SERVER['QUERY_STRING'])) {
153 2
            $uri = $uri->withQuery($_SERVER['QUERY_STRING']);
154
        }
155
156 49
        return $uri;
157
    }
158
159
    /**
160
     * @psalm-return array<string, string>
161
     */
162 49
    private function getHeaders(): array
163
    {
164
        /** @psalm-var array<string, string> $_SERVER */
165
166 49
        if (function_exists('getallheaders') && ($headers = getallheaders()) !== false) {
167
            /** @psalm-var array<string, string> $headers */
168
            return $headers;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $headers could return the type true which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
169
        }
170
171 49
        $headers = [];
172
173 49
        foreach ($_SERVER as $name => $value) {
174 49
            if (str_starts_with($name, 'REDIRECT_')) {
175 1
                $name = substr($name, 9);
176
177 1
                if (array_key_exists($name, $_SERVER)) {
178 1
                    continue;
179
                }
180
            }
181
182 49
            if (str_starts_with($name, 'HTTP_')) {
183 20
                $headers[$this->normalizeHeaderName(substr($name, 5))] = $value;
184 20
                continue;
185
            }
186
187 49
            if (str_starts_with($name, 'CONTENT_')) {
188 10
                $headers[$this->normalizeHeaderName($name)] = $value;
189
            }
190
        }
191
192 49
        return $headers;
193
    }
194
195 29
    private function normalizeHeaderName(string $name): string
196
    {
197 29
        return str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $name))));
198
    }
199
200
    /**
201
     * Populates uploaded files array from $_FILE data structure recursively.
202
     *
203
     * @param array $files Uploaded files array to be populated.
204
     * @param mixed $names File names provided by PHP.
205
     * @param mixed $tempNames Temporary file names provided by PHP.
206
     * @param mixed $types File types provided by PHP.
207
     * @param mixed $sizes File sizes provided by PHP.
208
     * @param mixed $errors Uploading issues provided by PHP.
209
     *
210
     * @psalm-suppress MixedArgument, ReferenceConstraintViolation
211
     */
212 8
    private function populateUploadedFileRecursive(
213
        array &$files,
214
        mixed $names,
215
        mixed $tempNames,
216
        mixed $types,
217
        mixed $sizes,
218
        mixed $errors
219
    ): void {
220 8
        if (is_array($names)) {
221
            /** @var array|string $name */
222 8
            foreach ($names as $i => $name) {
223 8
                $files[$i] = [];
224
                /** @psalm-suppress MixedArrayAccess */
225 8
                $this->populateUploadedFileRecursive(
226 8
                    $files[$i],
227 8
                    $name,
228 8
                    $tempNames[$i],
229 8
                    $types[$i],
230 8
                    $sizes[$i],
231 8
                    $errors[$i],
232 8
                );
233
            }
234
235 8
            return;
236
        }
237
238
        try {
239 8
            $stream = $this->streamFactory->createStreamFromFile($tempNames);
240 8
        } catch (RuntimeException) {
241 8
            $stream = $this->streamFactory->createStream();
242
        }
243
244 8
        $files = $this->uploadedFileFactory->createUploadedFile(
245 8
            $stream,
246 8
            (int) $sizes,
247 8
            (int) $errors,
248 8
            $names,
249 8
            $types
250 8
        );
251
    }
252
253
    /**
254
     * @throws BadRequestException
255
     */
256 13
    private function doParseBody(string $method, string $contentType, StreamInterface $body): ?array
257
    {
258 13
        if (in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) {
259 4
            return null;
260
        }
261
262
        if (
263 9
            $method === 'POST'
264
            && (
265 9
                preg_match('~^application/x-www-form-urlencoded($| |;)~', $contentType)
266 9
                || preg_match('~^multipart/form-data($| |;)~', $contentType)
267
            )
268
        ) {
269 2
            return $_POST;
270
        }
271
272 7
        if (preg_match('~^application/(|[\S]+\+)json($| |;)~', $contentType)) {
273 6
            $body = (string) $body;
274 6
            if ($body === '') {
275 1
                return null;
276
            }
277
278
            try {
279 5
                $parsedBody = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
280 1
            } catch (JsonException $e) {
281 1
                throw new BadRequestException(
282 1
                    'Error when parsing JSON request body.',
283 1
                    previous: $e
284 1
                );
285
            }
286
287 4
            if (!is_array($parsedBody)) {
288 2
                throw new BadRequestException(
289 2
                    sprintf(
290 2
                        'Parsed JSON must contain array, but "%s" given.',
291 2
                        get_debug_type($parsedBody)
292 2
                    )
293 2
                );
294
            }
295
296 2
            return $parsedBody;
297
        }
298
299 1
        return null;
300
    }
301
}
302