From 88178cb08d308ac84bb91c6d2307029526c79c14 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Thu, 9 Jun 2022 07:00:03 +0900 Subject: [PATCH] Rename class ReadEnvFile to DotEnv and add test cases previous named Get\ReadEnvFile is no Get\DotEnv, static method is the same. Update for not parsing comments at the end of a line if the line was not in quotes. Strips everything after comment mark and also right trims any trailing spaces Old: FOO=Test # Comment -> $_ENV['FOO'] = "Test # Comment" New: FOO=Test # Comment -> $_ENV['FOO'] = "Test" Add phpUnit tests for DotEnv class. Update config.php with new class name The old class name exists and is markted as deprecated until next major release --- 4dev/tests/CoreLibsGetDotEnvTest.php | 162 ++++++++++++++++++++++ 4dev/tests/dotenv/.env | 1 + 4dev/tests/dotenv/cannot_read.env | 0 4dev/tests/dotenv/empty.env | 0 4dev/tests/dotenv/test.env | 49 +++++++ www/admin/class_test.readenvfile.php | 3 + www/admin/test.env | 1 + www/configs/config.php | 2 +- www/lib/CoreLibs/Get/DotEnv.php | 106 ++++++++++++++ www/lib/CoreLibs/Get/ReadEnvFile.php | 67 ++------- www/vendor/composer/autoload_classmap.php | 1 + www/vendor/composer/autoload_static.php | 1 + 12 files changed, 334 insertions(+), 59 deletions(-) create mode 100644 4dev/tests/CoreLibsGetDotEnvTest.php create mode 120000 4dev/tests/dotenv/.env create mode 100644 4dev/tests/dotenv/cannot_read.env create mode 100644 4dev/tests/dotenv/empty.env create mode 100644 4dev/tests/dotenv/test.env create mode 120000 www/admin/test.env create mode 100644 www/lib/CoreLibs/Get/DotEnv.php diff --git a/4dev/tests/CoreLibsGetDotEnvTest.php b/4dev/tests/CoreLibsGetDotEnvTest.php new file mode 100644 index 00000000..285e19ef --- /dev/null +++ b/4dev/tests/CoreLibsGetDotEnvTest.php @@ -0,0 +1,162 @@ + 'A', + 'OTHER' => 'B IS B', + 'Complex' => 'A B \"D is F', + 'HAS_SPACE' => 'ABC', + 'HAS_COMMENT_QUOTES_SPACE' => 'Comment at end with quotes and space', + 'HAS_COMMENT_QUOTES_NO_SPACE' => 'Comment at end with quotes no space', + 'HAS_COMMENT_NO_QUOTES_SPACE' => 'Comment at end no quotes and space', + 'HAS_COMMENT_NO_QUOTES_NO_SPACE' => 'Comment at end no quotes no space', + 'COMMENT_IN_TEXT_QUOTES' => 'Foo bar # comment in here', + 'FAILURE' => 'ABC', + 'SIMPLEBOX' => 'A B C', + 'TITLE' => '1', + 'FOO' => '1.2', + 'SOME.TEST' => 'Test Var', + 'SOME.LIVE' => 'Live Var', + 'A_TEST1' => 'foo', + 'A_TEST2' => '${TEST1:-bar}', + 'A_TEST3' => '${TEST4:-bar}', + 'A_TEST5' => 'null', + 'A_TEST6' => '${TEST5-bar}', + 'A_TEST7' => '${TEST6:-bar}', + 'B_TEST1' => 'foo', + 'B_TEST2' => '${TEST1:=bar}', + 'B_TEST3' => '${TEST4:=bar}', + 'B_TEST5' => 'null', + 'B_TEST6' => '${TEST5=bar}', + 'B_TEST7' => '${TEST6=bar}', + 'Test' => 'A', + 'TEST' => 'B', + 'LINE' => "ABC\nDEF", + 'OTHERLINE' => "ABC\nAF\"ASFASDF\nMORESHIT", + 'SUPERLINE' => '', + '__FOO_BAR_1' => 'b', + '__FOOFOO' => 'f ', + 123123 => 'number', + 'EMPTY' => '', + ]; + // 0: folder relative to test folder, if unset __DIR__ + // 1: file, if unset .env + // 2: status to be returned + // 3: _ENV file content to be set + // 4: override chmod as octect in string + return [ + 'default' => [ + 'folder' => null, + 'file' => null, + 'status' => 3, + 'content' => [], + 'chmod' => null, + ], + 'cannot open file' => [ + 'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv', + 'file' => 'cannot_read.env', + 'status' => 2, + 'content' => [], + 'chmod' => '000', + ], + 'empty file' => [ + 'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv', + 'file' => 'empty.env', + 'status' => 1, + 'content' => [], + 'chmod' => null, + ], + 'override all' => [ + 'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv', + 'file' => 'test.env', + 'status' => 0, + 'content' => $dot_env_content, + 'chmod' => null, + ], + 'override directory' => [ + 'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv', + 'file' => null, + 'status' => 0, + 'content' => $dot_env_content, + 'chmod' => null, + ], + ]; + } + + /** + * test read .env file + * + * @covers ::readEnvFile + * @dataProvider envFileProvider + * @testdox Read _ENV file from $folder / $file with expected status: $expected_status and chmod $chmod [$_dataName] + * + * @param string|null $folder + * @param string|null $file + * @param int $expected_status + * @param array $expected_env + * @param string|null $chmod + * @return void + */ + public function testReadEnvFile( + ?string $folder, + ?string $file, + int $expected_status, + array $expected_env, + ?string $chmod + ): void { + // if we have file + chmod set + $old_chmod = null; + if ( + is_file($folder . DIRECTORY_SEPARATOR . $file) && + !empty($chmod) + ) { + // get the old permissions + $old_chmod = fileperms($folder . DIRECTORY_SEPARATOR . $file); + chmod($folder . DIRECTORY_SEPARATOR . $file, octdec($chmod)); + } + if ($folder !== null && $file !== null) { + $status = DotEnv::readEnvFile($folder, $file); + } elseif ($folder !== null) { + $status = DotEnv::readEnvFile($folder); + } else { + $status = DotEnv::readEnvFile(); + } + $this->assertEquals( + $status, + $expected_status, + 'Assert returned status equal' + ); + // now assert read data + $this->assertEquals( + $_ENV, + $expected_env, + 'Assert _ENV correct' + ); + // if we have file and chmod unset + if ($old_chmod !== null) { + chmod($folder . DIRECTORY_SEPARATOR . $file, $old_chmod); + } + } +} + +// __END__ diff --git a/4dev/tests/dotenv/.env b/4dev/tests/dotenv/.env new file mode 120000 index 00000000..a67af1f7 --- /dev/null +++ b/4dev/tests/dotenv/.env @@ -0,0 +1 @@ +test.env \ No newline at end of file diff --git a/4dev/tests/dotenv/cannot_read.env b/4dev/tests/dotenv/cannot_read.env new file mode 100644 index 00000000..e69de29b diff --git a/4dev/tests/dotenv/empty.env b/4dev/tests/dotenv/empty.env new file mode 100644 index 00000000..e69de29b diff --git a/4dev/tests/dotenv/test.env b/4dev/tests/dotenv/test.env new file mode 100644 index 00000000..46d34565 --- /dev/null +++ b/4dev/tests/dotenv/test.env @@ -0,0 +1,49 @@ +# enviroment file +SOMETHING=A +OTHER="B IS B" +Complex="A B \"D is F" +# COMMENT +HAS_SPACE= "ABC"; +# COMMENT AT END +HAS_COMMENT_QUOTES_SPACE="Comment at end with quotes and space" # Comment QE +HAS_COMMENT_QUOTES_NO_SPACE="Comment at end with quotes no space"# Comment QES +HAS_COMMENT_NO_QUOTES_SPACE=Comment at end no quotes and space # Comment NQE +HAS_COMMENT_NO_QUOTES_NO_SPACE=Comment at end no quotes no space# Comment NQES +COMMENT_IN_TEXT_QUOTES="Foo bar # comment in here" +FAILURE = ABC +SIMPLEBOX= A B C +TITLE=1 +FOO=1.2 +SOME.TEST=Test Var +SOME.LIVE=Live Var +# VAR TESTS - +A_TEST1 = foo +A_TEST2 = ${TEST1:-bar} # TEST1 is set so the value of TEST2 = foo +A_TEST3 = ${TEST4:-bar} # TEST4 is not set so the value of TEST3 = bar +A_TEST5 = null +A_TEST6 = ${TEST5-bar} # TEST5 is set but empty so the value of TEST6 = null +A_TEST7 = ${TEST6:-bar} # TEST5 is set and empty so the value of TEST7 = bar +# VAR TESTS = +B_TEST1 = foo +B_TEST2 = ${TEST1:=bar} # TEST1 is set so the value of TEST2 = foo +B_TEST3 = ${TEST4:=bar} # TEST4 is not set so the value of TEST3 = bar and TEST4 = bar +B_TEST5 = null +B_TEST6 = ${TEST5=bar} # TEST5 is set but emtpy so the value of TEST6 = null +B_TEST7 = ${TEST6=bar} # TEST5 is set and empty so the value of TEST7 = bar and TEST5 = bar +# VAR TEST END +Test="A" +TEST="B" +TEST="D" +LINE="ABC +DEF" +OTHERLINE="ABC +AF\"ASFASDF +MORESHIT" +SUPERLINE= +"asfasfasf" + __FOO_BAR_1 = b + __FOOFOO = f +123123=number +EMPTY= += flase +asfasdf diff --git a/www/admin/class_test.readenvfile.php b/www/admin/class_test.readenvfile.php index ee5b277e..c60ed033 100644 --- a/www/admin/class_test.readenvfile.php +++ b/www/admin/class_test.readenvfile.php @@ -40,6 +40,9 @@ print '

' . $PAGE_NAME . '

'; print "ALREADY from config.php: " . \CoreLibs\Debug\Support::printAr($_ENV) . "
"; // test .env in local +$status = \CoreLibs\Get\DotEnv::readEnvFile('.', 'test.env'); +print "test.env: STATUS: " . $status . "
"; +print "AFTER reading test.env file: " . \CoreLibs\Debug\Support::printAr($_ENV) . "
"; // error message print $log->printErrorMsg(); diff --git a/www/admin/test.env b/www/admin/test.env new file mode 120000 index 00000000..4988714a --- /dev/null +++ b/www/admin/test.env @@ -0,0 +1 @@ +../../4dev/tests/dotenv/test.env \ No newline at end of file diff --git a/www/configs/config.php b/www/configs/config.php index c0e08ed6..df7f3b95 100644 --- a/www/configs/config.php +++ b/www/configs/config.php @@ -50,7 +50,7 @@ for ( is_file($__DIR__PATH . $CONFIG_PATH_PREFIX . CONFIG_PATH . 'config.master.php') ) { // load enviorment file if it exists - \CoreLibs\Get\ReadEnvFile::readEnvFile( + \CoreLibs\Get\DotEnv::readEnvFile( $__DIR__PATH . $CONFIG_PATH_PREFIX . CONFIG_PATH ); // load master config file that loads all other config files diff --git a/www/lib/CoreLibs/Get/DotEnv.php b/www/lib/CoreLibs/Get/DotEnv.php new file mode 100644 index 00000000..8572b141 --- /dev/null +++ b/www/lib/CoreLibs/Get/DotEnv.php @@ -0,0 +1,106 @@ + abort + if (!is_file($env_file_target)) { + $status = 3; + return $status; + } + // cannot open file -> abort + if (!is_readable($env_file_target)) { + $status = 2; + return $status; + } + // open file + if (($fp = fopen($env_file_target, 'r')) === false) { + $status = 2; + return $status; + } + // set to readable but not yet any data loaded + $status = 1; + $block = false; + $var = ''; + while ($line = fgets($fp)) { + // main match for variable = value part + if (preg_match("/^\s*([\w_.]+)\s*=\s*((\"?).*)/", $line, $matches)) { + $var = $matches[1]; + $value = $matches[2]; + $quotes = $matches[3]; + // write only if env is not set yet, and write only the first time + if (empty($_ENV[$var])) { + if (!empty($quotes)) { + // match greedy for first to last so we move any " if there are + if (preg_match('/^"(.*[^\\\])"/U', $value, $matches)) { + $value = $matches[1]; + } else { + // this is a multi line + $block = true; + // first " in string remove + // add removed new line back because this is a multi line + $value = ltrim($value, '"') . PHP_EOL; + } + } else { + // strip any quotes at end for unquoted single line + // an right hand spaces are removed too + $value = false !== ($pos = strpos($value, self::COMMENT_CHAR)) ? + rtrim(substr($value, 0, $pos)) : $value; + } + // if block is set, we strip line of slashes + $_ENV[$var] = $block === true ? stripslashes($value) : $value; + // set successful load + $status = 0; + } + } elseif ($block === true) { + // read line until there is a unescaped " + // this also strips everything after the last " + if (preg_match("/(.*[^\\\])\"/", $line, $matches)) { + $block = false; + // strip ending " and EVERYTHING that follows after that + $line = $matches[1]; + } + // strip line of slashes + $_ENV[$var] .= stripslashes($line); + } + } + fclose($fp); + return $status; + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Get/ReadEnvFile.php b/www/lib/CoreLibs/Get/ReadEnvFile.php index 69a5881e..81eecfe5 100644 --- a/www/lib/CoreLibs/Get/ReadEnvFile.php +++ b/www/lib/CoreLibs/Get/ReadEnvFile.php @@ -4,8 +4,14 @@ declare(strict_types=1); namespace CoreLibs\Get; +/** + * @deprecated use \CoreLibs\Get\DotEnv instead + */ class ReadEnvFile { + /** @var string constant comment char, set to # */ + private const COMMENT_CHAR = '#'; + /** * parses .env file * @@ -24,71 +30,16 @@ class ReadEnvFile * @return int -1 other error * 0 for success full load * 1 for file loadable, but no data inside - * 2 for file not readable + * 2 for file not readable or open failed * 3 for file not found + * @deprecated Use \CoreLibs\Get\DotEnv::readEnvFile() instead */ public static function readEnvFile( string $path = __DIR__, string $env_file = '.env' ): int { - // default -1; - $status = -1; - $env_file_target = $path . DIRECTORY_SEPARATOR . $env_file; - // this is not a file -> abort - if (!is_file($env_file_target)) { - $status = 3; - return $status; - } - // cannot open file -> abort - if (($fp = fopen($env_file_target, 'r')) === false) { - $status = 2; - return $status; - } - // set to readable but not yet any data loaded - $status = 1; - $block = false; - $var = ''; - while ($line = fgets($fp)) { - // main match for variable = value part - if (preg_match("/^\s*([\w_.]+)\s*=\s*((\"?).*)/", $line, $matches)) { - $var = $matches[1]; - $value = $matches[2]; - $quotes = $matches[3]; - // write only if env is not set yet, and write only the first time - if (empty($_ENV[$var])) { - if (!empty($quotes)) { - // match greedy for first to last so we move any " if there are - if (preg_match('/^"(.*[^\\\])"/U', $value, $matches)) { - $value = $matches[1]; - } else { - // this is a multi line - $block = true; - // first " in string remove - // add removed new line back because this is a multi line - $value = ltrim($value, '"') . PHP_EOL; - } - } - // if block is set, we strip line of slashes - $_ENV[$var] = $block === true ? stripslashes($value) : $value; - // set successful load - $status = 0; - } - } elseif ($block === true) { - // read line until there is a unescaped " - // this also strips everything after the last " - if (preg_match("/(.*[^\\\])\"/", $line, $matches)) { - $block = false; - // strip ending " and EVERYTHING that follows after that - $line = $matches[1]; - } - // strip line of slashes - $_ENV[$var] .= stripslashes($line); - } - } - fclose($fp); - return $status; + return \CoreLibs\Get\DotEnv::readEnvFile($path, $env_file); } } - // __END__ diff --git a/www/vendor/composer/autoload_classmap.php b/www/vendor/composer/autoload_classmap.php index 91a57e0c..c7f04f9e 100644 --- a/www/vendor/composer/autoload_classmap.php +++ b/www/vendor/composer/autoload_classmap.php @@ -40,6 +40,7 @@ return array( 'CoreLibs\\Debug\\MemoryUsage' => $baseDir . '/lib/CoreLibs/Debug/MemoryUsage.php', 'CoreLibs\\Debug\\RunningTime' => $baseDir . '/lib/CoreLibs/Debug/RunningTime.php', 'CoreLibs\\Debug\\Support' => $baseDir . '/lib/CoreLibs/Debug/Support.php', + 'CoreLibs\\Get\\DotEnv' => $baseDir . '/lib/CoreLibs/Get/DotEnv.php', 'CoreLibs\\Get\\ReadEnvFile' => $baseDir . '/lib/CoreLibs/Get/ReadEnvFile.php', 'CoreLibs\\Get\\System' => $baseDir . '/lib/CoreLibs/Get/System.php', 'CoreLibs\\Language\\Core\\CachedFileReader' => $baseDir . '/lib/CoreLibs/Language/Core/CachedFileReader.php', diff --git a/www/vendor/composer/autoload_static.php b/www/vendor/composer/autoload_static.php index 9ec986ca..d9cb4fc2 100644 --- a/www/vendor/composer/autoload_static.php +++ b/www/vendor/composer/autoload_static.php @@ -105,6 +105,7 @@ class ComposerStaticInit10fe8fe2ec4017b8644d2b64bcf398b9 'CoreLibs\\Debug\\MemoryUsage' => __DIR__ . '/../..' . '/lib/CoreLibs/Debug/MemoryUsage.php', 'CoreLibs\\Debug\\RunningTime' => __DIR__ . '/../..' . '/lib/CoreLibs/Debug/RunningTime.php', 'CoreLibs\\Debug\\Support' => __DIR__ . '/../..' . '/lib/CoreLibs/Debug/Support.php', + 'CoreLibs\\Get\\DotEnv' => __DIR__ . '/../..' . '/lib/CoreLibs/Get/DotEnv.php', 'CoreLibs\\Get\\ReadEnvFile' => __DIR__ . '/../..' . '/lib/CoreLibs/Get/ReadEnvFile.php', 'CoreLibs\\Get\\System' => __DIR__ . '/../..' . '/lib/CoreLibs/Get/System.php', 'CoreLibs\\Language\\Core\\CachedFileReader' => __DIR__ . '/../..' . '/lib/CoreLibs/Language/Core/CachedFileReader.php',