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

RequestFactory::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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