Passed
Pull Request — master (#53)
by Sergei
02:59
created

RequestFactory::normalizeHeaderName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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
 * `RequestFactory` 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
        /** @psalm-suppress RedundantCondition It's bug in Psalm < 5 */
73 49
        if (array_key_exists('SERVER_PROTOCOL', $_SERVER) && $_SERVER['SERVER_PROTOCOL'] !== '') {
74 2
            $protocol = str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']);
75
        }
76 49
        $request = $request->withProtocolVersion($protocol);
77
78
        // Add body
79 49
        $body ??= fopen('php://input', 'rb');
80 49
        if ($body !== false) {
81 47
            $request = $request->withBody(
82 47
                $this->streamFactory->createStreamFromResource($body)
83 47
            );
84
        }
85
86
        // Add query and cookie params
87 49
        $request = $request
88 49
            ->withQueryParams($_GET)
89 49
            ->withCookieParams($_COOKIE);
90
91
        // Add uploaded files
92 49
        $files = [];
93
        /** @psalm-suppress PossiblyInvalidArrayAccess,PossiblyInvalidArrayOffset It's bug in Psalm < 5 */
94 49
        foreach ($_FILES as $class => $info) {
95 5
            $files[$class] = [];
96 5
            $this->populateUploadedFileRecursive(
97 5
                $files[$class],
98 5
                $info['name'],
99 5
                $info['tmp_name'],
100 5
                $info['type'],
101 5
                $info['size'],
102 5
                $info['error'],
103 5
            );
104
        }
105 49
        $request = $request->withUploadedFiles($files);
106
107 49
        return $request;
108
    }
109
110
    /**
111
     * @throws BadRequestException
112
     */
113 13
    public function parseBody(ServerRequestInterface $request): ServerRequestInterface
114
    {
115 13
        $parsedBody = $this->doParseBody(
116 13
            $request->getMethod(),
117 13
            $request->getHeaderLine('content-type'),
118 13
            $request->getBody(),
119 13
        );
120
121 10
        return $parsedBody === null
122 6
            ? $request
123 10
            : $request->withParsedBody($parsedBody);
124
    }
125
126 49
    private function createUri(): UriInterface
127
    {
128 49
        $uri = $this->uriFactory->createUri();
129
130 49
        if (array_key_exists('HTTPS', $_SERVER) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off') {
131 4
            $uri = $uri->withScheme('https');
132
        } else {
133 45
            $uri = $uri->withScheme('http');
134
        }
135
136 49
        $uri = isset($_SERVER['SERVER_PORT'])
137 2
            ? $uri->withPort((int) $_SERVER['SERVER_PORT'])
138 47
            : $uri->withPort($uri->getScheme() === 'https' ? 443 : 80);
139
140 49
        if (isset($_SERVER['HTTP_HOST'])) {
141 20
            $uri = preg_match('/^(.+):(\d+)$/', $_SERVER['HTTP_HOST'], $matches) === 1
142 6
                ? $uri
143 6
                    ->withHost($matches[1])
144 6
                    ->withPort((int) $matches[2])
145 14
                : $uri->withHost($_SERVER['HTTP_HOST']);
146 29
        } elseif (isset($_SERVER['SERVER_NAME'])) {
147 2
            $uri = $uri->withHost($_SERVER['SERVER_NAME']);
148
        }
149
150 49
        if (isset($_SERVER['REQUEST_URI'])) {
151 2
            $uri = $uri->withPath(explode('?', $_SERVER['REQUEST_URI'])[0]);
152
        }
153
154 49
        if (isset($_SERVER['QUERY_STRING'])) {
155 2
            $uri = $uri->withQuery($_SERVER['QUERY_STRING']);
156
        }
157
158 49
        return $uri;
159
    }
160
161
    /**
162
     * @psalm-return array<string, string>
163
     */
164 49
    private function getHeaders(): array
165
    {
166
        /** @psalm-var array<string, string> $_SERVER */
167
168 49
        if (function_exists('getallheaders') && ($headers = getallheaders()) !== false) {
169
            /** @psalm-var array<string, string> $headers */
170
            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...
171
        }
172
173 49
        $headers = [];
174
175 49
        foreach ($_SERVER as $name => $value) {
176 49
            if (str_starts_with($name, 'REDIRECT_')) {
177 1
                $name = substr($name, 9);
178
179 1
                if (array_key_exists($name, $_SERVER)) {
180 1
                    continue;
181
                }
182
            }
183
184 49
            if (str_starts_with($name, 'HTTP_')) {
185 20
                $headers[$this->normalizeHeaderName(substr($name, 5))] = $value;
186 20
                continue;
187
            }
188
189 49
            if (str_starts_with($name, 'CONTENT_')) {
190 10
                $headers[$this->normalizeHeaderName($name)] = $value;
191
            }
192
        }
193
194 49
        return $headers;
195
    }
196
197 29
    private function normalizeHeaderName(string $name): string
198
    {
199 29
        return str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $name))));
200
    }
201
202
    /**
203
     * Populates uploaded files array from $_FILE data structure recursively.
204
     *
205
     * @param array $files Uploaded files array to be populated.
206
     * @param mixed $names File names provided by PHP.
207
     * @param mixed $tempNames Temporary file names provided by PHP.
208
     * @param mixed $types File types provided by PHP.
209
     * @param mixed $sizes File sizes provided by PHP.
210
     * @param mixed $errors Uploading issues provided by PHP.
211
     *
212
     * @psalm-suppress MixedArgument, ReferenceConstraintViolation
213
     */
214 5
    private function populateUploadedFileRecursive(
215
        array &$files,
216
        mixed $names,
217
        mixed $tempNames,
218
        mixed $types,
219
        mixed $sizes,
220
        mixed $errors
221
    ): void {
222 5
        if (is_array($names)) {
223
            /** @var array|string $name */
224 5
            foreach ($names as $i => $name) {
225 5
                $files[$i] = [];
226
                /** @psalm-suppress MixedArrayAccess */
227 5
                $this->populateUploadedFileRecursive(
228 5
                    $files[$i],
229 5
                    $name,
230 5
                    $tempNames[$i],
231 5
                    $types[$i],
232 5
                    $sizes[$i],
233 5
                    $errors[$i],
234 5
                );
235
            }
236
237 5
            return;
238
        }
239
240
        try {
241 5
            $stream = $this->streamFactory->createStreamFromFile($tempNames);
242 5
        } catch (RuntimeException) {
243 5
            $stream = $this->streamFactory->createStream();
244
        }
245
246 5
        $files = $this->uploadedFileFactory->createUploadedFile(
247 5
            $stream,
248 5
            (int) $sizes,
249 5
            (int) $errors,
250 5
            $names,
251 5
            $types
252 5
        );
253
    }
254
255
    /**
256
     * @throws BadRequestException
257
     */
258 13
    private function doParseBody(string $method, string $contentType, StreamInterface $body): ?array
259
    {
260 13
        if (in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) {
261 4
            return null;
262
        }
263
264
        if (
265 9
            $method === 'POST'
266
            && (
267 9
                preg_match('~^application/x-www-form-urlencoded($| |;)~', $contentType)
268 9
                || preg_match('~^multipart/form-data($| |;)~', $contentType)
269
            )
270
        ) {
271 2
            return $_POST;
272
        }
273
274 7
        if (preg_match('~^application/(|[\S]+\+)json($| |;)~', $contentType)) {
275 6
            $body = (string) $body;
276 6
            if ($body === '') {
277 1
                return null;
278
            }
279
280
            try {
281 5
                $parsedBody = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
282 1
            } catch (JsonException $e) {
283 1
                throw new BadRequestException(
284 1
                    'Error when parsing JSON request body.',
285 1
                    previous: $e
286 1
                );
287
            }
288
289 4
            if (!is_array($parsedBody)) {
290 2
                throw new BadRequestException(
291 2
                    sprintf(
292 2
                        'Parsed JSON must contain array, but "%s" given.',
293 2
                        get_debug_type($parsedBody)
294 2
                    )
295 2
                );
296
            }
297
298 2
            return $parsedBody;
299
        }
300
301 1
        return null;
302
    }
303
}
304