Passed
Push — master ( e68ccc...280aab )
by Rustam
15:02 queued 12:40
created

ServerRequestFactory::getUploadedFilesArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 11
c 1
b 0
f 1
nc 2
nop 1
dl 0
loc 18
ccs 12
cts 12
cp 1
crap 2
rs 9.9
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Runner\Http;
6
7
use InvalidArgumentException;
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 is_array;
23
use function is_resource;
24
use function is_string;
25
use function preg_match;
26
use function str_replace;
27
use function strncmp;
28
use function strtolower;
29
use function substr;
30
use function ucwords;
31
32
/**
33
 * `ServerRequestFactory` creates an instance of a server request.
34
 */
35
final class ServerRequestFactory
36
{
37
    private ServerRequestFactoryInterface $serverRequestFactory;
38
    private UriFactoryInterface $uriFactory;
39
    private UploadedFileFactoryInterface $uploadedFileFactory;
40
    private StreamFactoryInterface $streamFactory;
41
42 50
    public function __construct(
43
        ServerRequestFactoryInterface $serverRequestFactory,
44
        UriFactoryInterface $uriFactory,
45
        UploadedFileFactoryInterface $uploadedFileFactory,
46
        StreamFactoryInterface $streamFactory
47
    ) {
48 50
        $this->serverRequestFactory = $serverRequestFactory;
49 50
        $this->uriFactory = $uriFactory;
50 50
        $this->uploadedFileFactory = $uploadedFileFactory;
51 50
        $this->streamFactory = $streamFactory;
52
    }
53
54
    /**
55
     * Creates an instance of a server request from PHP superglobals.
56
     *
57
     * @return ServerRequestInterface The server request instance.
58
     */
59 22
    public function createFromGlobals(): ServerRequestInterface
60
    {
61
        /** @psalm-var array<string, string> $_SERVER */
62 22
        return $this->createFromParameters(
63
            $_SERVER,
64 22
            $this->getHeadersFromGlobals(),
65
            $_COOKIE,
66
            $_GET,
67
            $_POST,
68
            $_FILES,
69 22
            fopen('php://input', 'rb') ?: null
70
        );
71
    }
72
73
    /**
74
     * Creates an instance of a server request from custom parameters.
75
     *
76
     * @param array $server
77
     * @param array $headers
78
     * @param array $cookies
79
     * @param array $get
80
     * @param array $post
81
     * @param array $files
82
     * @param resource|StreamInterface|string|null $body
83
     *
84
     * @psalm-param array<string, string> $server
85
     * @psalm-param array<string, string|string[]> $headers
86
     * @psalm-param mixed $body
87
     *
88
     * @return ServerRequestInterface The server request instance.
89
     */
90 50
    public function createFromParameters(
91
        array $server,
92
        array $headers = [],
93
        array $cookies = [],
94
        array $get = [],
95
        array $post = [],
96
        array $files = [],
97
        mixed $body = null
98
    ): ServerRequestInterface {
99 50
        $method = $server['REQUEST_METHOD'] ?? null;
100
101 50
        if ($method === null) {
102 1
            throw new RuntimeException('Unable to determine HTTP request method.');
103
        }
104
105 49
        $uri = $this->getUri($server);
106 49
        $request = $this->serverRequestFactory->createServerRequest($method, $uri, $server);
107
108 49
        foreach ($headers as $name => $value) {
109 13
            if ($name === 'Host' && $request->hasHeader('Host')) {
110 13
                continue;
111
            }
112
113 1
            $request = $request->withAddedHeader($name, $value);
114
        }
115
116 49
        $protocol = '1.1';
117 49
        if (array_key_exists('SERVER_PROTOCOL', $server) && $server['SERVER_PROTOCOL'] !== '') {
118 2
            $protocol = str_replace('HTTP/', '', $server['SERVER_PROTOCOL']);
119
        }
120
121 49
        $request = $request
122 49
            ->withProtocolVersion($protocol)
123 49
            ->withQueryParams($get)
124 49
            ->withParsedBody($post)
125 49
            ->withCookieParams($cookies)
126 49
            ->withUploadedFiles($this->getUploadedFilesArray($files))
127
        ;
128
129 49
        if ($body === null) {
130 17
            return $request;
131
        }
132
133 32
        if ($body instanceof StreamInterface) {
134 1
            return $request->withBody($body);
135
        }
136
137 31
        if (is_string($body)) {
138 1
            return $request->withBody($this->streamFactory->createStream($body));
139
        }
140
141 30
        if (is_resource($body)) {
142 23
            return $request->withBody($this->streamFactory->createStreamFromResource($body));
143
        }
144
145 7
        throw new InvalidArgumentException(
146
            'Body parameter for "ServerRequestFactory::createFromParameters()"'
147
            . 'must be instance of StreamInterface, resource or null.',
148
        );
149
    }
150
151
    /**
152
     * @psalm-param array<string, string> $server
153
     */
154 49
    private function getUri(array $server): UriInterface
155
    {
156 49
        $uri = $this->uriFactory->createUri();
157
158 49
        if (array_key_exists('HTTPS', $server) && $server['HTTPS'] !== '' && $server['HTTPS'] !== 'off') {
159 4
            $uri = $uri->withScheme('https');
160
        } else {
161 45
            $uri = $uri->withScheme('http');
162
        }
163
164 49
        $uri = isset($server['SERVER_PORT'])
165 2
            ? $uri->withPort((int)$server['SERVER_PORT'])
166 47
            : $uri->withPort($uri->getScheme() === 'https' ? 443 : 80);
167
168 49
        if (isset($server['HTTP_HOST'])) {
169 20
            $uri = preg_match('/^(.+):(\d+)$/', $server['HTTP_HOST'], $matches) === 1
170
                ? $uri
171 6
                    ->withHost($matches[1])
172 6
                    ->withPort((int) $matches[2])
173 14
                : $uri->withHost($server['HTTP_HOST'])
174
            ;
175 29
        } elseif (isset($server['SERVER_NAME'])) {
176 2
            $uri = $uri->withHost($server['SERVER_NAME']);
177
        }
178
179 49
        if (isset($server['REQUEST_URI'])) {
180 2
            $uri = $uri->withPath(explode('?', $server['REQUEST_URI'])[0]);
181
        }
182
183 49
        if (isset($server['QUERY_STRING'])) {
184 2
            $uri = $uri->withQuery($server['QUERY_STRING']);
185
        }
186
187 49
        return $uri;
188
    }
189
190
    /**
191
     * @psalm-return array<string, string>
192
     */
193 22
    private function getHeadersFromGlobals(): array
194
    {
195 22
        if (function_exists('getallheaders') && ($headers = getallheaders()) !== false) {
196
            /** @psalm-var array<string, string> $headers */
197
            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...
198
        }
199
200 22
        $headers = [];
201
202
        /**
203
         * @var string $name
204
         * @var string $value
205
         */
206 22
        foreach ($_SERVER as $name => $value) {
207 22
            if (strncmp($name, 'REDIRECT_', 9) === 0) {
208 1
                $name = substr($name, 9);
209
210 1
                if (array_key_exists($name, $_SERVER)) {
211 1
                    continue;
212
                }
213
            }
214
215 22
            if (strncmp($name, 'HTTP_', 5) === 0) {
216 13
                $headers[$this->normalizeHeaderName(substr($name, 5))] = $value;
217 13
                continue;
218
            }
219
220 22
            if (strncmp($name, 'CONTENT_', 8) === 0) {
221 1
                $headers[$this->normalizeHeaderName($name)] = $value;
222
            }
223
        }
224
225 22
        return $headers;
226
    }
227
228 13
    private function normalizeHeaderName(string $name): string
229
    {
230 13
        return str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $name))));
231
    }
232
233 49
    private function getUploadedFilesArray(array $filesArray): array
234
    {
235 49
        $files = [];
236
237
        /** @var array $info */
238 49
        foreach ($filesArray as $class => $info) {
239 5
            $files[$class] = [];
240 5
            $this->populateUploadedFileRecursive(
241 5
                $files[$class],
242 5
                $info['name'],
243 5
                $info['tmp_name'],
244 5
                $info['type'],
245 5
                $info['size'],
246 5
                $info['error'],
247
            );
248
        }
249
250 49
        return $files;
251
    }
252
253
    /**
254
     * Populates uploaded files array from $_FILE data structure recursively.
255
     *
256
     * @param array $files Uploaded files array to be populated.
257
     * @param mixed $names File names provided by PHP.
258
     * @param mixed $tempNames Temporary file names provided by PHP.
259
     * @param mixed $types File types provided by PHP.
260
     * @param mixed $sizes File sizes provided by PHP.
261
     * @param mixed $errors Uploading issues provided by PHP.
262
     *
263
     * @psalm-suppress MixedArgument, ReferenceConstraintViolation
264
     */
265 5
    private function populateUploadedFileRecursive(
266
        array &$files,
267
        mixed $names,
268
        mixed $tempNames,
269
        mixed $types,
270
        mixed $sizes,
271
        mixed $errors
272
    ): void {
273 5
        if (is_array($names)) {
274
            /** @var array|string $name */
275 5
            foreach ($names as $i => $name) {
276 5
                $files[$i] = [];
277
                /** @psalm-suppress MixedArrayAccess */
278 5
                $this->populateUploadedFileRecursive(
279 5
                    $files[$i],
280
                    $name,
281 5
                    $tempNames[$i],
282 5
                    $types[$i],
283 5
                    $sizes[$i],
284 5
                    $errors[$i],
285
                );
286
            }
287
288 5
            return;
289
        }
290
291
        try {
292 5
            $stream = $this->streamFactory->createStreamFromFile($tempNames);
293 5
        } catch (RuntimeException $e) {
294 5
            $stream = $this->streamFactory->createStream();
295
        }
296
297 5
        $files = $this->uploadedFileFactory->createUploadedFile(
298
            $stream,
299 5
            (int) $sizes,
300 5
            (int) $errors,
301
            $names,
302
            $types
303
        );
304
    }
305
}
306