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 | public const ENV_VAR_REGEX = '/\${(?<name>[a-zA-Z0-9_]+)}/'; |
||
12 | |||
13 | /** |
||
14 | * Replace environment variables in a string with their respective value. |
||
15 | * The format for env vars is '${ENV_VAR_NAME}', e.g.: 'Hello ${NAME}' will return 'Hello world' if the `NAME` |
||
16 | * environment variable is set to 'world'. |
||
17 | * Nesting is, when enabled via the `$parseNested` argument, also possible, e.g.: |
||
18 | * Let's assume whe have the following environment variables set: |
||
19 | * `NAME1=Alice`, `NAME2=Bob`, `NAMES=${NAME1} and ${NAME2}`, |
||
20 | * the string 'Hello ${NAMES}' would return 'Hello Alice and Bob'. |
||
21 | * |
||
22 | * For security, at least the $allowList must be set. |
||
23 | * When the $allowList is set the variable(s) must match at least 1 pattern in the list to be allowed |
||
24 | * (others will be denied). |
||
25 | * When both the $allowList and the $denyList are set the variable(s) must match at least 1 pattern in the |
||
26 | * allowList and not match any pattern in the denyList in order to be allowed. |
||
27 | * |
||
28 | * Both the $allowList and $denyList can take 3 different types of patterns: |
||
29 | * 1. Exact match, e.g.: 'MY_ENV_VAR'. |
||
30 | * 2. Wildcard where `*` would match zero or more characters, e.g.: 'MY_ENV_*'. |
||
31 | * 3. A regular expression, e.g.: '/^MY_[ABC]{1,3}_VAR$/'. |
||
32 | * |
||
33 | * By default an `EnvironmentVariableNotSetException` is thrown when a specified environment variable is not set. |
||
34 | * Similarly, an `EnvironmentVariableNotAllowedException` is thrown when access to a specified environment variable |
||
35 | * is not allowed by the $allowList and/or $denyList. |
||
36 | * This behavior can be disabled by setting the $exceptionWhenNotSet or $exceptionWhenNotAllowed to `false` |
||
37 | * respectively. In that case, instead of an exception being thrown, the specified environment variable will be |
||
38 | * replaced with an empty string. |
||
39 | * |
||
40 | * @param string $string The input string containing the environment variable(s). |
||
41 | * @param string[] $allowList List of patterns of which at least 1 has to match to allow replacement. |
||
42 | * @param string[]|null $denyList List of patterns of which any match will deny replacement. |
||
43 | * @param bool $parseNested Should nested (a.k.a. recursive) environment variables be parsed. |
||
44 | * @param bool $exceptionWhenNotSet Throw an exception when an environment variable is not set (default behavior), |
||
45 | * silently use an empty string as the value otherwise. |
||
46 | * @param bool $exceptionWhenNotAllowed Throw an exception when the usage of an environment variable is not allowed |
||
47 | * (default behavior), silently use an empty string as the value otherwise. |
||
48 | * @return string The input string where the environment variables are replaced with their respective value. |
||
49 | * @throws InvalidArgumentException When a parameter has an invalid value. |
||
50 | * @throws EnvironmentVariableNotSetException When the $string references an environment variable that is not set. |
||
51 | * @throws EnvironmentVariableNotAllowedException When the $string references an environment variable which usage |
||
52 | * is not allowed by the $allowList or $denyList. |
||
53 | */ |
||
54 | 32 | public static function parseEnvVars( |
|
55 | string $string, |
||
56 | array $allowList, |
||
57 | ?array $denyList = null, |
||
58 | bool $parseNested = false, |
||
59 | bool $exceptionWhenNotSet = true, |
||
60 | bool $exceptionWhenNotAllowed = true |
||
61 | ): string |
||
62 | { |
||
63 | 32 | if (!$allowList) { |
|
0 ignored issues
–
show
|
|||
64 | 1 | throw new InvalidArgumentException('$allowList cannot be empty.'); |
|
65 | } |
||
66 | |||
67 | 31 | return preg_replace_callback( |
|
68 | 31 | static::ENV_VAR_REGEX, |
|
69 | 31 | function (array $matches) use ( |
|
70 | 31 | $allowList, |
|
71 | 31 | $denyList, |
|
72 | 31 | $parseNested, |
|
73 | 31 | $exceptionWhenNotSet, |
|
74 | 31 | $exceptionWhenNotAllowed |
|
75 | 31 | ) { |
|
76 | 31 | $envVarName = $matches['name']; /** @var string $envVarName */ |
|
77 | 31 | if (!static::matchList($allowList, $envVarName)) { |
|
78 | 2 | return static::handleEnvVarNotAllowed($envVarName, $exceptionWhenNotAllowed); |
|
79 | } |
||
80 | 29 | if (!empty($denyList) && static::matchList($denyList, $envVarName)) { |
|
81 | 8 | return static::handleEnvVarNotAllowed($envVarName, $exceptionWhenNotAllowed); |
|
82 | } |
||
83 | |||
84 | 21 | $value = getenv($envVarName); /** @var string|false $value */ |
|
85 | 21 | if ($value === false) { |
|
86 | 3 | if ($exceptionWhenNotSet) { |
|
87 | 2 | throw new EnvironmentVariableNotSetException($envVarName); |
|
88 | } |
||
89 | 1 | $value = ''; |
|
90 | 19 | } elseif ($parseNested && (mb_strlen($value) > 3)) { |
|
91 | 15 | $value = static::parseEnvVars( |
|
92 | 15 | $value, |
|
93 | 15 | $allowList, |
|
94 | 15 | $denyList, |
|
95 | 15 | $parseNested, |
|
96 | 15 | $exceptionWhenNotSet, |
|
97 | 15 | $exceptionWhenNotAllowed, |
|
98 | 15 | ); |
|
99 | } |
||
100 | 19 | return $value; |
|
101 | 31 | }, |
|
102 | 31 | $string, |
|
103 | 31 | ); |
|
104 | } |
||
105 | |||
106 | /** |
||
107 | * @param string[] $patterns |
||
108 | * @param string $subject |
||
109 | * @return bool |
||
110 | */ |
||
111 | 31 | protected static function matchList($patterns, $subject) |
|
112 | { |
||
113 | 31 | foreach ($patterns as $pattern) { |
|
114 | 31 | if ($pattern === '*' || $subject === $pattern) { |
|
115 | 27 | return true; |
|
116 | } |
||
117 | 15 | $pregMatch = @preg_match($pattern, $subject); |
|
118 | 15 | if ($pregMatch === 1) { // Regex match. |
|
119 | 5 | return true; |
|
120 | 13 | } elseif ($pregMatch === false) { // Not a Regex. |
|
121 | 13 | if (mb_strpos($pattern, '*') !== false) { |
|
122 | 7 | $regex = '/^' . str_replace('*', '[a-zA-Z0-9_]*', $pattern) . '$/'; |
|
123 | 7 | if (preg_match($regex, $subject)) { |
|
124 | 3 | return true; |
|
125 | } |
||
126 | } |
||
127 | } |
||
128 | } |
||
129 | |||
130 | 4 | return false; |
|
131 | } |
||
132 | |||
133 | /** |
||
134 | * @param string $envVarName |
||
135 | * @param bool $exceptionWhenNotAllowed |
||
136 | * @return string |
||
137 | * @throws EnvironmentVariableNotAllowedException |
||
138 | */ |
||
139 | 10 | protected static function handleEnvVarNotAllowed(string $envVarName, bool $exceptionWhenNotAllowed): string |
|
140 | { |
||
141 | 10 | if ($exceptionWhenNotAllowed) { |
|
142 | 5 | throw new EnvironmentVariableNotAllowedException($envVarName); |
|
143 | } else { |
||
144 | 5 | return ''; |
|
145 | } |
||
146 | } |
||
147 | } |
||
148 |
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.