Removed symfony/dotenv and rely on our simple env parser, this will allow us to use non quoted strings in .env file. Fixes #506

This commit is contained in:
abdulmohsen
2024-06-24 19:11:15 +03:00
parent 912c41574f
commit 438d8536f2
9 changed files with 228 additions and 134 deletions

View File

@@ -30,7 +30,6 @@
"symfony/yaml": "^6.1.4",
"symfony/process": "^6.1.3",
"symfony/http-client": "^6.1.4",
"symfony/dotenv": "^6.1",
"symfony/lock": "^6.1.3",
"league/container": "^4.2",
"psr/http-client": "^1.0.1",

120
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0ee505d4fae765b6b830886bb694ac15",
"content-hash": "1aac4505fd9a6fa51ceef1181c194d7a",
"packages": [
{
"name": "dragonmantank/cron-expression",
@@ -1602,80 +1602,6 @@
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/dotenv",
"version": "v6.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/dotenv.git",
"reference": "55aefa0029adff89ecffdb560820e945c7983f06"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dotenv/zipball/55aefa0029adff89ecffdb560820e945c7983f06",
"reference": "55aefa0029adff89ecffdb560820e945c7983f06",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"conflict": {
"symfony/console": "<5.4",
"symfony/process": "<5.4"
},
"require-dev": {
"symfony/console": "^5.4|^6.0|^7.0",
"symfony/process": "^5.4|^6.0|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Dotenv\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Registers environment variables from a .env file",
"homepage": "https://symfony.com",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"source": "https://github.com/symfony/dotenv/tree/v6.4.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:49:08+00:00"
},
{
"name": "symfony/http-client",
"version": "v6.4.8",
@@ -2522,16 +2448,16 @@
},
{
"name": "myclabs/deep-copy",
"version": "1.11.1",
"version": "1.12.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
"reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
"reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
"reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
"shasum": ""
},
"require": {
@@ -2539,11 +2465,12 @@
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3,<3.2.2"
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
@@ -2569,7 +2496,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
"source": "https://github.com/myclabs/DeepCopy/tree/1.12.0"
},
"funding": [
{
@@ -2577,7 +2504,7 @@
"type": "tidelift"
}
],
"time": "2023-03-08T13:26:56+00:00"
"time": "2024-06-12T14:39:25+00:00"
},
{
"name": "perftools/php-profiler",
@@ -3187,12 +3114,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "cde5826457b1afd988a50206946cf6512b75ac7c"
"reference": "64eaaecdc0e915ce201f399e4707f83155389e96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/cde5826457b1afd988a50206946cf6512b75ac7c",
"reference": "cde5826457b1afd988a50206946cf6512b75ac7c",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/64eaaecdc0e915ce201f399e4707f83155389e96",
"reference": "64eaaecdc0e915ce201f399e4707f83155389e96",
"shasum": ""
},
"conflict": {
@@ -3201,7 +3128,7 @@
"adodb/adodb-php": "<=5.20.20|>=5.21,<=5.21.3",
"aheinze/cockpit": "<2.2",
"aimeos/ai-client-html": ">=2020.04.1,<2020.10.27|>=2021.04.1,<2021.10.21|>=2022.04.1,<2022.10.12|>=2023.04.1,<2023.10.14|>=2024.04.1,<2024.04.4",
"aimeos/aimeos-core": "<2024.04.7",
"aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7",
"aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5",
"airesvsg/acf-to-rest-api": "<=3.1",
"akaunting/akaunting": "<2.1.13",
@@ -3284,7 +3211,7 @@
"codeigniter4/framework": "<4.4.7",
"codeigniter4/shield": "<1.0.0.0-beta8",
"codiad/codiad": "<=2.8.4",
"composer/composer": "<1.10.27|>=2,<2.2.23|>=2.3,<2.7",
"composer/composer": "<1.10.27|>=2,<2.2.24|>=2.3,<2.7.7",
"concrete5/concrete5": "<9.2.8",
"concrete5/core": "<8.5.8|>=9,<9.1",
"contao-components/mediaelement": ">=2.14.2,<2.21.1",
@@ -3418,7 +3345,7 @@
"gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3",
"gree/jose": "<2.2.1",
"gregwar/rst": "<1.0.3",
"grumpydictator/firefly-iii": "<6.1.7",
"grumpydictator/firefly-iii": "<6.1.17",
"gugoan/economizzer": "<=0.9.0.0-beta1",
"guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5",
"guzzlehttp/psr7": "<1.9.1|>=2,<2.4.5",
@@ -3475,6 +3402,7 @@
"jsdecena/laracom": "<2.0.9",
"jsmitty12/phpwhois": "<5.1",
"juzaweb/cms": "<=3.4",
"jweiland/events2": "<8.3.8|>=9,<9.0.6",
"kazist/phpwhois": "<=4.2.6",
"kelvinmo/simplexrd": "<3.1.1",
"kevinpapst/kimai2": "<1.16.7",
@@ -3512,7 +3440,7 @@
"lms/routes": "<2.1.1",
"localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2",
"luyadev/yii-helpers": "<1.2.1",
"magento/community-edition": "<2.4.3.0-patch3|>=2.4.4,<2.4.5",
"magento/community-edition": "<2.4.5|==2.4.5|>=2.4.5.0-patch1,<2.4.5.0-patch8|==2.4.6|>=2.4.6.0-patch1,<2.4.6.0-patch6|==2.4.7",
"magento/core": "<=1.9.4.5",
"magento/magento1ce": "<1.9.4.3-dev",
"magento/magento1ee": ">=1,<1.14.4.3-dev",
@@ -3545,7 +3473,7 @@
"mojo42/jirafeau": "<4.4",
"mongodb/mongodb": ">=1,<1.9.2",
"monolog/monolog": ">=1.8,<1.12",
"moodle/moodle": "<4.3.4",
"moodle/moodle": "<4.3.5|>=4.4.0.0-beta,<4.4.1",
"mos/cimage": "<0.7.19",
"movim/moxl": ">=0.8,<=0.10",
"movingbytes/social-network": "<=1.2.1",
@@ -3738,7 +3666,7 @@
"slim/slim": "<2.6",
"slub/slub-events": "<3.0.3",
"smarty/smarty": "<4.5.3|>=5,<5.1.1",
"snipe/snipe-it": "<=6.2.2",
"snipe/snipe-it": "<6.4.2",
"socalnick/scn-social-auth": "<1.15.2",
"socialiteproviders/steam": "<1.1",
"spatie/browsershot": "<3.57.4",
@@ -3751,6 +3679,7 @@
"statamic/cms": "<4.46|>=5.3,<5.6.2",
"stormpath/sdk": "<9.9.99",
"studio-42/elfinder": "<2.1.62",
"studiomitte/friendlycaptcha": "<0.1.4",
"subhh/libconnect": "<7.0.8|>=8,<8.1",
"sukohi/surpass": "<1",
"sulu/form-bundle": ">=2,<2.5.3",
@@ -3817,7 +3746,7 @@
"thorsten/phpmyfaq": "<3.2.2",
"tikiwiki/tiki-manager": "<=17.1",
"timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1",
"tinymce/tinymce": "<7",
"tinymce/tinymce": "<7.2",
"tinymighty/wiki-seo": "<1.2.2",
"titon/framework": "<9.9.99",
"tobiasbg/tablepress": "<=2.0.0.0-RC1",
@@ -3880,7 +3809,7 @@
"winter/wn-dusk-plugin": "<2.1",
"winter/wn-system-module": "<1.2.4",
"wintercms/winter": "<=1.2.3",
"woocommerce/woocommerce": "<6.6",
"woocommerce/woocommerce": "<6.6|>=8.8,<8.8.5|>=8.9,<8.9.3",
"wp-cli/wp-cli": ">=0.12,<2.5",
"wp-graphql/wp-graphql": "<=1.14.5",
"wp-premium/gravityforms": "<2.4.21",
@@ -3982,7 +3911,7 @@
"type": "tidelift"
}
],
"time": "2024-06-07T22:04:16+00:00"
"time": "2024-06-21T16:04:36+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -5021,7 +4950,8 @@
"ext-curl": "*",
"ext-sodium": "*",
"ext-simplexml": "*",
"ext-fileinfo": "*"
"ext-fileinfo": "*",
"ext-redis": "*"
},
"platform-dev": [],
"plugin-api-version": "2.6.0"

View File

@@ -55,15 +55,13 @@
</div>
<div class="column is-6 has-text-left">
<strong class="is-hidden-mobile">Timer:&nbsp;</strong>
<NuxtLink class="has-tooltip"
:to='makeEnvLink(`WS_CRON_${task.name.toUpperCase()}_AT`,`"${task.timer}"`)'>
<NuxtLink class="has-tooltip" :to='makeEnvLink(`WS_CRON_${task.name.toUpperCase()}_AT`, task.timer)'>
{{ task.timer }}
</NuxtLink>
</div>
<div class="column is-6 has-text-right" v-if="task.args">
<strong class="is-hidden-mobile">Args:&nbsp;</strong>
<NuxtLink class="has-tooltip"
:to='makeEnvLink(`WS_CRON_${task.name.toUpperCase()}_ARGS`, `"${task.args}"`)'>
<NuxtLink class="has-tooltip" :to='makeEnvLink(`WS_CRON_${task.name.toUpperCase()}_ARGS`, task.args)'>
{{ task.args }}
</NuxtLink>
</div>

View File

@@ -127,14 +127,6 @@ final class Env
try {
$value = $this->setType($spec, $value);
if (true === is_string($value)) {
// -- check if the string contains space but not quoted.
// symfony/dotenv throws an exception if the value contains a space but not quoted.
if (str_contains($value, ' ') && (!str_starts_with($value, '"') || !str_ends_with($value, '"'))) {
throw new ValidationException('The value must be "quoted string", as it contains a space.');
}
}
if (true === ag_exists($spec, 'validate')) {
$value = $spec['validate']($value, $spec);
}

View File

@@ -29,13 +29,6 @@ final class EnvCommand extends Command
{
$this->setName(self::ROUTE)
->setDescription('Show/edit environment variables.')
->addOption(
'envfile',
null,
InputOption::VALUE_REQUIRED,
'Environment file.',
Config::get('path') . '/config/.env'
)
->addOption('key', 'k', InputOption::VALUE_REQUIRED, 'Key to update.')
->addOption('set', 'e', InputOption::VALUE_REQUIRED, 'Value to set.')
->addOption('delete', 'd', InputOption::VALUE_NONE, 'Delete key.')
@@ -51,10 +44,11 @@ final class EnvCommand extends Command
<notice>[ Environment variables rules ]</notice>
-------------------------------
* the key MUST be in CAPITAL LETTERS. For example [<flag>WS_CRON_IMPORT</flag>].
* the key MUST start with [<flag>WS_</flag>]. For example [<flag>WS_CRON_EXPORT</flag>].
* the value is usually simple type, usually string unless otherwise stated.
* the key SHOULD attempt to mirror the key path in default config, If not applicable or otherwise impossible it
* The key MUST be in CAPITAL LETTERS. For example [<flag>WS_CRON_IMPORT</flag>].
* The key MUST start with [<flag>WS_</flag>]. For example [<flag>WS_CRON_EXPORT</flag>].
* The value is simple string. No complex data types are allowed. or shell expansion variables.
* The value MUST be in one line. No multi-line values are allowed.
* The key SHOULD attempt to mirror the key path in default config, If not applicable or otherwise impossible it
should then use an approximate path.
-------
@@ -63,13 +57,17 @@ final class EnvCommand extends Command
<question># How to load environment variables?</question>
You can load environment variables in many ways. However, the recommended methods are:
For <comment>WatchState</comment> specific environment variables, we recommend using the <comment>WebUI</comment>,
to manage the environment variables. However, you can also use this command to manage the environment variables.
<question>(1) Via Docker compose file</>
We use this file to load your environment variables:
You can load environment variables via [<comment>compose.yaml</comment>] file by adding them under the [<comment>environment</comment>] key.
For example, to enable import task, do the following:
- <flag>{path}</flag>/<comment>.env</comment>
To load container specific variables i,e, the keys that does not start with <comment>WS_</comment> prefix,
you can use the <comment>compose.yaml</comment> file.
For example,
-------------------------------
services:
watchstate:
@@ -77,22 +75,48 @@ final class EnvCommand extends Command
restart: unless-stopped
container_name: watchstate
<flag>environment:</flag>
- <flag>WS_CRON_IMPORT</flag>=<value>1</value>
- <flag>HTTP_PORT</flag>=<value>8080</value>
- <flag>DISABLE_CACHE</flag>=<value>1</value>
.......
-------------------------------
<question>(2) Via .env file</question>
<question># How to set environment variables?</question>
We automatically look for [<value>.env</value>] in this path [<value>{path}</value>]. The file usually
does not exist unless you have created it.
To set an environment variable, you can use the following command:
The file format is simple <flag>key</flag>=<value>value</value> per line. For example, to enable import task, edit the [<value>.env</value>] and add
{cmd} <cmd>{route}</cmd> <flag>-k <value>ENV_NAME</value> -e <value>ENV_VALUE</value></flag>
-------------------------------
<flag>WS_CRON_IMPORT</flag>=<value>1</value>
-------------------------------
<notice>Note: if you are using a space within the value you need to use the long form --set, for example:
{cmd} <cmd>{route}</cmd> <flag>-k <value>ENV_NAME</value> --set=<notice>"</notice><value>ENV VALUE</value><notice>"</notice></flag>
As you can notice the spaced value is wrapped with double <value>""</value> quotes.</notice>
<question># How to see all possible environment variables?</question>
{cmd} <cmd>{route}</cmd> <flag>--list</flag>
<question># How to delete environment variable?</question>
{cmd} <cmd>{route}</cmd> <flag>-d -k</flag> <value>ENV_NAME</value>
<question># How to get specific environment variable value?</question>
{cmd} <cmd>{route}</cmd> <flag>-k</flag> <value>ENV_NAME</value>
<notice>This will show the hidden value if the environment variable marked as sensitive.</notice>
<question># How to expose the hidden values for secret environment variables?</question>
You can use the <flag>--expose</flag> flag to expose the hidden values. for both <flag>--list</flag>
or just the normal table display. For example:
{cmd} <cmd>{route}</cmd> <flag>--expose</flag>
HELP,
[
'cmd' => trim(commandContext()),
'route' => self::ROUTE,
'path' => after(Config::get('path') . '/config', ROOT_PATH),
]
)

View File

@@ -27,7 +27,6 @@ use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\Yaml\Yaml;
use Throwable;
@@ -58,13 +57,13 @@ final class Initializer
(function () {
// -- This env file should only be used during development or direct installation.
if (file_exists(__DIR__ . '/../../.env')) {
(new Dotenv())->usePutenv(true)->overload(__DIR__ . '/../../.env');
loadEnvFile(file: __DIR__ . '/../../.env', usePutEnv: true, override: true);
}
// -- This is the official place where users are supposed to store .env file.
$dataPath = env('WS_DATA_PATH', fn() => inContainer() ? '/config' : __DIR__ . '/../../var');
if (file_exists($dataPath . '/config/.env')) {
(new Dotenv())->usePutenv(true)->overload($dataPath . '/config/.env');
loadEnvFile(file: $dataPath . '/config/.env', usePutEnv: true, override: true);
}
})();

View File

@@ -1506,3 +1506,89 @@ if (!function_exists('getEnvSpec')) {
return [];
}
}
if (!function_exists('parseEnvFile')) {
/**
* Parse the environment file, and returns key/value pairs.
*
* @param string $file The file to load.
*
* @return array<string, string> The environment variables.
* @throws InvalidArgumentException Throws an exception if the file does not exist.
*/
function parseEnvFile(string $file): array
{
$env = [];
if (false === file_exists($file)) {
throw new InvalidArgumentException(r("The file '{file}' does not exist.", ['file' => $file]));
}
foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (empty($line)) {
continue;
}
if (true === str_starts_with($line, '#') || false === str_contains($line, '=')) {
continue;
}
[$name, $value] = explode('=', $line, 2);
// -- check if value is quoted.
if ((true === str_starts_with($value, '"') && true === str_ends_with($value, '"')) ||
(true === str_starts_with($value, "'") && true === str_ends_with($value, "'"))) {
$value = substr($value, 1, -1);
}
$value = trim($value);
if ('' === $value) {
continue;
}
$env[$name] = $value;
}
return $env;
}
}
if (!function_exists('loadEnvFile')) {
/**
* Load the environment file.
*
* @param string $file The file to load.
* @param bool $usePutEnv (Optional) Whether to use putenv.
* @param bool $override (Optional) Whether to override existing values.
*
* @return void
*/
function loadEnvFile(string $file, bool $usePutEnv = false, bool $override = true): void
{
try {
$env = parseEnvFile($file);
if (count($env) < 1) {
return;
}
} catch (InvalidArgumentException) {
return;
}
foreach ($env as $name => $value) {
if (false === $override && true === array_key_exists($name, $_ENV)) {
continue;
}
if (true === $usePutEnv) {
putenv("{$name}={$value}");
}
$_ENV[$name] = $value;
if (!str_starts_with($name, 'HTTP_')) {
$_SERVER[$name] = $value;
}
}
}
}

View File

@@ -0,0 +1,12 @@
WS_TZ=Asia/Kuwait
WS_CRON_IMPORT=1
WS_CRON_EXPORT=0
WS_FOO_BAR=" "
WS_CRON_IMPORT_AT=16 */1 * * *
WS_CRON_EXPORT_AT="30 */3 * * *"
WS_CRON_PUSH_AT='*/10 * * * *'
# Commit_line=foo
# Next is empty line
# Intentionally left "=" from the string
FOOBAR_KAZ

View File

@@ -6,6 +6,7 @@ namespace Tests\Libs;
use App\Libs\Config;
use App\Libs\Entity\StateEntity;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\HTTP_STATUS;
use App\Libs\TestCase;
@@ -705,4 +706,57 @@ class HelpersTest extends TestCase
);
$this->assertFalse(isValidURL('example.com'), 'When invalid url is passed, false is returned.');
}
public function test_parseEnvFile(): void
{
$envFile = __DIR__ . '/../Fixtures/test_env_vars';
$parsed = parseEnvFile($envFile);
$correctData = [
"WS_TZ" => "Asia/Kuwait",
"WS_CRON_IMPORT" => "1",
"WS_CRON_EXPORT" => "0",
"WS_CRON_IMPORT_AT" => "16 */1 * * *",
"WS_CRON_EXPORT_AT" => "30 */3 * * *",
"WS_CRON_PUSH_AT" => "*/10 * * * *",
];
$this->assertCount(count($correctData), $parsed, 'When parsing env file, filter out garbage data.');
foreach ($correctData as $key => $value) {
$this->assertSame($value, $parsed[$key], 'Make sure correct values are returned when parsing env file.');
}
$this->expectException(InvalidArgumentException::class);
parseEnvFile(__DIR__ . '/../Fixtures/non_existing_file');
}
public function test_loadEnvFile(): void
{
$envFile = __DIR__ . '/../Fixtures/test_env_vars';
$correctData = [
"WS_TZ" => "Asia/Kuwait",
"WS_CRON_IMPORT" => "1",
"WS_CRON_EXPORT" => "0",
"WS_CRON_IMPORT_AT" => "16 */1 * * *",
"WS_CRON_EXPORT_AT" => "30 */3 * * *",
"WS_CRON_PUSH_AT" => "*/10 * * * *",
];
$_ENV['WS_TZ'] = 'Asia/Kuwait';
putenv('WS_TZ=Asia/Kuwait');
loadEnvFile($envFile, usePutEnv: true, override: false);
foreach ($correctData as $key => $value) {
$this->assertSame($value, env($key), 'Make sure correct values are returned when parsing env file.');
}
// -- if given invalid file. it should not throw exception.
try {
loadEnvFile(__DIR__ . '/../Fixtures/non_existing_file');
} catch (\Throwable) {
$this->fail('This function shouldn\'t throw exception when invalid file is given.');
}
}
}