Passed
Pull Request — master (#19)
by
unknown
02:01
created

ServerRequestFactory::createFromGlobals()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.686

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 8
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 11
ccs 4
cts 9
cp 0.4444
crap 2.686
rs 10
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 50
    }
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 9
            if ($name === 'Host' && $request->hasHeader('Host')) {
110 9
                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 7
            . '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 16
            $uri = preg_match('/^(.+):(\d+)$/', $server['HTTP_HOST'], $matches) === 1
170 6
                ? $uri->withHost($matches[1])->withPort((int) $matches[2])
171 10
                : $uri->withHost($server['HTTP_HOST'])
172
            ;
173 33
        } elseif (isset($server['SERVER_NAME'])) {
174 2
            $uri = $uri->withHost($server['SERVER_NAME']);
175
        }
176
177 49
        if (isset($server['REQUEST_URI'])) {
178 2
            $uri = $uri->withPath(explode('?', $server['REQUEST_URI'])[0]);
179
        }
180
181 49
        if (isset($server['QUERY_STRING'])) {
182 2
            $uri = $uri->withQuery($server['QUERY_STRING']);
183
        }
184
185 49
        return $uri;
186
    }
187
188
    /**
189
     * @psalm-return array<string, string>
190
     */
191 22
    private function getHeadersFromGlobals(): array
192
    {
193 22
        if (function_exists('getallheaders') && ($headers = getallheaders()) !== false) {
194
            /** @psalm-var array<string, string> $headers */
195
            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...
196
        }
197
198 22
        $headers = [];
199
200
        /**
201
         * @var string $name
202
         * @var string $value
203
         */
204 22
        foreach ($_SERVER as $name => $value) {
205 22
            if (strncmp($name, 'REDIRECT_', 9) === 0) {
206 1
                $name = substr($name, 9);
207
208 1
                if (array_key_exists($name, $_SERVER)) {
209 1
                    continue;
210
                }
211
            }
212
213 22
            if (strncmp($name, 'HTTP_', 5) === 0) {
214 9
                $headers[$this->normalizeHeaderName(substr($name, 5))] = $value;
215 9
                continue;
216
            }
217
218 22
            if (strncmp($name, 'CONTENT_', 8) === 0) {
219 1
                $headers[$this->normalizeHeaderName($name)] = $value;
220
            }
221
        }
222
223 22
        return $headers;
224
    }
225
226 9
    private function normalizeHeaderName(string $name): string
227
    {
228 9
        return str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $name))));
229
    }
230
231 49
    private function getUploadedFilesArray(array $filesArray): array
232
    {
233 49
        $files = [];
234
235
        /** @var array $info */
236 49
        foreach ($filesArray as $class => $info) {
237 17
            $files[$class] = [];
238 17
            $this->populateUploadedFileRecursive(
239 17
                $files[$class],
240 17
                $info['name'],
241 17
                $info['tmp_name'],
242 17
                $info['type'],
243 17
                $info['size'],
244 17
                $info['error'],
245
            );
246
        }
247
248 49
        return $files;
249
    }
250
251
    /**
252
     * Populates uploaded files array from $_FILE data structure recursively.
253
     *
254
     * @param array $files Uploaded files array to be populated.
255
     * @param mixed $names File names provided by PHP.
256
     * @param mixed $tempNames Temporary file names provided by PHP.
257
     * @param mixed $types File types provided by PHP.
258
     * @param mixed $sizes File sizes provided by PHP.
259
     * @param mixed $errors Uploading issues provided by PHP.
260
     *
261
     * @psalm-suppress MixedArgument, ReferenceConstraintViolation
262
     */
263 17
    private function populateUploadedFileRecursive(
264
        array &$files,
265
        mixed $names,
266
        mixed $tempNames,
267
        mixed $types,
268
        mixed $sizes,
269
        mixed $errors
270
    ): void {
271 17
        if (is_array($names)) {
272
            /** @var array|string $name */
273 17
            foreach ($names as $i => $name) {
274 17
                $files[$i] = [];
275
                /** @psalm-suppress MixedArrayAccess */
276 17
                $this->populateUploadedFileRecursive(
277 17
                    $files[$i],
278 17
                    $name,
279 17
                    $tempNames[$i],
280 17
                    $types[$i],
281 17
                    $sizes[$i],
282 17
                    $errors[$i],
283
                );
284
            }
285
286 17
            return;
287
        }
288
289
        try {
290 17
            $stream = $this->streamFactory->createStreamFromFile($tempNames);
291 17
        } catch (RuntimeException $e) {
292 17
            $stream = $this->streamFactory->createStream();
293
        }
294
295 17
        $files = $this->uploadedFileFactory->createUploadedFile(
296 17
            $stream,
297 17
            (int) $sizes,
298 17
            (int) $errors,
299
            $names,
300
            $types
301
        );
302 17
    }
303
}
304