Passed
Push — master ( 0ce24c...6c61f0 )
by Eric
01:53
created

Environment::isPublicIp()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 2
rs 10
c 0
b 0
f 0
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 2
    public static function host(bool $stripWww = false, bool $acceptForwarded = false): string
146
    {
147
        /** @var string $forwarded */
148 2
        $forwarded = Environment::var(self::HOST_HEADERS['forwarded']);
149
150
        /** @var string $host */
151 2
        $host = (
152 2
            ($acceptForwarded && ($forwarded !== ''))
153 1
            ? $forwarded
154 2
            : (Environment::var(self::HOST_HEADERS['host'], Environment::var(self::HOST_HEADERS['server'])))
155 2
        );
156 2
        $host = trim($host);
157
158 2
        if ($host === '' || preg_match(Environment::VALIDATE_HOST_REGEX, $host) === 0) {
159 1
            $host = self::HOST_HEADERS['default'];
160
        }
161
162 2
        $host = Strings::lower($host);
163
164
        // Strip 'www.'
165 2
        if ($stripWww) {
166 1
            $strippedHost = preg_replace('#^www\.#', '', $host);
167
        }
168
169 2
        return ($strippedHost ?? $host);
170
    }
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 3
    public static function iniGet(string $option, bool $standardize = false): string
183
    {
184 3
        static $iniGetAvailable;
185
186 3
        $iniGetAvailable ??= \function_exists('ini_get');
187
188 3
        if (!$iniGetAvailable) {
189
            // disabled_functions?
190
            // @codeCoverageIgnoreStart
191
            throw new RuntimeException('Native ini_get function not available.');
192
            // @codeCoverageIgnoreEnd
193
        }
194
195 3
        $value = \ini_get($option);
196
197 3
        if ($value === false) {
198 1
            throw new RuntimeException('$option does not exist.');
199
        }
200
201 3
        $value = trim($value);
202
203 3
        if ($standardize) {
204 1
            return Environment::BOOLEAN_MAPPINGS[Strings::lower($value)] ?? $value;
205
        }
206
207 3
        return $value;
208
    }
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 2
    public static function iniSet(string $option, null|bool|float|int|string $value): false|string
223
    {
224 2
        static $iniSetAvailable;
225
226 2
        $iniSetAvailable ??= \function_exists('ini_set');
227
228 2
        if (!$iniSetAvailable) {
229
            // disabled_functions?
230
            // @codeCoverageIgnoreStart
231
            throw new RuntimeException('Native ini_set function not available.');
232
            // @codeCoverageIgnoreEnd
233
        }
234
235 2
        return ini_set($option, $value);
236
    }
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 1
    public static function ipAddress(bool $trustProxy = false): string
246
    {
247
        // If behind cloudflare, attempt to grab the IP forwarded from the service.
248 1
        $cloudflare = Environment::var(self::IP_ADDRESS_HEADERS['cloudflare']);
249
250
        // cloudflare connecting ip found, update REMOTE_ADDR
251 1
        if ($cloudflare !== '') {
252 1
            Arrays::set($_SERVER, self::IP_ADDRESS_HEADERS['default'], $cloudflare);
253
        }
254
255
        // If we are not trusting HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR, we return REMOTE_ADDR.
256 1
        if (!$trustProxy) {
257
            /** @var string */
258 1
            return Environment::var(self::IP_ADDRESS_HEADERS['default']);
259
        }
260
261 1
        $ip = '';
262
263
        /** @var string $forwarded */
264 1
        $forwarded = Environment::var(self::IP_ADDRESS_HEADERS['forwarded']);
265
266
        /** @var string $realip */
267 1
        $realip = Environment::var(self::IP_ADDRESS_HEADERS['realip']);
268
269
        /** @var list<string> $ips */
270 1
        $ips = match (true) {
271 1
            $forwarded !== '' => explode(',', $forwarded),
272 1
            $realip !== ''    => explode(',', $realip),
273 1
            default           => []
274 1
        };
275
276
        /** @var list<string> $ips */
277 1
        $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 1
            if (Environment::isPublicIp($value)) {
285 1
                $ip = $value;
286
            }
287
288 1
            return $ip;
289 1
        });
290 1
        unset($ips);
291
292
        // 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 1
            $ip = Environment::var(
296 1
                self::IP_ADDRESS_HEADERS['client'],
297 1
                Environment::var(self::IP_ADDRESS_HEADERS['default'])
298 1
            );
299
        }
300
301 1
        return $ip;
302
    }
303
304
    /**
305
     * isHttps().
306
     *
307
     * Checks to see if SSL is in use.
308
     */
309 2
    public static function isHttps(): bool
310
    {
311 2
        $headers = getallheaders();
312
313 2
        $server    = Environment::var(self::HTTPS_HEADERS['default']);
314 2
        $frontEnd  = Arrays::get($headers, self::HTTPS_HEADERS['forwarded'], '');
315 2
        $forwarded = Arrays::get($headers, self::HTTPS_HEADERS['frontend'], '');
316
317 2
        if ($server !== 'off' && $server !== '') {
318 2
            return true;
319
        }
320
321 2
        return $forwarded === 'https' || ($frontEnd !== '' && $frontEnd !== 'off');
322
    }
323
324
    /**
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 3
    public static function isPrivateIp(string $ipaddress): bool
332
    {
333 3
        return !(bool) filter_var(
334 3
            $ipaddress,
335 3
            FILTER_VALIDATE_IP,
336 3
            FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE
337 3
        );
338
    }
339
340
    /**
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 2
    public static function isPublicIp(string $ipaddress): bool
348
    {
349 2
        return (!Environment::isPrivateIp($ipaddress) && !Environment::isReservedIp($ipaddress));
350
    }
351
352
    /**
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 3
    public static function isReservedIp(string $ipaddress): bool
360
    {
361 3
        return !(bool) filter_var(
362 3
            $ipaddress,
363 3
            FILTER_VALIDATE_IP,
364 3
            FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_RES_RANGE
365 3
        );
366
    }
367
368
    /**
369
     * requestMethod().
370
     *
371
     * Gets the request method.
372
     */
373 1
    public static function requestMethod(): string
374
    {
375
        /** @var string $method */
376 1
        $method = (
377 1
            Environment::var(
378 1
                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
386 1
        return Strings::upper($method);
387
    }
388
389
    /**
390
     * url().
391
     *
392
     * Retrieve the current URL.
393
     */
394 1
    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 1
        $authPwd  = Environment::var(self::URL_HEADERS['authpw']);
402 1
        $auth     = \sprintf('%s:%s@', $authUser, $authPwd);
403
404 1
        if ($auth === ':@') {
405 1
            $auth = '';
406
        }
407
408
        // Host and port
409 1
        $host = Environment::host();
410
411
        /** @var int $port */
412 1
        $port = Environment::var(self::URL_HEADERS['port'], 0);
413 1
        $port = ($port === (Environment::isHttps() ? Environment::PORT_SECURE : Environment::PORT_UNSECURE) || $port === 0) ? '' : ':' . $port;
414
415
        // Path
416
        /** @var string $self */
417 1
        $self = Environment::var(self::URL_HEADERS['self']);
418
419
        /** @var string $query */
420 1
        $query = Environment::var(self::URL_HEADERS['query']);
421
422
        /** @var string $request */
423 1
        $request = Environment::var(self::URL_HEADERS['request']);
424
425
        /** @var string $path */
426 1
        $path = ($request === '' ? $self . ($query !== '' ? '?' . $query : '') : $request);
427
428
        // Put it all together
429
        /** @var non-falsy-string $url */
430 1
        $url = \sprintf('%s%s%s%s%s', $scheme, $auth, $host, $port, $path);
431
432 1
        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
     */
443 5
    public static function var(string $var, null|int|string $default = ''): null|int|string
444
    {
445
        /** @var null|int|string $value */
446 5
        $value = Arrays::get($_SERVER, $var) ?? $default;
447
448 5
        return $value;
449
    }
450
}
451