Passed
Pull Request — master (#53)
by Sergei
04:17 queued 01:18
created

RequestFactory::populateUploadedFileRecursive()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 38
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 4

Importance

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