Passed
Push — master ( 6e3e92...9760cf )
by Eric
02:58 queued 55s
created

Environment::ipAddress()   B

Complexity

Conditions 7
Paths 14

Size

Total Lines 55
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 25
nc 14
nop 1
dl 0
loc 55
ccs 29
cts 29
cp 1
crap 7
rs 8.5866
c 0
b 0
f 0

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 Esi\Utility.
7
 *
8
 * (c) 2017 - 2025 Eric Sizemore <[email protected]>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE.md file that was distributed with this source code.
12
 */
13
14
namespace Esi\Utility;
15
16
use ArgumentCountError;
17
use RuntimeException;
18
19
use function explode;
20
use function filter_var;
21
use function getallheaders;
22
use function ini_set;
23
use function preg_match;
24
use function preg_replace;
25
use function trim;
26
27
use const FILTER_FLAG_IPV4;
28
use const FILTER_FLAG_IPV6;
29
use const FILTER_FLAG_NO_PRIV_RANGE;
30
use const FILTER_FLAG_NO_RES_RANGE;
31
use const FILTER_VALIDATE_IP;
32
33
/**
34
 * Environment utilities.
35
 *
36
 * @see Tests\EnvironmentTest
37
 */
38
abstract class Environment
39
{
40
    /**
41
     * Maps values to their boolean equivalent for Environment::iniGet(standardize: true).
42
     *
43
     * @var array<string> BOOLEAN_MAPPINGS
44
     */
45
    public const BOOLEAN_MAPPINGS = [
46
        'yes'   => '1',
47
        'on'    => '1',
48
        'true'  => '1',
49
        '1'     => '1',
50
        'no'    => '0',
51
        'off'   => '0',
52
        'false' => '0',
53
        '0'     => '0',
54
    ];
55
56
    /**
57
     * A list of headers that Environment::host() checks to determine hostname, with a default of 'localhost'
58
     * if it cannot make a determination.
59
     *
60
     * @var array<string> HOST_HEADERS
61
     */
62
    public const HOST_HEADERS = [
63
        'forwarded' => 'HTTP_X_FORWARDED_HOST',
64
        'server'    => 'SERVER_NAME',
65
        'host'      => 'HTTP_HOST',
66
        'default'   => 'localhost',
67
    ];
68
69
    /**
70
     * A list of headers that Environment::isHttps() checks for to determine if current
71
     * environment is under SSL.
72
     *
73
     * @var array<string> HTTPS_HEADERS
74
     */
75
    public const HTTPS_HEADERS = [
76
        'default'   => 'HTTPS',
77
        'forwarded' => 'X-Forwarded-Proto',
78
        'frontend'  => 'Front-End-Https',
79
    ];
80
81
    /**
82
     * The default list of headers that Environment::getIpAddress() checks for.
83
     *
84
     * @var array<string> IP_ADDRESS_HEADERS
85
     */
86
    public const IP_ADDRESS_HEADERS = [
87
        'cloudflare' => 'HTTP_CF_CONNECTING_IP',
88
        'forwarded'  => 'HTTP_X_FORWARDED_FOR',
89
        'realip'     => 'HTTP_X_REAL_IP',
90
        'client'     => 'HTTP_CLIENT_IP',
91
        'default'    => 'REMOTE_ADDR',
92
    ];
93
94
    /**
95
     * Default https/http port numbers.
96
     *
97
     * @var int PORT_SECURE
98
     * @var int PORT_UNSECURE
99
     */
100
    public const PORT_SECURE = 443;
101
102
    public const PORT_UNSECURE = 80;
103
104
    /**
105
     * A list of options/headers used by Environment::requestMethod() to determine
106
     * current request method.
107
     *
108
     * @var array<string> REQUEST_HEADERS
109
     */
110
    public const REQUEST_HEADERS = [
111
        'override' => 'HTTP_X_HTTP_METHOD_OVERRIDE',
112
        'method'   => 'REQUEST_METHOD',
113
        'default'  => 'GET',
114
    ];
115
116
    /**
117
     * A list of headers that Environment::url() checks for and uses to build a URL.
118
     *
119
     * @var array<string> URL_HEADERS
120
     */
121
    public const URL_HEADERS = [
122
        'authuser' => 'PHP_AUTH_USER',
123
        'authpw'   => 'PHP_AUTH_PW',
124
        'port'     => 'SERVER_PORT',
125
        'self'     => 'PHP_SELF',
126
        'query'    => 'QUERY_STRING',
127
        'request'  => 'REQUEST_URI',
128
    ];
129
130
    /**
131
     * Regex used by Environment::host() to validate a hostname.
132
     *
133
     * @var string VALIDATE_HOST_REGEX
134
     */
135
    public const VALIDATE_HOST_REGEX = '#^\[?(?:[a-z0-9-:\]_]+\.?)+$#';
136
137
    private static ?bool $iniGetAvailable = null;
138
139
    private static ?bool $iniSetAvailable = null;
140
141
    /**
142
     * host().
143
     *
144
     * Determines current hostname.
145
     *
146
     * @param bool $stripWww        True to strip www. off the host, false to leave it be.
147
     * @param bool $acceptForwarded True to accept, false otherwise.
148
     *
149
     * @return non-empty-string
1 ignored issue
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
150
     */
151 2
    public static function host(bool $stripWww = false, bool $acceptForwarded = false): string
152
    {
153
        /** @var string $forwarded */
154 2
        $forwarded = Environment::var(self::HOST_HEADERS['forwarded']);
155
156
        /** @var string $host */
157 2
        $host = (
158 2
            ($acceptForwarded && ($forwarded !== ''))
159 1
            ? $forwarded
160 2
            : (Environment::var(self::HOST_HEADERS['host'], Environment::var(self::HOST_HEADERS['server'])))
161 2
        );
162 2
        $host = trim($host);
163
164 2
        if ($host === '' || preg_match(Environment::VALIDATE_HOST_REGEX, $host) === 0) {
165 1
            return self::HOST_HEADERS['default'];
166
        }
167
168 2
        $host = Strings::lower($host);
169
170 2
        if ($stripWww) {
171
            /** @var non-empty-string $result */
172 1
            $result = (string) preg_replace('#^www\.#', '', $host);
173
174 1
            return $result;
175
        }
176
177
        /** @var non-empty-string */
178 2
        return $host;
179
    }
180
181
    /**
182
     * iniGet().
183
     *
184
     * Safe ini_get taking into account its availability.
185
     *
186
     * @param string $option      The configuration option name.
187
     * @param bool   $standardize Standardize returned values to 1 or 0?
188
     *
189
     * @throws ArgumentCountError|RuntimeException
190
     */
191 3
    public static function iniGet(string $option, bool $standardize = false): string
192
    {
193 3
        self::$iniGetAvailable ??= \function_exists('ini_get');
194
195 3
        if (self::$iniGetAvailable === false) {
196
            //@codeCoverageIgnoreStart
197
            throw new RuntimeException('Native ini_get function not available.');
198
            //@codeCoverageIgnoreEnd
199
        }
200
201 3
        $value = \ini_get($option);
202
203 3
        if ($value === false) {
204 1
            throw new RuntimeException('$option does not exist.');
205
        }
206
207 3
        $value = trim($value);
208
209 3
        if ($standardize) {
210 1
            return Environment::BOOLEAN_MAPPINGS[Strings::lower($value)] ?? $value;
211
        }
212
213 3
        return $value;
214
    }
215
216
    /**
217
     * iniSet().
218
     *
219
     * Safe ini_set taking into account its availability.
220
     *
221
     * @param string                     $option The configuration option name.
222
     * @param null|bool|float|int|string $value  The new value for the option.
223
     *
224
     * @throws ArgumentCountError|RuntimeException
225
     *
226
     * @return false|string
227
     */
228 2
    public static function iniSet(string $option, null|bool|float|int|string $value): false|string
229
    {
230 2
        self::$iniSetAvailable ??= \function_exists('ini_set');
231
232 2
        if (self::$iniSetAvailable === false) {
233
            //@codeCoverageIgnoreStart
234
            throw new RuntimeException('Native ini_set function not available.');
235
            //@codeCoverageIgnoreEnd
236
        }
237
238 2
        return ini_set($option, (string) $value);
239
    }
240
241
    /**
242
     * ipAddress().
243
     *
244
     * Return the visitor's IP address.
245
     *
246
     * @param bool $trustProxy Whether to trust HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR.
247
     */
248 1
    public static function ipAddress(bool $trustProxy = false): string
249
    {
250
        // If behind cloudflare, attempt to grab the IP forwarded from the service.
251 1
        $cloudflare = Environment::var(self::IP_ADDRESS_HEADERS['cloudflare']);
252
253
        // cloudflare connecting ip found, update REMOTE_ADDR
254 1
        if ($cloudflare !== '') {
255
            /** @var array<string, mixed> $_SERVER */
256 1
            Arrays::set($_SERVER, self::IP_ADDRESS_HEADERS['default'], $cloudflare);
257
        }
258
259
        // If we are not trusting HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR, we return REMOTE_ADDR.
260 1
        if (!$trustProxy) {
261
            /** @var string */
262 1
            return Environment::var(self::IP_ADDRESS_HEADERS['default']);
263
        }
264
265 1
        $ip = '';
266
267
        /** @var string */
268 1
        $forwarded = Environment::var(self::IP_ADDRESS_HEADERS['forwarded']);
269
        /** @var string */
270 1
        $realip = Environment::var(self::IP_ADDRESS_HEADERS['realip']);
271
272 1
        $ips = match (true) {
273 1
            $forwarded !== '' => explode(',', $forwarded),
274 1
            $realip !== ''    => explode(',', $realip),
275 1
            default           => []
276 1
        };
277
278
        /** @var list<string> $mappedIps */
279 1
        $mappedIps = Arrays::mapDeep($ips, static fn (mixed $value): string => \is_string($value) ? trim($value) : '');
280
281 1
        $filteredIps = array_filter(
282 1
            $mappedIps,
283 1
            static fn (string $value): bool => Strings::length($value) > 0
284 1
        );
285
286 1
        foreach ($filteredIps as $value) {
287 1
            if (Environment::isPublicIp($value)) {
288 1
                $ip = $value;
289 1
                break;
290
            }
291
        }
292
293
        // If at this point $ip is empty, then we are not dealing with proxy ip's
294 1
        if ($ip === '') {
295
            /** @var string */
296 1
            $ip = Environment::var(
297 1
                self::IP_ADDRESS_HEADERS['client'],
298 1
                Environment::var(self::IP_ADDRESS_HEADERS['default'])
299 1
            );
300
        }
301
302 1
        return $ip;
303
    }
304
305
    /**
306
     * isHttps().
307
     *
308
     * Checks to see if SSL is in use.
309
     */
310 2
    public static function isHttps(): bool
311
    {
312
        /** @var array<string, string> $headers */
313 2
        $headers = getallheaders();
314
315
        /** @var string $server */
316 2
        $server    = Environment::var(self::HTTPS_HEADERS['default']);
317 2
        $frontEnd  = Arrays::get($headers, self::HTTPS_HEADERS['forwarded'], '');
318 2
        $forwarded = Arrays::get($headers, self::HTTPS_HEADERS['frontend'], '');
319
320 2
        if ($server !== 'off' && $server !== '') {
321 2
            return true;
322
        }
323
324 2
        return $forwarded === 'https' || ($frontEnd !== '' && $frontEnd !== 'off');
325
    }
326
327
    /**
328
     * isPrivateIp().
329
     *
330
     * Determines if an IP address is within the private range.
331
     *
332
     * @param string $ipaddress IP address to check.
333
     */
334 3
    public static function isPrivateIp(string $ipaddress): bool
335
    {
336 3
        return !(bool) filter_var(
337 3
            $ipaddress,
338 3
            FILTER_VALIDATE_IP,
339 3
            FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE
340 3
        );
341
    }
342
343
    /**
344
     * isPublicIp().
345
     *
346
     * Determines if an IP address is not within the private or reserved ranges.
347
     *
348
     * @param string $ipaddress IP address to check.
349
     */
350 2
    public static function isPublicIp(string $ipaddress): bool
351
    {
352 2
        return (!Environment::isPrivateIp($ipaddress) && !Environment::isReservedIp($ipaddress));
353
    }
354
355
    /**
356
     * isReservedIp().
357
     *
358
     * Determines if an IP address is within the reserved range.
359
     *
360
     * @param string $ipaddress IP address to check.
361
     */
362 3
    public static function isReservedIp(string $ipaddress): bool
363
    {
364 3
        return !(bool) filter_var(
365 3
            $ipaddress,
366 3
            FILTER_VALIDATE_IP,
367 3
            FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_RES_RANGE
368 3
        );
369
    }
370
371
    /**
372
     * requestMethod().
373
     *
374
     * Gets the request method.
375
     */
376 1
    public static function requestMethod(): string
377
    {
378
        /** @var string $method */
379 1
        $method = (
380 1
            Environment::var(
381 1
                self::REQUEST_HEADERS['override'],
382 1
                Environment::var(
383 1
                    self::REQUEST_HEADERS['method'],
384 1
                    self::REQUEST_HEADERS['default']
385 1
                )
386 1
            )
387 1
        );
388
389 1
        return Strings::upper($method);
390
    }
391
392
    /**
393
     * url().
394
     *
395
     * Retrieve the current URL.
396
     */
397 1
    public static function url(): string
398
    {
399 1
        $scheme = (Environment::isHttps()) ? 'https://' : 'http://';
400
401 1
        $authUser = (string) Environment::var(self::URL_HEADERS['authuser']);
402 1
        $authPwd  = (string) Environment::var(self::URL_HEADERS['authpw']);
403
404 1
        $auth = ($authUser !== '' || $authPwd !== '')
405 1
            ? \sprintf('%s:%s@', $authUser, $authPwd)
406 1
            : '';
407
408 1
        $host = Environment::host();
409 1
        $port = (int) Environment::var(self::URL_HEADERS['port'], 0);
410
411 1
        $portStr = ($port === (Environment::isHttps() ? Environment::PORT_SECURE : Environment::PORT_UNSECURE)
412 1
            || $port === 0) ? '' : ':' . $port;
413
414
        /** @var string $self */
415 1
        $self = Environment::var(self::URL_HEADERS['self']);
416
        /** @var string $query */
417 1
        $query = Environment::var(self::URL_HEADERS['query']);
418
        /** @var string $request */
419 1
        $request = Environment::var(self::URL_HEADERS['request']);
420
421 1
        $path = ($request === '' ? $self . ($query !== '' ? '?' . $query : '') : $request);
422
423
        /** @var non-empty-string */
424 1
        return \sprintf('%s%s%s%s%s', $scheme, $auth, $host, $portStr, $path);
425
    }
426
427
    /**
428
     * var().
429
     *
430
     * Gets a variable from $_SERVER using $default if not provided.
431
     *
432
     * @param string          $var     Variable name.
433
     * @param null|int|string $default Default value to substitute.
434
     */
435 5
    public static function var(string $var, null|int|string $default = ''): null|int|string
436
    {
437
        /** @var null|int|string $value */
438 5
        $value = Arrays::get($_SERVER, $var) ?? $default;
439
440 5
        return $value;
441
    }
442
}
443