diff --git a/src/Parser.php b/src/Parser.php index a37511ab..6dae2180 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -34,6 +34,8 @@ public static function parse($entry) * * @param string $line * + * @throws \Dotenv\Exception\InvalidFileException + * * @return array */ private static function splitStringIntoParts($line) @@ -45,6 +47,12 @@ private static function splitStringIntoParts($line) list($name, $value) = array_map('trim', explode('=', $line, 2)); } + if ($name === '') { + throw new InvalidFileException( + self::getErrorMessage('an unexpected equals', $line) + ); + } + return [$name, $value]; } @@ -53,15 +61,37 @@ private static function splitStringIntoParts($line) * * @param string $name * + * @throws \Dotenv\Exception\InvalidFileException + * * @return string */ private static function sanitiseName($name) { - return trim(str_replace(['export ', '\'', '"'], '', $name)); + $name = trim(str_replace(['export ', '\'', '"'], '', $name)); + + if (!self::isValidName($name)) { + throw new InvalidFileException( + self::getErrorMessage('an invalid name', $name) + ); + } + + return $name; } /** - * Strips quotes from the environment variable value. + * Is the given variable name valid? + * + * @param string $name + * + * @return bool + */ + private static function isValidName($name) + { + return preg_match('~\A[a-zA-Z0-9_.]+\z~', $name) === 1; + } + + /** + * Strips quotes and comments from the environment variable value. * * @param string|null $value * @@ -75,46 +105,77 @@ private static function sanitiseValue($value) return $value; } - if (self::beginsWithAQuote($value)) { // value starts with a quote - $quote = $value[0]; - $regexPattern = sprintf( - '/^ - %1$s # match a quote at the start of the value - ( # capturing sub-pattern used - (?: # we do not need to capture this - [^%1$s\\\\]+ # any character other than a quote or backslash - |\\\\\\\\ # or two backslashes together - |\\\\%1$s # or an escaped quote e.g \" - )* # as many characters that match the previous rules - ) # end of the capturing sub-pattern - %1$s # and the closing quote - .*$ # and discard any string after the closing quote - /mx', - $quote - ); - $value = preg_replace($regexPattern, '$1', $value); - $value = str_replace("\\$quote", $quote, $value); - $value = str_replace('\\\\', '\\', $value); - } else { - $parts = explode(' #', $value, 2); - $value = $parts[0]; - - // Unquoted values cannot contain whitespace - if (preg_match('/\s+/', $value) > 0) { - // Check if value is a comment (usually triggered when empty value with comment) - if (preg_match('/^#/', $value) > 0) { - $value = ''; - } else { - throw new InvalidFileException( - 'Dotenv values containing spaces must be surrounded by quotes.' - ); - } + if (self::beginsWithAQuote($value)) { + return self::processQuotedValue($value); + } + + // Strip comments from the left + $value = explode(' #', $value, 2)[0]; + + // Unquoted values cannot contain whitespace + if (preg_match('/\s+/', $value) > 0) { + // Check if value is a comment (usually triggered when empty value with comment) + if (preg_match('/^#/', $value) > 0) { + $value = ''; + } else { + throw new InvalidFileException( + self::getErrorMessage('an unexpected space', $value) + ); } } return $value; } + /** + * Strips quotes from the environment variable value. + * + * @param string $value + * + * @return string + */ + private static function processQuotedValue($value) + { + $quote = $value[0]; + + $pattern = sprintf( + '/^ + %1$s # match a quote at the start of the value + ( # capturing sub-pattern used + (?: # we do not need to capture this + [^%1$s\\\\]+ # any character other than a quote or backslash + |\\\\\\\\ # or two backslashes together + |\\\\%1$s # or an escaped quote e.g \" + )* # as many characters that match the previous rules + ) # end of the capturing sub-pattern + %1$s # and the closing quote + .*$ # and discard any string after the closing quote + /mx', + $quote + ); + + $value = preg_replace($pattern, '$1', $value); + + return str_replace('\\\\', '\\', str_replace("\\$quote", $quote, $value)); + } + + /** + * Generate a friendly error message. + * + * @param string $cause + * @param string $subject + * + * @return string + */ + private static function getErrorMessage($cause, $subject) + { + return sprintf( + 'Failed to parse dotenv file due to %s. Failed at [%s].', + $cause, + strtok($subject, "\n") + ); + } + /** * Determine if the given string begins with a quote. * diff --git a/tests/Dotenv/DotenvTest.php b/tests/Dotenv/DotenvTest.php index 1a5b816e..7863af42 100644 --- a/tests/Dotenv/DotenvTest.php +++ b/tests/Dotenv/DotenvTest.php @@ -86,16 +86,6 @@ public function testQuotedDotenvLoadsEnvironmentVars() $this->assertSame('test some escaped characters like a quote (") or maybe a backslash (\\)', getenv('QESCAPED')); } - /** - * @expectedException \Dotenv\Exception\InvalidFileException - * @expectedExceptionMessage Dotenv values containing spaces must be surrounded by quotes. - */ - public function testSpacedValuesWithoutQuotesThrowsException() - { - $dotenv = Dotenv::create(dirname(__DIR__).'/fixtures/env-wrong', 'spaced-wrong.env'); - $dotenv->load(); - } - public function testExportedDotenvLoadsEnvironmentVars() { $dotenv = Dotenv::create($this->fixturesFolder, 'exported.env'); diff --git a/tests/Dotenv/ParserTest.php b/tests/Dotenv/ParserTest.php index f5081633..dba2392b 100644 --- a/tests/Dotenv/ParserTest.php +++ b/tests/Dotenv/ParserTest.php @@ -19,4 +19,31 @@ public function testExportParse() { $this->assertSame(['FOO', 'bar baz'], Parser::parse('export FOO="bar baz"')); } + + /** + * @expectedException \Dotenv\Exception\InvalidFileException + * @expectedExceptionMessage Failed to parse dotenv file due to an unexpected space. Failed at [bar baz]. + */ + public function testParseInvalidSpaces() + { + Parser::parse('FOO=bar baz'); + } + + /** + * @expectedException \Dotenv\Exception\InvalidFileException + * @expectedExceptionMessage Failed to parse dotenv file due to an unexpected equals. Failed at [=]. + */ + public function testParseStrayEquals() + { + Parser::parse('='); + } + + /** + * @expectedException \Dotenv\Exception\InvalidFileException + * @expectedExceptionMessage Failed to parse dotenv file due to an invalid name. Failed at [FOO_ASD!]. + */ + public function testParseInvalidName() + { + Parser::parse('FOO_ASD!=BAZ'); + } } diff --git a/tests/fixtures/env-wrong/spaced-wrong.env b/tests/fixtures/env-wrong/spaced-wrong.env deleted file mode 100644 index 5e63f27e..00000000 --- a/tests/fixtures/env-wrong/spaced-wrong.env +++ /dev/null @@ -1 +0,0 @@ -QWFOO=with spaces