Passed
Push — master ( 02bea5...62dfbf )
by Rutger
03:04
created

EnvironmentHelper::parseEnvVars()   B

Complexity

Conditions 9
Paths 2

Size

Total Lines 49
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 9

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 30
c 1
b 0
f 0
dl 0
loc 49
ccs 34
cts 34
cp 1
rs 8.0555
cc 9
nc 2
nop 6
crap 9
1
<?php
2
3
namespace rhertogh\Yii2Oauth2Server\helpers;
4
5
use InvalidArgumentException;
6
use rhertogh\Yii2Oauth2Server\helpers\exceptions\EnvironmentVariableNotAllowedException;
7
use rhertogh\Yii2Oauth2Server\helpers\exceptions\EnvironmentVariableNotSetException;
8
9
class EnvironmentHelper
10
{
11
    /**
12
     * Replace environment variables in a string with their respective value.
13
     * The format for env vars is '${ENV_VAR_NAME}', e.g.: 'Hello ${NAME}' will return 'Hello world' if the `NAME`
14
     * environment variable is set to 'world'.
15
     * Nesting is, when enabled via the `$parseNested` argument, also possible, e.g.:
16
     * Let's assume whe have the following environment variables set:
17
     * `NAME1=Alice`, `NAME2=Bob`, `NAMES=${NAME1} and ${NAME2}`,
18
     * the string 'Hello ${NAMES}' would return 'Hello Alice and Bob'.
19
     *
20
     * For security, at least the $allowList must be set.
21
     * When the $allowList is set the variable(s) must match at least 1 pattern in the list to be allowed
22
     * (others will be denied).
23
     * When both the $allowList and the $denyList are set the variable(s) must match at least 1 pattern in the
24
     * allowList and not match any pattern in the denyList in order to be allowed.
25
     *
26
     * Both the $allowList and $denyList can take 3 different types of patterns:
27
     * 1. Exact match, e.g.: 'MY_ENV_VAR'.
28
     * 2. Wildcard where `*` would match zero or more characters, e.g.: 'MY_ENV_*'.
29
     * 3. A regular expression, e.g.: '/^MY_[ABC]{1,3}_VAR$/'.
30
     *
31
     * By default an `EnvironmentVariableNotSetException` is thrown when a specified environment variable is not set.
32
     * Similarly, an `EnvironmentVariableNotAllowedException` is thrown when access to a specified environment variable
33
     * is not allowed by the $allowList and/or $denyList.
34
     * This behavior can be disabled by setting the $exceptionWhenNotSet or $exceptionWhenNotAllowed to `false`
35
     * respectively. In that case, instead of an exception being thrown, the specified environment variable will be
36
     * replaced with an empty string.
37
     *
38
     * @param string $string The input string containing the environment variable(s).
39
     * @param string[] $allowList List of patterns of which at least 1 has to match to allow replacement.
40
     * @param string[]|null $denyList List of patterns of which any match will deny replacement.
41
     * @param bool $parseNested Should nested (a.k.a. recursive) environment variables be parsed.
42
     * @param bool $exceptionWhenNotSet Throw an exception when an environment variable is not set (default behavior),
43
     * silently use an empty string as the value otherwise.
44
     * @param bool $exceptionWhenNotAllowed Throw an exception when the usage of an environment variable is not allowed
45
     * (default behavior), silently use an empty string as the value otherwise.
46
     * @return string The input string where the environment variables are replaced with their respective value.
47
     * @throws InvalidArgumentException When a parameter has an invalid value.
48
     * @throws EnvironmentVariableNotSetException When the $string references an environment variable that is not set.
49
     * @throws EnvironmentVariableNotAllowedException When the $string references an environment variable which usage
50
     * is not allowed by the $allowList or $denyList.
51
     */
52 32
    public static function parseEnvVars(
53
        string $string,
54
        array $allowList,
55
        ?array $denyList = null,
56
        bool $parseNested = false,
57
        bool $exceptionWhenNotSet = true,
58
        bool $exceptionWhenNotAllowed = true
59
    ): string
60
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
61 32
        if (!$allowList) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowList of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
62 1
            throw new InvalidArgumentException('$allowList cannot be empty.');
63
        }
64
65 31
        return preg_replace_callback(
66 31
            '/\${(?<name>[a-zA-Z0-9_]+)}/',
67 31
            function(array $matches) use (
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
68 31
                $allowList,
69 31
                $denyList,
70 31
                $parseNested,
71 31
                $exceptionWhenNotSet,
72 31
                $exceptionWhenNotAllowed
73 31
            ) {
74 31
                $envVarName = $matches['name']; /** @var string $envVarName */
75 31
                if (!static::matchList($allowList, $envVarName)) {
76 2
                    return static::handleEnvVarNotAllowed($envVarName, $exceptionWhenNotAllowed);
77
                }
78 29
                if (!empty($denyList) && static::matchList($denyList, $envVarName)) {
79 8
                    return static::handleEnvVarNotAllowed($envVarName, $exceptionWhenNotAllowed);
80
                }
81
82 21
                $value = getenv($envVarName); /** @var string|false $value */
83 21
                if ($value === false) {
84 3
                    if ($exceptionWhenNotSet) {
85 2
                        throw new EnvironmentVariableNotSetException($envVarName);
86
                    }
87 1
                    $value = '';
88 19
                } elseif ($parseNested && (mb_strlen($value) > 3)) {
89 15
                    $value = static::parseEnvVars(
90 15
                        $value,
91 15
                        $allowList,
92 15
                        $denyList,
93 15
                        $parseNested,
94 15
                        $exceptionWhenNotSet,
95 15
                        $exceptionWhenNotAllowed,
96 15
                    );
97
                }
98 19
                return $value;
99 31
            },
100 31
            $string,
101 31
        );
102
    }
103
104
    /**
105
     * @param string[] $patterns
106
     * @param string $subject
107
     * @return bool
108
     */
109 31
    protected static function matchList($patterns, $subject)
110
    {
111 31
        foreach ($patterns as $pattern) {
112 31
            if ($pattern === '*' || $subject === $pattern) {
113 27
                return true;
114
            }
115 15
            $pregMatch = @preg_match($pattern, $subject);
116 15
            if ($pregMatch === 1) { // Regex match.
117 5
                return true;
118 13
            } elseif ($pregMatch === false) { // Not a Regex.
119 13
                if (mb_strpos($pattern, '*') !== false) {
120 7
                    $regex = '/^' . str_replace('*', '[a-zA-Z0-9_]*', $pattern) . '$/';
121 7
                    if (preg_match($regex, $subject)) {
122 3
                        return true;
123
                    }
124
                }
125
            }
126
        }
127
128 4
        return false;
129
    }
130
131
    /**
132
     * @param string $envVarName
133
     * @param bool $exceptionWhenNotAllowed
134
     * @return string
135
     * @throws EnvironmentVariableNotAllowedException
136
     */
137 10
    protected static function handleEnvVarNotAllowed(string $envVarName, bool $exceptionWhenNotAllowed): string
138
    {
139 10
        if ($exceptionWhenNotAllowed) {
140 5
            throw new EnvironmentVariableNotAllowedException($envVarName);
141
        } else {
142 5
            return '';
143
        }
144
    }
145
}
146