Passed
Push — master ( f509b3...5b74be )
by Eric
12:34 queued 10:26
created

Environment::host()   A

Complexity

Conditions 6
Paths 16

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 12
nc 16
nop 2
dl 0
loc 24
c 0
b 0
f 0
cc 6
ccs 14
cts 14
cp 1
crap 6
rs 9.2222
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Utility - Collection of various PHP utility functions.
7
 *
8
 * @author    Eric Sizemore <[email protected]>
9
 * @version   2.0.0
10
 * @copyright (C) 2017 - 2024 Eric Sizemore
11
 * @license   The MIT License (MIT)
12
 *
13
 * Copyright (C) 2017 - 2024 Eric Sizemore <https://www.secondversion.com>.
14
 *
15
 * Permission is hereby granted, free of charge, to any person obtaining a copy
16
 * of this software and associated documentation files (the "Software"), to
17
 * deal in the Software without restriction, including without limitation the
18
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
19
 * sell copies of the Software, and to permit persons to whom the Software is
20
 * furnished to do so, subject to the following conditions:
21
 *
22
 * The above copyright notice and this permission notice shall be included in
23
 * all copies or substantial portions of the Software.
24
 *
25
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
31
 * THE SOFTWARE.
32
 */
33
34
namespace Esi\Utility;
35
36
// Exceptions
37
use RuntimeException;
38
use ArgumentCountError;
39
40
// Functions
41
use function explode;
42
use function count;
43
use function inet_ntop;
44
use function inet_pton;
45
use function filter_var;
46
use function trim;
47
use function preg_match;
48
use function preg_replace;
49
use function sprintf;
50
use function ini_get;
51
use function ini_set;
52
use function function_exists;
53
54
// Constants
55
use const FILTER_VALIDATE_IP;
56
use const FILTER_FLAG_IPV4;
57
use const FILTER_FLAG_IPV6;
58
use const FILTER_FLAG_NO_PRIV_RANGE;
59
use const FILTER_FLAG_NO_RES_RANGE;
60
61
/**
62
 * Environment utilities.
63
 */
64
final class Environment
65
{
66
    /**
67
     * Default https/http port numbers.
68
     *
69
     * @var int PORT_SECURE
70
     * @var int PORT_UNSECURE
71
     */
72
    public const PORT_SECURE = 443;
73
    public const PORT_UNSECURE = 80;
74
75
    /**
76
     * Regex used by Environment::host() to validate a hostname.
77
     *
78
     * @var string VALIDATE_HOST_REGEX
79
     */
80
    public const VALIDATE_HOST_REGEX = '#^\[?(?:[a-z0-9-:\]_]+\.?)+$#';
81
82
    /**
83
     * Maps values to their boolean equivalent for Environment::iniGet(standardize: true)
84
     *
85
     * @var array<string> BOOLEAN_MAPPINGS
86
     */
87
    public const BOOLEAN_MAPPINGS = [
88
        'yes'   => '1',
89
        'on'    => '1',
90
        'true'  => '1',
91
        '1'     => '1',
92
        'no'    => '0',
93
        'off'   => '0',
94
        'false' => '0',
95
        '0'     => '0',
96
    ];
97
98
    /**
99
     * The default list of headers that Environment::getIpAddress() checks for.
100
     *
101
     * @var array<string> IP_ADDRESS_HEADERS
102
     */
103
    public const IP_ADDRESS_HEADERS = [
104
        'cloudflare' => 'HTTP_CF_CONNECTING_IP',
105
        'forwarded'  => 'HTTP_X_FORWARDED_FOR',
106
        'realip'     => 'HTTP_X_REAL_IP',
107
        'client'     => 'HTTP_CLIENT_IP',
108
        'default'    => 'REMOTE_ADDR',
109
    ];
110
111
    /**
112
     * A list of headers that Environment::host() checks to determine hostname, with a default of 'localhost'
113
     * if it cannot make a determination.
114
     *
115
     * @var array<string> HOST_HEADERS
116
     */
117
    public const HOST_HEADERS = [
118
        'forwarded' => 'HTTP_X_FORWARDED_HOST',
119
        'server'    => 'SERVER_NAME',
120
        'host'      => 'HTTP_HOST',
121
        'default'   => 'localhost',
122
    ];
123
124
    /**
125
     * A list of headers that Environment::url() checks for and uses to build a URL.
126
     *
127
     * @var array<string> URL_HEADERS
128
     */
129
    public const URL_HEADERS = [
130
        'authuser' => 'PHP_AUTH_USER',
131
        'authpw'   => 'PHP_AUTH_PW',
132
        'port'     => 'SERVER_PORT',
133
        'self'     => 'PHP_SELF',
134
        'query'    => 'QUERY_STRING',
135
        'request'  => 'REQUEST_URI',
136
    ];
137
138
    /**
139
     * A list of headers that Environment::isHttps() checks for to determine if current
140
     * environment is under SSL.
141
     *
142
     * @var array<string> HTTPS_HEADERS
143
     */
144
    public const HTTPS_HEADERS = [
145
        'default'   => 'HTTPS',
146
        'forwarded' => 'X-Forwarded-Proto',
147
        'frontend'  => 'Front-End-Https',
148
    ];
149
150
    /**
151
     * A list of options/headers used by Environment::requestMethod() to determine
152
     * current request method.
153
     *
154
     * @var array<string> REQUEST_HEADERS
155
     */
156
    public const REQUEST_HEADERS = [
157
        'override' => 'HTTP_X_HTTP_METHOD_OVERRIDE',
158
        'method'   => 'REQUEST_METHOD',
159
        'default'  => 'GET',
160
    ];
161
162
    /**
163
     * requestMethod()
164
     *
165
     * Gets the request method.
166
     *
167
     * @return  string
168
     */
169 1
    public static function requestMethod(): string
170
    {
171
        /** @var string $method */
172 1
        $method = (
173 1
            Environment::var(
174 1
                self::REQUEST_HEADERS['override'],
175 1
                Environment::var(
176 1
                    self::REQUEST_HEADERS['method'],
177 1
                    self::REQUEST_HEADERS['default']
178 1
                )
179 1
            )
180 1
        );
181 1
        return Strings::upper($method);
182
    }
183
184
    /**
185
     * var()
186
     *
187
     * Gets a variable from $_SERVER using $default if not provided.
188
     *
189
     * @param   string           $var      Variable name.
190
     * @param   string|int|null  $default  Default value to substitute.
191
     * @return  string|int|null
192
     */
193 5
    public static function var(string $var, string | int | null $default = ''): string | int | null
194
    {
195
        /** @var string|int|null $value */
196 5
        $value = Arrays::get($_SERVER, $var) ?? $default;
197
198 5
        return $value;
199
    }
200
201
    /**
202
     * ipAddress()
203
     *
204
     * Return the visitor's IP address.
205
     *
206
     * @param   bool    $trustProxy  Whether to trust HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR.
207
     * @return  string
208
     */
209 1
    public static function ipAddress(bool $trustProxy = false): string
210
    {
211
        // If behind cloudflare, attempt to grab the IP forwarded from the service.
212 1
        $cloudflare = Environment::var(self::IP_ADDRESS_HEADERS['cloudflare']);
213
214
        // cloudflare connecting ip found, update REMOTE_ADDR
215 1
        if ($cloudflare !== '') {
216 1
            Arrays::set($_SERVER, self::IP_ADDRESS_HEADERS['default'], $cloudflare);
217
        }
218
219
        // If we are not trusting HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR, we return REMOTE_ADDR.
220 1
        if (!$trustProxy) {
221
            /** @var string */
222 1
            return Environment::var(self::IP_ADDRESS_HEADERS['default']);
223
        }
224
225 1
        $ip = '';
226 1
        $ips = [];
227
228
        /** @var string $forwarded */
229 1
        $forwarded = Environment::var(self::IP_ADDRESS_HEADERS['forwarded']);
230
        /** @var string $realip */
231 1
        $realip = Environment::var(self::IP_ADDRESS_HEADERS['realip']);
232
233 1
        if ($forwarded !== '') {
234
            /** @var list<string> $ips */
235 1
            $ips = explode(',', $forwarded);
236 1
        } elseif ($realip !== '') {
237
            /** @var list<string> $ips */
238 1
            $ips = explode(',', $realip);
239
        }
240
241
        /** @var list<string> $ips */
242 1
        $ips = Arrays::mapDeep($ips, 'trim');
243
        // Filter out any potentially empty entries
244 1
        $ips = array_filter(
245 1
            $ips,
1 ignored issue
show
Bug introduced by
$ips of type Esi\Utility\list is incompatible with the type array expected by parameter $array of array_filter(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

245
            /** @scrutinizer ignore-type */ $ips,
Loading history...
246 1
            static fn (string $string): int => Strings::length($string)
247 1
        );
248
249
        // Traverses the $ips array. Set $ip to current value if it's a public IP.
250 1
        array_walk($ips, static function (string $value, int $key) use (&$ip): string {
1 ignored issue
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

250
        array_walk($ips, static function (string $value, /** @scrutinizer ignore-unused */ int $key) use (&$ip): string {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
251 1
            if (Environment::isPublicIp($value)) {
252 1
                $ip = $value;
253
            }
254 1
            return $ip;
255 1
        });
256 1
        unset($ips);
257
258
        // If at this point $ip is empty, then we are not dealing with proxy ip's
259 1
        if ($ip === '') {
260
            /** @var string $ip */
261 1
            $ip = Environment::var(
262 1
                self::IP_ADDRESS_HEADERS['client'],
263 1
                Environment::var(self::IP_ADDRESS_HEADERS['default'])
264 1
            );
265
        }
266 1
        return $ip;
267
    }
268
269
    /**
270
     * isPrivateIp()
271
     *
272
     * Determines if an IP address is within the private range.
273
     *
274
     * @param   string  $ipaddress  IP address to check.
275
     * @return  bool
276
     */
277 3
    public static function isPrivateIp(string $ipaddress): bool
278
    {
279 3
        return !(bool) filter_var(
280 3
            $ipaddress,
281 3
            FILTER_VALIDATE_IP,
282 3
            FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE
283 3
        );
284
    }
285
286
    /**
287
     * isReservedIp()
288
     *
289
     * Determines if an IP address is within the reserved range.
290
     *
291
     * @param   string  $ipaddress  IP address to check.
292
     * @return  bool
293
     */
294 3
    public static function isReservedIp(string $ipaddress): bool
295
    {
296 3
        return !(bool) filter_var(
297 3
            $ipaddress,
298 3
            FILTER_VALIDATE_IP,
299 3
            FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_RES_RANGE
300 3
        );
301
    }
302
303
    /**
304
     * isPublicIp()
305
     *
306
     * Determines if an IP address is not within the private or reserved ranges.
307
     *
308
     * @param   string  $ipaddress  IP address to check.
309
     * @return  bool
310
     */
311 2
    public static function isPublicIp(string $ipaddress): bool
312
    {
313 2
        return (!Environment::isPrivateIp($ipaddress) && !Environment::isReservedIp($ipaddress));
314
    }
315
316
    /**
317
     * host()
318
     *
319
     * Determines current hostname.
320
     *
321
     * @param   bool    $stripWww         True to strip www. off the host, false to leave it be.
322
     * @param   bool    $acceptForwarded  True to accept, false otherwise.
323
     * @return  string
324
     */
325 2
    public static function host(bool $stripWww = false, bool $acceptForwarded = false): string
326
    {
327
        /** @var string $forwarded */
328 2
        $forwarded = Environment::var(self::HOST_HEADERS['forwarded']);
329
330
        /** @var string $host */
331 2
        $host = (
332 2
            ($acceptForwarded && ($forwarded !== ''))
333 1
            ? $forwarded
334 2
            : (Environment::var(self::HOST_HEADERS['host'], Environment::var(self::HOST_HEADERS['server'])))
335 2
        );
336 2
        $host = trim($host);
337
338 2
        if ($host === '' || preg_match(Environment::VALIDATE_HOST_REGEX, $host) === 0) {
339 1
            $host = self::HOST_HEADERS['default'];
340
        }
341
342 2
        $host = Strings::lower($host);
343
344
        // Strip 'www.'
345 2
        if ($stripWww) {
346 1
            $strippedHost = preg_replace('#^www\.#', '', $host);
347
        }
348 2
        return ($strippedHost ?? $host);
349
    }
350
351
    /**
352
     * isHttps()
353
     *
354
     * Checks to see if SSL is in use.
355
     *
356
     * @return  bool
357
     */
358 2
    public static function isHttps(): bool
359
    {
360 2
        $headers = \getallheaders();
361
362 2
        $server = Environment::var(self::HTTPS_HEADERS['default']);
363 2
        $frontEnd = Arrays::get($headers, self::HTTPS_HEADERS['forwarded'], '');
1 ignored issue
show
Bug introduced by
It seems like $headers can also be of type true; however, parameter $array of Esi\Utility\Arrays::get() does only seem to accept ArrayAccess|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

363
        $frontEnd = Arrays::get(/** @scrutinizer ignore-type */ $headers, self::HTTPS_HEADERS['forwarded'], '');
Loading history...
364 2
        $forwarded = Arrays::get($headers, self::HTTPS_HEADERS['frontend'], '');
365
366 2
        if ($server !== 'off' && $server !== '') {
367 2
            return true;
368
        }
369 2
        return $forwarded === 'https' || ($frontEnd !== '' && $frontEnd !== 'off');
370
    }
371
372
    /**
373
     * url()
374
     *
375
     * Retrieve the current URL.
376
     *
377
     * @return  string
378
     */
379 1
    public static function url(): string
380
    {
381
        // Scheme
382 1
        $scheme = (Environment::isHttps()) ? 'https://' : 'http://';
383
384
        // Auth
385 1
        $authUser = Environment::var(self::URL_HEADERS['authuser']);
386 1
        $authPwd = Environment::var(self::URL_HEADERS['authpw']);
387 1
        $auth = ($authUser !== '' ? $authUser . ($authPwd !== '' ? ":$authPwd" : '') . '@' : '');
388
389
        // Host and port
390 1
        $host = Environment::host();
391
392
        /** @var int $port */
393 1
        $port = Environment::var(self::URL_HEADERS['port'], 0);
394 1
        $port = ($port === (Environment::isHttps() ? Environment::PORT_SECURE : Environment::PORT_UNSECURE) || $port === 0) ? '' : ":$port";
395
396
        // Path
397
        /** @var string $self */
398 1
        $self = Environment::var(self::URL_HEADERS['self']);
399
        /** @var string $query */
400 1
        $query = Environment::var(self::URL_HEADERS['query']);
401
        /** @var string $request */
402 1
        $request = Environment::var(self::URL_HEADERS['request']);
403
        /** @var string $path */
404 1
        $path = ($request === '' ? $self . ($query !== '' ? '?' . $query : '') : $request);
405
406
        // Put it all together
407
        /** @var non-falsy-string $url */
408 1
        $url = sprintf('%s%s%s%s%s', $scheme, $auth, $host, $port, $path);
409
410 1
        return $url;
411
    }
412
413
    /**
414
     * iniGet()
415
     *
416
     * Safe ini_get taking into account its availability.
417
     *
418
     * @param   string  $option       The configuration option name.
419
     * @param   bool    $standardize  Standardize returned values to 1 or 0?
420
     * @return  string
421
     *
422
     * @throws  RuntimeException|ArgumentCountError
423
     */
424 2
    public static function iniGet(string $option, bool $standardize = false): string
425
    {
426 2
        static $iniGetAvailable;
427
428 2
        $iniGetAvailable ??= function_exists('ini_get');
429
430 2
        if (!$iniGetAvailable) {
431
            // disabled_functions?
432
            // @codeCoverageIgnoreStart
433
            throw new RuntimeException('Native ini_get function not available.');
434
            // @codeCoverageIgnoreEnd
435
        }
436
437 2
        $value = ini_get($option);
438
439 2
        if ($value === false) {
440 1
            throw new RuntimeException('$option does not exist.');
441
        }
442
443 2
        $value = trim($value);
444
445 2
        if ($standardize) {
446 1
            return Environment::BOOLEAN_MAPPINGS[Strings::lower($value)] ?? $value;
447
        }
448 2
        return $value;
449
    }
450
451
    /**
452
     * iniSet()
453
     *
454
     * Safe ini_set taking into account its availability.
455
     *
456
     * @param   string  $option  The configuration option name.
457
     * @param   string|int|float|bool|null $value   The new value for the option.
458
     * @return  string|false
459
     *
460
     * @throws RuntimeException|ArgumentCountError
461
     */
462 1
    public static function iniSet(string $option, string | int | float | bool | null $value): string | false
463
    {
464 1
        static $iniSetAvailable;
465
466 1
        $iniSetAvailable ??= function_exists('ini_set');
467
468 1
        if (!$iniSetAvailable) {
469
            // disabled_functions?
470
            // @codeCoverageIgnoreStart
471
            throw new RuntimeException('Native ini_set function not available.');
472
            // @codeCoverageIgnoreEnd
473
        }
474 1
        return ini_set($option, $value);
1 ignored issue
show
Bug introduced by
It seems like $value can also be of type boolean and null; however, parameter $value of ini_set() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

474
        return ini_set($option, /** @scrutinizer ignore-type */ $value);
Loading history...
475
    }
476
}
477