Passed
Push — master ( 242957...4d276b )
by Alexander
04:50 queued 02:17
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 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
        // Parse body
83 44
        if ($method === 'POST') {
84 6
            $contentType = $request->getHeaderLine('content-type');
85 6
            if (preg_match('~^application/x-www-form-urlencoded($| |;)~', $contentType)
86 6
                || preg_match('~^multipart/form-data($| |;)~', $contentType)
87
            ) {
88 2
                $request = $request->withParsedBody($_POST);
89
            }
90
        }
91
92
        // Add query and cookie params
93 44
        $request = $request
94 44
            ->withQueryParams($_GET)
95 44
            ->withCookieParams($_COOKIE);
96
97
        // Add uploaded files
98 44
        $files = [];
99
        /** @psalm-suppress PossiblyInvalidArrayAccess,PossiblyInvalidArrayOffset It's bug in Psalm < 5 */
100 44
        foreach ($_FILES as $class => $info) {
101 40
            $files[$class] = [];
102 40
            $this->populateUploadedFileRecursive(
103 40
                $files[$class],
104 40
                $info['name'],
105 40
                $info['tmp_name'],
106 40
                $info['type'],
107 40
                $info['size'],
108 40
                $info['error'],
109 40
            );
110
        }
111 44
        $request = $request->withUploadedFiles($files);
112
113 44
        return $request;
114
    }
115
116 44
    private function createUri(): UriInterface
117
    {
118 44
        $uri = $this->uriFactory->createUri();
119
120 44
        if (array_key_exists('HTTPS', $_SERVER) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off') {
121 4
            $uri = $uri->withScheme('https');
122
        } else {
123 40
            $uri = $uri->withScheme('http');
124
        }
125
126 44
        $uri = isset($_SERVER['SERVER_PORT'])
127 2
            ? $uri->withPort((int) $_SERVER['SERVER_PORT'])
128 42
            : $uri->withPort($uri->getScheme() === 'https' ? 443 : 80);
129
130 44
        if (isset($_SERVER['HTTP_HOST'])) {
131 16
            $uri = preg_match('/^(.+):(\d+)$/', $_SERVER['HTTP_HOST'], $matches) === 1
132 6
                ? $uri
133 6
                    ->withHost($matches[1])
134 6
                    ->withPort((int) $matches[2])
135 10
                : $uri->withHost($_SERVER['HTTP_HOST']);
136 28
        } elseif (isset($_SERVER['SERVER_NAME'])) {
137 2
            $uri = $uri->withHost($_SERVER['SERVER_NAME']);
138
        }
139
140 44
        if (isset($_SERVER['REQUEST_URI'])) {
141 2
            $uri = $uri->withPath(explode('?', $_SERVER['REQUEST_URI'])[0]);
142
        }
143
144 44
        if (isset($_SERVER['QUERY_STRING'])) {
145 2
            $uri = $uri->withQuery($_SERVER['QUERY_STRING']);
146
        }
147
148 44
        return $uri;
149
    }
150
151
    /**
152
     * @psalm-return array<string, string>
153
     */
154 44
    private function getHeaders(): array
155
    {
156
        /** @psalm-var array<string, string> $_SERVER */
157
158 44
        if (function_exists('getallheaders') && ($headers = getallheaders()) !== false) {
159
            /** @psalm-var array<string, string> $headers */
160
            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...
161
        }
162
163 44
        $headers = [];
164
165 44
        foreach ($_SERVER as $name => $value) {
166 44
            if (str_starts_with($name, 'REDIRECT_')) {
167 1
                $name = substr($name, 9);
168
169 1
                if (array_key_exists($name, $_SERVER)) {
170 1
                    continue;
171
                }
172
            }
173
174 44
            if (str_starts_with($name, 'HTTP_')) {
175 16
                $headers[$this->normalizeHeaderName(substr($name, 5))] = $value;
176 16
                continue;
177
            }
178
179 44
            if (str_starts_with($name, 'CONTENT_')) {
180 5
                $headers[$this->normalizeHeaderName($name)] = $value;
181
            }
182
        }
183
184 44
        return $headers;
185
    }
186
187 20
    private function normalizeHeaderName(string $name): string
188
    {
189 20
        return str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $name))));
190
    }
191
192
    /**
193
     * Populates uploaded files array from $_FILE data structure recursively.
194
     *
195
     * @param array $files Uploaded files array to be populated.
196
     * @param mixed $names File names provided by PHP.
197
     * @param mixed $tempNames Temporary file names provided by PHP.
198
     * @param mixed $types File types provided by PHP.
199
     * @param mixed $sizes File sizes provided by PHP.
200
     * @param mixed $errors Uploading issues provided by PHP.
201
     *
202
     * @psalm-suppress MixedArgument, ReferenceConstraintViolation
203
     */
204 40
    private function populateUploadedFileRecursive(
205
        array &$files,
206
        mixed $names,
207
        mixed $tempNames,
208
        mixed $types,
209
        mixed $sizes,
210
        mixed $errors
211
    ): void {
212 40
        if (is_array($names)) {
213
            /** @var array|string $name */
214 40
            foreach ($names as $i => $name) {
215 40
                $files[$i] = [];
216
                /** @psalm-suppress MixedArrayAccess */
217 40
                $this->populateUploadedFileRecursive(
218 40
                    $files[$i],
219 40
                    $name,
220 40
                    $tempNames[$i],
221 40
                    $types[$i],
222 40
                    $sizes[$i],
223 40
                    $errors[$i],
224 40
                );
225
            }
226
227 40
            return;
228
        }
229
230
        try {
231 40
            $stream = $this->streamFactory->createStreamFromFile($tempNames);
232 40
        } catch (RuntimeException) {
233 40
            $stream = $this->streamFactory->createStream();
234
        }
235
236 40
        $files = $this->uploadedFileFactory->createUploadedFile(
237 40
            $stream,
238 40
            (int) $sizes,
239 40
            (int) $errors,
240 40
            $names,
241 40
            $types
242 40
        );
243
    }
244
}
245