Passed
Pull Request — master (#30)
by
unknown
02:50
created

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