Issues (150)

src/helpers/EnvironmentHelper.php (1 issue)

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
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...
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