RequestFactory   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 210
Duplicated Lines 0 %

Test Coverage

Coverage 99.01%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 93
c 1
b 0
f 0
dl 0
loc 210
ccs 100
cts 101
cp 0.9901
rs 9.44
wmc 37

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B getHeaders() 0 31 8
A populateUploadedFileRecursive() 0 38 4
B createUri() 0 33 11
A normalizeHeaderName() 0 3 1
C create() 0 65 12
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 24
            if ($name === 'Host' && $request->hasHeader('Host')) {
61 20
                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);
0 ignored issues
show
Bug introduced by
The method withParsedBody() does not exist on Psr\Http\Message\MessageInterface. It seems like you code against a sub-type of Psr\Http\Message\MessageInterface such as Psr\Http\Message\ServerRequestInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

88
                /** @scrutinizer ignore-call */ 
89
                $request = $request->withParsedBody($_POST);
Loading history...
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 44
            $files[$class] = [];
102 44
            $this->populateUploadedFileRecursive(
103 44
                $files[$class],
104 44
                $info['name'],
105 44
                $info['tmp_name'],
106 44
                $info['type'],
107 44
                $info['size'],
108 44
                $info['error'],
109 44
            );
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 20
            $uri = preg_match('/^(.+):(\d+)$/', $_SERVER['HTTP_HOST'], $matches) === 1
132 6
                ? $uri
133 6
                    ->withHost($matches[1])
134 6
                    ->withPort((int) $matches[2])
135 14
                : $uri->withHost($_SERVER['HTTP_HOST']);
136 24
        } 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 20
                $headers[$this->normalizeHeaderName(substr($name, 5))] = $value;
176 20
                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 24
    private function normalizeHeaderName(string $name): string
188
    {
189 24
        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 44
    private function populateUploadedFileRecursive(
205
        array &$files,
206
        mixed $names,
207
        mixed $tempNames,
208
        mixed $types,
209
        mixed $sizes,
210
        mixed $errors
211
    ): void {
212 44
        if (is_array($names)) {
213
            /** @var array|string $name */
214 44
            foreach ($names as $i => $name) {
215 44
                $files[$i] = [];
216
                /** @psalm-suppress MixedArrayAccess */
217 44
                $this->populateUploadedFileRecursive(
218 44
                    $files[$i],
219 44
                    $name,
220 44
                    $tempNames[$i],
221 44
                    $types[$i],
222 44
                    $sizes[$i],
223 44
                    $errors[$i],
224 44
                );
225
            }
226
227 44
            return;
228
        }
229
230
        try {
231 44
            $stream = $this->streamFactory->createStreamFromFile($tempNames);
232 44
        } catch (RuntimeException) {
233 44
            $stream = $this->streamFactory->createStream();
234
        }
235
236 44
        $files = $this->uploadedFileFactory->createUploadedFile(
237 44
            $stream,
238 44
            (int) $sizes,
239 44
            (int) $errors,
240 44
            $names,
241 44
            $types
242 44
        );
243
    }
244
}
245