diff --git a/src/Symfony/Component/Dotenv/Dotenv.php b/src/Symfony/Component/Dotenv/Dotenv.php index b495d60cba999..e19f2cbbcdff2 100644 --- a/src/Symfony/Component/Dotenv/Dotenv.php +++ b/src/Symfony/Component/Dotenv/Dotenv.php @@ -80,7 +80,13 @@ public function usePutenv(bool $usePutenv = true): static */ public function load(string $path, string ...$extraPaths): void { - $this->doLoad(false, \func_get_args()); + $needsAutoValueResolution = 1 === \func_num_args(); + + $this->doLoad(false, $needsAutoValueResolution, \func_get_args()); + + if (!$needsAutoValueResolution) { + $this->resolveAllVariables(); + } } /** @@ -99,12 +105,16 @@ public function load(string $path, string ...$extraPaths): void */ public function loadEnv(string $path, string $envKey = null, string $defaultEnv = 'dev', array $testEnvs = ['test'], bool $overrideExistingVars = false): void { + // FIXME - Keep 'false' for loadEnv() or try to know if we will have multiple files to load? + // -> The environment is grabbed on the first doLoad() call so we cannot check conditions (except by loading the files multiple times..) + $needsAutoValueResolution = false; + $k = $envKey ?? $this->envKey; if (is_file($path) || !is_file($p = "$path.dist")) { - $this->doLoad($overrideExistingVars, [$path]); + $this->doLoad($overrideExistingVars, $needsAutoValueResolution, [$path]); } else { - $this->doLoad($overrideExistingVars, [$p]); + $this->doLoad($overrideExistingVars, $needsAutoValueResolution, [$p]); } if (null === $env = $_SERVER[$k] ?? $_ENV[$k] ?? null) { @@ -112,20 +122,28 @@ public function loadEnv(string $path, string $envKey = null, string $defaultEnv } if (!\in_array($env, $testEnvs, true) && is_file($p = "$path.local")) { - $this->doLoad($overrideExistingVars, [$p]); + $this->doLoad($overrideExistingVars, $needsAutoValueResolution, [$p]); $env = $_SERVER[$k] ?? $_ENV[$k] ?? $env; } if ('local' === $env) { + if (!$needsAutoValueResolution) { + $this->resolveAllVariables(); + } + return; } if (is_file($p = "$path.$env")) { - $this->doLoad($overrideExistingVars, [$p]); + $this->doLoad($overrideExistingVars, $needsAutoValueResolution, [$p]); } if (is_file($p = "$path.$env.local")) { - $this->doLoad($overrideExistingVars, [$p]); + $this->doLoad($overrideExistingVars, $needsAutoValueResolution, [$p]); + } + + if (!$needsAutoValueResolution) { + $this->resolveAllVariables(); } } @@ -166,7 +184,13 @@ public function bootEnv(string $path, string $defaultEnv = 'dev', array $testEnv */ public function overload(string $path, string ...$extraPaths): void { - $this->doLoad(true, \func_get_args()); + $needsAutoValueResolution = 1 === \func_num_args(); + + $this->doLoad(true, $needsAutoValueResolution, \func_get_args()); + + if (!$needsAutoValueResolution) { + $this->resolveAllVariables(); + } } /** @@ -219,12 +243,13 @@ public function populate(array $values, bool $overrideExistingVars = false): voi /** * Parses the contents of an .env file. * - * @param string $data The data to be parsed - * @param string $path The original file name where data where stored (used for more meaningful error messages) + * @param string $data The data to be parsed + * @param string $path The original file name where data where stored (used for more meaningful error messages) + * @param bool $needsValueResolution true when the value resolution needs to be done automatically * * @throws FormatException when a file has a syntax error */ - public function parse(string $data, string $path = '.env'): array + public function parse(string $data, string $path = '.env', bool $needsValueResolution = true): array { $this->path = $path; $this->data = str_replace(["\r\n", "\r"], "\n", $data); @@ -245,7 +270,7 @@ public function parse(string $data, string $path = '.env'): array break; case self::STATE_VALUE: - $this->values[$name] = $this->lexValue(); + $this->values[$name] = $this->lexValue($needsValueResolution); $state = self::STATE_VARNAME; break; } @@ -291,7 +316,7 @@ private function lexVarname(): string return $matches[2]; } - private function lexValue(): string + private function lexValue(bool $needsValueResolution): string { if (preg_match('/[ \t]*+(?:#.*)?$/Am', $this->data, $matches, 0, $this->cursor)) { $this->moveCursor($matches[0]); @@ -340,7 +365,11 @@ private function lexValue(): string ++$this->cursor; $value = str_replace(['\\"', '\r', '\n'], ['"', "\r", "\n"], $value); $resolvedValue = $value; - $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars); + + if ($needsValueResolution) { + $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars); + } + $resolvedValue = $this->resolveCommands($resolvedValue, $loadedVars); $resolvedValue = str_replace('\\\\', '\\', $resolvedValue); $v .= $resolvedValue; @@ -363,7 +392,11 @@ private function lexValue(): string } $value = rtrim($value); $resolvedValue = $value; - $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars); + + if ($needsValueResolution) { + $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars); + } + $resolvedValue = $this->resolveCommands($resolvedValue, $loadedVars); $resolvedValue = str_replace('\\\\', '\\', $resolvedValue); @@ -545,14 +578,35 @@ private function createFormatException(string $message): FormatException return new FormatException($message, new FormatExceptionContext($this->data, $this->path, $this->lineno, $this->cursor)); } - private function doLoad(bool $overrideExistingVars, array $paths): void + private function doLoad(bool $overrideExistingVars, bool $needsValueResolution, array $paths): void { foreach ($paths as $path) { if (!is_readable($path) || is_dir($path)) { throw new PathException($path); } - $this->populate($this->parse(file_get_contents($path), $path), $overrideExistingVars); + $this->populate($this->parse(file_get_contents($path), $path, $needsValueResolution), $overrideExistingVars); } } + + private function resolveAllVariables(): void + { + $resolvedVars = []; + $loadedVars = array_flip(explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? '')); + unset($loadedVars['']); + + foreach ($_ENV as $name => $value) { + if ('SYMFONY_DOTENV_VARS' === $name) { + continue; + } + + $resolvedValue = $this->resolveVariables($value, $loadedVars); + + if ($resolvedValue !== $value) { + $resolvedVars[$name] = $resolvedValue; + } + } + + $this->populate($resolvedVars, true); + } } diff --git a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php index 47c9bfba137ac..2076a02231413 100644 --- a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php +++ b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php @@ -224,6 +224,53 @@ public function testLoad() $this->assertSame('BAZ', $bar); } + public function testLoadDynamicVar() + { + unset($_ENV['VAR_STATIC1']); + unset($_ENV['VAR_STATIC2']); + unset($_ENV['VAR_DYNAMIC1']); + unset($_ENV['VAR_DYNAMIC2']); + unset($_ENV['VAR_DYNAMIC3']); + unset($_ENV['VAR_DYNAMIC_FAIL']); + unset($_SERVER['VAR_STATIC1']); + unset($_SERVER['VAR_STATIC2']); + unset($_SERVER['VAR_DYNAMIC1']); + unset($_SERVER['VAR_DYNAMIC2']); + unset($_SERVER['VAR_DYNAMIC3']); + unset($_SERVER['VAR_DYNAMIC_FAIL']); + + @mkdir($tmpdir = sys_get_temp_dir().'/dotenv'); + + $path1 = tempnam($tmpdir, 'sf-'); + $path2 = tempnam($tmpdir, 'sf-'); + $path3 = tempnam($tmpdir, 'sf-'); + + file_put_contents($path1, "VAR_STATIC1=env\nVAR_DYNAMIC1=profile:\${VAR_STATIC1}\nVAR_DYNAMIC_FAIL=\$VAR_DYNAMIC1"); + file_put_contents($path2, 'VAR_STATIC1=local'); + file_put_contents($path3, "VAR_STATIC2=file3\nVAR_DYNAMIC2=\$VAR_STATIC1\nVAR_DYNAMIC3=\$VAR_STATIC2"); + + (new Dotenv())->usePutenv()->load($path1, $path2, $path3); + + $static1 = getenv('VAR_STATIC1'); + $static2 = getenv('VAR_STATIC2'); + $dynamic1 = getenv('VAR_DYNAMIC1'); + $dynamic2 = getenv('VAR_DYNAMIC2'); + $dynamic3 = getenv('VAR_DYNAMIC3'); + $dynamicFail = getenv('VAR_DYNAMIC_FAIL'); + + unlink($path1); + unlink($path2); + unlink($path3); + rmdir($tmpdir); + + $this->assertSame('local', $static1); + $this->assertSame('file3', $static2); + $this->assertSame('profile:local', $dynamic1); + $this->assertSame('local', $dynamic2); + $this->assertSame('file3', $dynamic3); + $this->assertSame('profile:${VAR_STATIC1}', $dynamicFail); + } + public function testLoadEnv() { $resetContext = static function (): void { @@ -350,6 +397,38 @@ public function testLoadEnv() rmdir($tmpdir); } + public function testLoadEnvDynamicVar() + { + unset($_ENV['VAR_STATIC1']); + unset($_ENV['VAR_DYNAMIC1']); + unset($_ENV['VAR_DYNAMIC_FAIL']); + unset($_SERVER['VAR_STATIC1']); + unset($_SERVER['VAR_DYNAMIC1']); + unset($_SERVER['VAR_DYNAMIC_FAIL']); + + @mkdir($tmpdir = sys_get_temp_dir().'/dotenv'); + + $pathEnv = tempnam($tmpdir, 'sf-'); + $pathLocalEnv = "$pathEnv.local"; + + file_put_contents($pathEnv, "VAR_STATIC1=env\nVAR_DYNAMIC1=profile:\${VAR_STATIC1}\nVAR_DYNAMIC_FAIL=\$VAR_DYNAMIC1"); + file_put_contents($pathLocalEnv, 'VAR_STATIC1=local'); + + (new Dotenv())->usePutenv()->loadEnv($pathEnv, 'TEST_APP_ENV'); + + $static1 = getenv('VAR_STATIC1'); + $dynamic1 = getenv('VAR_DYNAMIC1'); + $dynamicFail = getenv('VAR_DYNAMIC_FAIL'); + + unlink($pathEnv); + unlink($pathLocalEnv); + rmdir($tmpdir); + + $this->assertSame('local', $static1); + $this->assertSame('profile:local', $dynamic1); + $this->assertSame('profile:${VAR_STATIC1}', $dynamicFail); + } + public function testOverload() { unset($_ENV['FOO']); @@ -385,6 +464,53 @@ public function testOverload() $this->assertSame('BAZ', $bar); } + public function testOverloadDynamicVar() + { + unset($_ENV['VAR_STATIC1']); + unset($_ENV['VAR_STATIC2']); + unset($_ENV['VAR_DYNAMIC1']); + unset($_ENV['VAR_DYNAMIC2']); + unset($_ENV['VAR_DYNAMIC3']); + unset($_ENV['VAR_DYNAMIC_FAIL']); + unset($_SERVER['VAR_STATIC1']); + unset($_SERVER['VAR_STATIC2']); + unset($_SERVER['VAR_DYNAMIC1']); + unset($_SERVER['VAR_DYNAMIC2']); + unset($_SERVER['VAR_DYNAMIC3']); + unset($_SERVER['VAR_DYNAMIC_FAIL']); + + @mkdir($tmpdir = sys_get_temp_dir().'/dotenv'); + + $path1 = tempnam($tmpdir, 'sf-'); + $path2 = tempnam($tmpdir, 'sf-'); + $path3 = tempnam($tmpdir, 'sf-'); + + file_put_contents($path1, "VAR_STATIC1=env\nVAR_DYNAMIC1=profile:\${VAR_STATIC1}\nVAR_DYNAMIC_FAIL=\$VAR_DYNAMIC1"); + file_put_contents($path2, 'VAR_STATIC1=local'); + file_put_contents($path3, "VAR_STATIC2=file3\nVAR_DYNAMIC2=\$VAR_STATIC1\nVAR_DYNAMIC3=\$VAR_STATIC2"); + + (new Dotenv())->usePutenv()->overload($path1, $path2, $path3); + + $static1 = getenv('VAR_STATIC1'); + $static2 = getenv('VAR_STATIC2'); + $dynamic1 = getenv('VAR_DYNAMIC1'); + $dynamic2 = getenv('VAR_DYNAMIC2'); + $dynamic3 = getenv('VAR_DYNAMIC3'); + $dynamicFail = getenv('VAR_DYNAMIC_FAIL'); + + unlink($path1); + unlink($path2); + unlink($path3); + rmdir($tmpdir); + + $this->assertSame('local', $static1); + $this->assertSame('file3', $static2); + $this->assertSame('profile:local', $dynamic1); + $this->assertSame('local', $dynamic2); + $this->assertSame('file3', $dynamic3); + $this->assertSame('profile:${VAR_STATIC1}', $dynamicFail); + } + public function testLoadDirectory() { $this->expectException(PathException::class);