Passed
Pull Request — master (#53)
by Sergei
07:34 queued 04:53
created

ServerRequestFactory::createFromGlobals()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

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