Passed
Push — master ( 0d207d...0ce24c )
by Eric
05:53
created

Environment::ipAddress()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 57
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 24
nc 6
nop 1
dl 0
loc 57
ccs 27
cts 27
cp 1
crap 5
rs 9.2248
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 - 2024 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
    /**
138
     * host().
139
     *
140
     * Determines current hostname.
141
     *
142
     * @param bool $stripWww        True to strip www. off the host, false to leave it be.
143
     * @param bool $acceptForwarded True to accept, false otherwise.
144
     */
145
    public static function host(bool $stripWww = false, bool $acceptForwarded = false): string
146 2
    {
147
        /** @var string $forwarded */
148
        $forwarded = Environment::var(self::HOST_HEADERS['forwarded']);
149 2
150
        /** @var string $host */
151
        $host = (
152 2
            ($acceptForwarded && ($forwarded !== ''))
153 2
            ? $forwarded
154 1
            : (Environment::var(self::HOST_HEADERS['host'], Environment::var(self::HOST_HEADERS['server'])))
155 2
        );
156 2
        $host = trim($host);
157 2
158
        if ($host === '' || preg_match(Environment::VALIDATE_HOST_REGEX, $host) === 0) {
159 2
            $host = self::HOST_HEADERS['default'];
160 1
        }
161
162
        $host = Strings::lower($host);
163 2
164
        // Strip 'www.'
165
        if ($stripWww) {
166 2
            $strippedHost = preg_replace('#^www\.#', '', $host);
167 1
        }
168
169
        return ($strippedHost ?? $host);
170 2
    }
171
172
    /**
173
     * iniGet().
174
     *
175
     * Safe ini_get taking into account its availability.
176
     *
177
     * @param string $option      The configuration option name.
178
     * @param bool   $standardize Standardize returned values to 1 or 0?
179
     *
180
     * @throws ArgumentCountError|RuntimeException
181
     */
182
    public static function iniGet(string $option, bool $standardize = false): string
183 3
    {
184
        static $iniGetAvailable;
185 3
186
        $iniGetAvailable ??= \function_exists('ini_get');
187 3
188
        if (!$iniGetAvailable) {
189 3
            // disabled_functions?
190
            // @codeCoverageIgnoreStart
191
            throw new RuntimeException('Native ini_get function not available.');
192
            // @codeCoverageIgnoreEnd
193
        }
194
195
        $value = \ini_get($option);
196 3
197
        if ($value === false) {
198 3
            throw new RuntimeException('$option does not exist.');
199 1
        }
200
201
        $value = trim($value);
202 3
203
        if ($standardize) {
204 3
            return Environment::BOOLEAN_MAPPINGS[Strings::lower($value)] ?? $value;
205 1
        }
206
207
        return $value;
208 3
    }
209
210
    /**
211
     * iniSet().
212
     *
213
     * Safe ini_set taking into account its availability.
214
     *
215
     * @param string                     $option The configuration option name.
216
     * @param null|bool|float|int|string $value  The new value for the option.
217
     *
218
     * @throws ArgumentCountError|RuntimeException
219
     *
220
     * @return false|string
221
     */
222
    public static function iniSet(string $option, null|bool|float|int|string $value): false|string
223 2
    {
224
        static $iniSetAvailable;
225 2
226
        $iniSetAvailable ??= \function_exists('ini_set');
227 2
228
        if (!$iniSetAvailable) {
229 2
            // disabled_functions?
230
            // @codeCoverageIgnoreStart
231
            throw new RuntimeException('Native ini_set function not available.');
232
            // @codeCoverageIgnoreEnd
233
        }
234
235
        return ini_set($option, $value);
236 2
    }
237
238
    /**
239
     * ipAddress().
240
     *
241
     * Return the visitor's IP address.
242
     *
243
     * @param bool $trustProxy Whether to trust HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR.
244
     */
245
    public static function ipAddress(bool $trustProxy = false): string
246 1
    {
247
        // If behind cloudflare, attempt to grab the IP forwarded from the service.
248
        $cloudflare = Environment::var(self::IP_ADDRESS_HEADERS['cloudflare']);
249 1
250
        // cloudflare connecting ip found, update REMOTE_ADDR
251
        if ($cloudflare !== '') {
252 1
            Arrays::set($_SERVER, self::IP_ADDRESS_HEADERS['default'], $cloudflare);
253 1
        }
254
255
        // If we are not trusting HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR, we return REMOTE_ADDR.
256
        if (!$trustProxy) {
257 1
            /** @var string */
258
            return Environment::var(self::IP_ADDRESS_HEADERS['default']);
259 1
        }
260
261
        $ip = '';
262 1
263 1
        /** @var string $forwarded */
264
        $forwarded = Environment::var(self::IP_ADDRESS_HEADERS['forwarded']);
265
266 1
        /** @var string $realip */
267
        $realip = Environment::var(self::IP_ADDRESS_HEADERS['realip']);
268
269 1
        /** @var list<string> $ips */
270
        $ips = match (true) {
271 1
            $forwarded !== '' => explode(',', $forwarded),
272
            $realip !== ''    => explode(',', $realip),
273 1
            default           => []
274 1
        };
275
276 1
        /** @var list<string> $ips */
277
        $ips = Arrays::mapDeep($ips, 'trim');
278
279
        // Filter out any potentially empty entries
280 1
        $ips = array_filter($ips, static fn (string $string): bool => Strings::length($string) > 0);
281
282
        // Traverses the $ips array. Set $ip to current value if it's a public IP.
283 1
        array_walk($ips, static function (string $value, int $key) use (&$ip): string {
284
            if (Environment::isPublicIp($value)) {
285
                $ip = $value;
286 1
            }
287 1
288 1
            return $ip;
289
        });
290
        unset($ips);
291 1
292 1
        // If at this point $ip is empty, then we are not dealing with proxy ip's
293 1
        if ($ip === '') {
294
            /** @var string $ip */
295
            $ip = Environment::var(
296 1
                self::IP_ADDRESS_HEADERS['client'],
297
                Environment::var(self::IP_ADDRESS_HEADERS['default'])
298 1
            );
299 1
        }
300 1
301 1
        return $ip;
302
    }
303
304 1
    /**
305
     * isHttps().
306
     *
307
     * Checks to see if SSL is in use.
308
     */
309
    public static function isHttps(): bool
310
    {
311
        $headers = getallheaders();
312 2
313
        $server    = Environment::var(self::HTTPS_HEADERS['default']);
314 2
        $frontEnd  = Arrays::get($headers, self::HTTPS_HEADERS['forwarded'], '');
315
        $forwarded = Arrays::get($headers, self::HTTPS_HEADERS['frontend'], '');
316 2
317 2
        if ($server !== 'off' && $server !== '') {
318 2
            return true;
319
        }
320 2
321 2
        return $forwarded === 'https' || ($frontEnd !== '' && $frontEnd !== 'off');
322
    }
323
324 2
    /**
325
     * isPrivateIp().
326
     *
327
     * Determines if an IP address is within the private range.
328
     *
329
     * @param string $ipaddress IP address to check.
330
     */
331
    public static function isPrivateIp(string $ipaddress): bool
332
    {
333
        return !(bool) filter_var(
334 3
            $ipaddress,
335
            FILTER_VALIDATE_IP,
336 3
            FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE
337 3
        );
338 3
    }
339 3
340 3
    /**
341
     * isPublicIp().
342
     *
343
     * Determines if an IP address is not within the private or reserved ranges.
344
     *
345
     * @param string $ipaddress IP address to check.
346
     */
347
    public static function isPublicIp(string $ipaddress): bool
348
    {
349
        return (!Environment::isPrivateIp($ipaddress) && !Environment::isReservedIp($ipaddress));
350 2
    }
351
352 2
    /**
353
     * isReservedIp().
354
     *
355
     * Determines if an IP address is within the reserved range.
356
     *
357
     * @param string $ipaddress IP address to check.
358
     */
359
    public static function isReservedIp(string $ipaddress): bool
360
    {
361
        return !(bool) filter_var(
362 3
            $ipaddress,
363
            FILTER_VALIDATE_IP,
364 3
            FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_RES_RANGE
365 3
        );
366 3
    }
367 3
368 3
    /**
369
     * requestMethod().
370
     *
371
     * Gets the request method.
372
     */
373
    public static function requestMethod(): string
374
    {
375
        /** @var string $method */
376 1
        $method = (
377
            Environment::var(
378
                self::REQUEST_HEADERS['override'],
379 1
                Environment::var(
380 1
                    self::REQUEST_HEADERS['method'],
381 1
                    self::REQUEST_HEADERS['default']
382 1
                )
383 1
            )
384 1
        );
385 1
386 1
        return Strings::upper($method);
387 1
    }
388
389 1
    /**
390
     * url().
391
     *
392
     * Retrieve the current URL.
393
     */
394
    public static function url(): string
395
    {
396
        // Scheme
397 1
        $scheme = (Environment::isHttps()) ? 'https://' : 'http://';
398
399
        // Auth
400 1
        $authUser = Environment::var(self::URL_HEADERS['authuser']);
401
        $authPwd  = Environment::var(self::URL_HEADERS['authpw']);
402
        $auth     = \sprintf('%s:%s@', $authUser, $authPwd);
403 1
404 1
        if ($auth === ':@') {
405 1
            $auth = '';
406
        }
407
408 1
        // Host and port
409
        $host = Environment::host();
410
411 1
        /** @var int $port */
412 1
        $port = Environment::var(self::URL_HEADERS['port'], 0);
413
        $port = ($port === (Environment::isHttps() ? Environment::PORT_SECURE : Environment::PORT_UNSECURE) || $port === 0) ? '' : ':' . $port;
414
415
        // Path
416 1
        /** @var string $self */
417
        $self = Environment::var(self::URL_HEADERS['self']);
418
419 1
        /** @var string $query */
420
        $query = Environment::var(self::URL_HEADERS['query']);
421
422 1
        /** @var string $request */
423
        $request = Environment::var(self::URL_HEADERS['request']);
424
425 1
        /** @var string $path */
426
        $path = ($request === '' ? $self . ($query !== '' ? '?' . $query : '') : $request);
427
428
        // Put it all together
429 1
        /** @var non-falsy-string $url */
430
        $url = \sprintf('%s%s%s%s%s', $scheme, $auth, $host, $port, $path);
431 1
432
        return $url;
433
    }
434
435
    /**
436
     * var().
437
     *
438
     * Gets a variable from $_SERVER using $default if not provided.
439
     *
440
     * @param string          $var     Variable name.
441
     * @param null|int|string $default Default value to substitute.
442 5
     */
443
    public static function var(string $var, null|int|string $default = ''): null|int|string
444
    {
445 5
        /** @var null|int|string $value */
446
        $value = Arrays::get($_SERVER, $var) ?? $default;
447 5
448
        return $value;
449
    }
450
}
451