UriFactory::marshalUriFromServer()   B
last analyzed

Complexity

Conditions 8
Paths 24

Size

Total Lines 58
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 28
c 0
b 0
f 0
nc 24
nop 2
dl 0
loc 58
rs 8.4444

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Http\Message\Factory;
15
16
use Psr\Http\Message\UriInterface;
17
use Valkyrja\Http\Message\Factory\Data\HostPortAccumulator;
18
use Valkyrja\Http\Message\Uri\Contract\Uri;
19
use Valkyrja\Http\Message\Uri\Enum\Scheme;
20
use Valkyrja\Http\Message\Uri\Exception\InvalidPathException;
21
use Valkyrja\Http\Message\Uri\Exception\InvalidPortException;
22
use Valkyrja\Http\Message\Uri\Exception\InvalidQueryException;
23
use Valkyrja\Http\Message\Uri\Uri as HttpUri;
24
25
use function array_change_key_case;
26
use function array_key_exists;
27
use function explode;
28
use function implode;
29
use function is_array;
30
use function ltrim;
31
use function preg_match;
32
use function preg_replace;
33
use function strlen;
34
use function strpos;
35
use function strrpos;
36
use function strtolower;
37
use function substr;
38
39
/**
40
 * Abstract Class UriFactory.
41
 *
42
 * @author Melech Mizrachi
43
 */
44
abstract class UriFactory
45
{
46
    /**
47
     * Marshal the URI from the $_SERVER array and headers.
48
     *
49
     * @param array<string, string>          $server
50
     * @param array<string, string|string[]> $headers
51
     *
52
     * @throws InvalidQueryException
53
     * @throws InvalidPortException
54
     * @throws InvalidPathException
55
     *
56
     * @return Uri
57
     */
58
    public static function marshalUriFromServer(array $server, array $headers): Uri
59
    {
60
        $uri = new HttpUri();
61
62
        // URI scheme
63
        $scheme = Scheme::HTTP;
64
        /** @var string|null $https */
65
        $https = $server['HTTPS'] ?? null;
66
67
        if (
68
            ($https !== null && $https !== 'off')
69
            || self::getHeader('x-forwarded-proto', $headers) === Scheme::HTTPS->value
70
        ) {
71
            $scheme = Scheme::HTTPS;
72
        }
73
74
        $uri = $uri->withScheme($scheme);
75
76
        // Set the host
77
        $accumulator = new HostPortAccumulator();
78
79
        self::marshalHostAndPortFromHeaders($accumulator, $server, $headers);
80
81
        /** @var string $host */
82
        $host = $accumulator->host;
83
        /** @var int|null $port */
84
        $port = $accumulator->port;
85
86
        if (! empty($host)) {
87
            $uri = $uri->withHost($host);
88
89
            if ($port !== null) {
90
                $uri = $uri->withPort($port);
91
            }
92
        }
93
94
        // URI path
95
        $path = self::marshalRequestUri($server);
96
        $path = self::stripQueryString($path);
97
98
        // URI query
99
        $query = '';
100
101
        if (isset($server['QUERY_STRING'])) {
102
            $query = ltrim($server['QUERY_STRING'], '?');
103
        }
104
105
        // URI fragment
106
        $fragment = '';
107
108
        if (str_contains($path, '#')) {
109
            [$path, $fragment] = explode('#', $path);
110
        }
111
112
        return $uri
113
            ->withPath($path)
114
            ->withQuery($query)
115
            ->withFragment($fragment);
116
    }
117
118
    /**
119
     * Search for a header value.
120
     * Does a case-insensitive search for a matching header.
121
     * If found, it is returned as a string, using comma concatenation.
122
     * If not, the $default is returned.
123
     *
124
     * @param string                         $header
125
     * @param array<string, string|string[]> $headers
126
     * @param string|null                    $default
127
     *
128
     * @return string
129
     */
130
    public static function getHeader(string $header, array $headers, string|null $default = null): string
131
    {
132
        $header  = strtolower($header);
133
        $headers = array_change_key_case($headers);
134
135
        if (array_key_exists($header, $headers)) {
136
            return is_array($headers[$header])
137
                ? implode(', ', $headers[$header])
138
                : $headers[$header];
139
        }
140
141
        return $default ?? '';
142
    }
143
144
    /**
145
     * Marshal the host and port from HTTP headers and/or the PHP environment.
146
     *
147
     * @param HostPortAccumulator            $accumulator
148
     * @param array<string, string>          $server
149
     * @param array<string, string|string[]> $headers
150
     *
151
     * @return void
152
     */
153
    public static function marshalHostAndPortFromHeaders(
154
        HostPortAccumulator $accumulator,
155
        array $server,
156
        array $headers
157
    ): void {
158
        if (self::getHeader('host', $headers) !== '') {
159
            self::marshalHostAndPortFromHeader($accumulator, self::getHeader('host', $headers));
160
161
            return;
162
        }
163
164
        if (! isset($server['SERVER_NAME'])) {
165
            return;
166
        }
167
168
        $accumulator->host = $server['SERVER_NAME'];
169
170
        if (isset($server['SERVER_PORT'])) {
171
            $accumulator->port = (int) $server['SERVER_PORT'];
172
        }
173
174
        if (! isset($server['SERVER_ADDR']) || ! preg_match('/^\[[0-9a-fA-F\:]+\]$/', $accumulator->host)) {
175
            return;
176
        }
177
178
        // Misinterpreted IPv6-Address
179
        // Reported for Safari on Windows
180
        self::marshalIpv6HostAndPort($accumulator, $server);
181
    }
182
183
    /**
184
     * Detect the base URI for the request.
185
     * Looks at a variety of criteria in order to attempt to autodetect a base
186
     * URI, including rewrite URIs, proxy URIs, etc.
187
     * From ZF2's Zend\Http\PhpEnvironment\Request class.
188
     *
189
     * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
190
     * @license   http://framework.zend.com/license/new-bsd New BSD License
191
     *
192
     * @param array<string, string> $server
193
     *
194
     * @return string
195
     */
196
    public static function marshalRequestUri(array $server): string
197
    {
198
        // IIS7 with URL Rewrite: make sure we get the unencoded url
199
        // (double slash problem).
200
        /** @var string|null $iisUrlRewritten */
201
        $iisUrlRewritten = $server['IIS_WasUrlRewritten'] ?? null;
202
        /** @var string $unencodedUrl */
203
        $unencodedUrl = $server['UNENCODED_URL'] ?? '';
204
205
        if ($iisUrlRewritten === '1' && $unencodedUrl !== '') {
206
            return $unencodedUrl;
207
        }
208
209
        /** @var string|null $requestUri */
210
        $requestUri = $server['REQUEST_URI'] ?? null;
211
212
        // Check this first so IIS will catch.
213
        /** @var string|null $httpXRewriteUrl */
214
        $httpXRewriteUrl = $server['HTTP_X_REWRITE_URL'] ?? null;
215
216
        if ($httpXRewriteUrl !== null && $httpXRewriteUrl !== '') {
217
            $requestUri = $httpXRewriteUrl;
218
        }
219
220
        // Check for IIS 7.0 or later with ISAPI_Rewrite
221
        /** @var string|null $httpXOriginalUrl */
222
        $httpXOriginalUrl = $server['HTTP_X_ORIGINAL_URL'] ?? null;
223
224
        if ($httpXOriginalUrl !== null && $httpXOriginalUrl !== '') {
225
            $requestUri = $httpXOriginalUrl;
226
        }
227
228
        if ($requestUri !== null && $requestUri !== '') {
229
            return preg_replace('#^[^/:]+://[^/]+#', '', $requestUri) ?? $requestUri;
230
        }
231
232
        /** @var string|null $origPathInfo */
233
        $origPathInfo = $server['ORIG_PATH_INFO'] ?? null;
234
235
        if ($origPathInfo === null || $origPathInfo === '') {
236
            return '/';
237
        }
238
239
        return $origPathInfo;
240
    }
241
242
    /**
243
     * Strip the query string from a path.
244
     *
245
     * @param string $path
246
     *
247
     * @return string
248
     */
249
    public static function stripQueryString(string $path): string
250
    {
251
        if (($queryPos = strpos($path, '?')) !== false) {
252
            return substr($path, 0, $queryPos);
253
        }
254
255
        return $path;
256
    }
257
258
    /**
259
     * Get a Uri object from a PSR UriInterface object.
260
     *
261
     * @param UriInterface $uri The PSR uri
262
     *
263
     * @return Uri
264
     */
265
    public static function fromPsr(UriInterface $uri): Uri
266
    {
267
        $userInfo = $uri->getUserInfo();
268
        $user     = '';
269
        $password = null;
270
271
        if ($userInfo !== '') {
272
            [$user, $password] = explode(':', $userInfo);
273
        }
274
275
        return (new HttpUri())
276
            ->withScheme(Scheme::from($uri->getScheme()))
277
            ->withUserInfo($user, $password)
278
            ->withHost($uri->getHost())
279
            ->withPort($uri->getPort())
280
            ->withPath($uri->getPath())
281
            ->withQuery($uri->getQuery())
282
            ->withFragment($uri->getFragment());
283
    }
284
285
    /**
286
     * Marshal the host and port from the request header.
287
     *
288
     * @param HostPortAccumulator $accumulator
289
     * @param string              $host
290
     *
291
     * @return void
292
     */
293
    private static function marshalHostAndPortFromHeader(HostPortAccumulator $accumulator, string $host): void
294
    {
295
        $accumulator->host = $host;
296
        $accumulator->port = null;
297
298
        // Works for regname, IPv4 & IPv6
299
        if (preg_match('|\:(\d+)$|', $accumulator->host, $matches)) {
300
            $accumulator->host = substr($accumulator->host, 0, -1 * (strlen($matches[1]) + 1));
301
            $accumulator->port = (int) $matches[1];
302
        }
303
    }
304
305
    /**
306
     * Marshal host/port from misinterpreted IPv6 address.
307
     *
308
     * @param HostPortAccumulator   $accumulator
309
     * @param array<string, string> $server
310
     *
311
     * @return void
312
     */
313
    private static function marshalIpv6HostAndPort(HostPortAccumulator $accumulator, array $server): void
314
    {
315
        $accumulator->host = '[' . $server['SERVER_ADDR'] . ']';
316
        $accumulator->port ??= 80;
317
318
        $portOffset = strrpos($accumulator->host, ':');
319
320
        if ($portOffset !== false && (((string) $accumulator->port) . ']') === substr($accumulator->host, $portOffset + 1)) {
321
            // The last digit of the IPv6-Address has been taken as port
322
            // Unset the port so the default port can be used
323
            $accumulator->port = null;
324
        }
325
    }
326
}
327