diff --git a/FAQ.md b/FAQ.md index e74dfc0b..59f89ade 100644 --- a/FAQ.md +++ b/FAQ.md @@ -582,6 +582,31 @@ https://watchstate.example.org { --- +### WS_API_AUTO + +The purpose of this environment variable is to automate the configuration process. It's mainly used for people who uses many browsers +to access the `WebUI` and want to automate the configuration process. as it's requires the API settings to be configured before it +can be used. This environment variable can be enabled by setting `WS_API_AUTO=true` in `${WS_DATA_PATH}/config/.env`. + +#### Why you should use it? + +You normally should not use it, as it's a **GREAT SECURITY RISK**. However, if you are using the tool in a secure environment +and not worried about exposing your API key, you can use it to automate the configuration process. + +#### Why you should not use it? + +Because, by exposing your API key, you are also exposing every data you have in the tool. This is a **GREAT SECURITY RISK**, +any person or bot that are able to access the `WebUI` will also be able to visit `/v1/api/system/auto` and get your API key. And with this key +they can do anything they want with your data. including viewing your media servers api keys. + +So, please while we have this option available, we strongly recommend not to use it if `WatchState` is exposed to the internet. + +> [!IMPORTANT] +> This environment variable is **GREAT SECURITY RISK**, and we strongly recommend not to use it if `WatchState` is exposed to the internet. +> I cannot stress this enough, please do not use it unless you are in a secure environment. + +--- + ### How to disable the included cache server and use external cache server? Set this environment variable in your `compose.yaml` file `DISABLE_CACHE` with value of `1`. to use external redis server @@ -745,7 +770,7 @@ Once that is done you are ready to compile the `WebUI`. ```bash $ cd frontend -$ yarn install --production --prefer-offline --frozen-lockfile +$ yarn install --production --prefer-offline --frozen-lockfile && yarn run generate ``` There should be a new directory called `exported`, you need to move that folder to the `public` directory. @@ -761,7 +786,7 @@ ws:/opt/app/public$ ls exported index.php ``` -There must be exactly one `index.php` file and one `exported` directory. inside that directory. +There must be exactly one `index.php` file and one `exported` directory. inside that directory, or if you prefer, you can add `WS_WEBUI_PATH` environment variable to point to the `exported` directory. * link the app to the frontend proxy. For caddy, you can use the following configuration. diff --git a/NEWS.md b/NEWS.md index 50d7a997..fe4083be 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,21 @@ # Old Updates +### 2024-05-14 + +We are happy to announce the beta testing of the `WebUI`. To get started on using it you just need to visit the url `http://localhost:8080` We are supposed to +enabled it by default tomorrow, but we decided to give you a head start. We are looking forward to your feedback. If you don't use the `WebUI` then you need to +add the environment variable `WEBUI_ENABLED=0` in your `compose.yaml` file. and restart the container. + +### 2024-05-13 + +In preparation for the beta testing of `WebUI` in two days, we have made little breaking change, we have changed the +environment variable `WS_WEBUI_ENABLED` to just `WEBUI_ENABLED`, We made this change to make sure people don't disable +the `WebUI`by mistake via the environment page in the `WebUI`. The `WebUI` will be enabled by default, in two days from +now, to disable it from now add `WEBUI_ENABLED=false` to your `compose.yaml` file. As this environment variable is +system level, it cannot be set via `.env` file. + +Note: `WS_WEBUI_ENABLED` will be gone in few weeks, However it will still work for now, if `WEBUI_ENABLED` is not set. + ### 2024-05-05 **Edit** - We received requests that people are exposing watchstate externally, and there was concern that having open diff --git a/README.md b/README.md index df1caaa3..5b43b813 100644 --- a/README.md +++ b/README.md @@ -7,29 +7,29 @@ This tool primary goal is to sync your backends play state without relying on third party services, out of the box, this tool support `Jellyfin`, `Plex` and `Emby` media servers. -## updates +## Updates -### 2024-05-14 +### 2024-06-23 -We are happy to announce the beta testing of the `WebUI`. To get started on using it you just need to visit the url `http://localhost:8080` We are supposed to -enabled it by default tomorrow, but we decided to give you a head start. We are looking forward to your feedback. If you don't use the `WebUI` then you need to -add the environment variable `WEBUI_ENABLED=0` in your `compose.yaml` file. and restart the container. +WE are happy to announce that the `WebUI` is ready for wider usage and we are planning to release it in the next few months. +We are actively working on it to improve it. If you have any feedback or suggestions, please let us know. We feel it's almost future complete +for the things that we want. -### 2024-05-13 +On another related news, we have added new environment variable `WS_API_AUTO` "disabled by default" which can be used +to automatically expose your **API KEY/TOKEN**. This is useful for users who are using the `WebUI` from many different browsers +and want to automate the configuration process. -In preparation for the beta testing of `WebUI` in two days, we have made little breaking change, we have changed the -environment variable `WS_WEBUI_ENABLED` to just `WEBUI_ENABLED`, We made this change to make sure people don't disable -the `WebUI`by mistake via the environment page in the `WebUI`. The `WebUI` will be enabled by default, in two days from -now, to disable it from now add `WEBUI_ENABLED=false` to your `compose.yaml` file. As this environment variable is -system level, it cannot be set via `.env` file. +While the `WebUI` is included in the main project, it's a standalone feature and requires the API settings to be configured before it +can be used. This environment variable can be enabled by setting `WS_API_AUTO=true` in `${WS_DATA_PATH}/config/.env`. -Note: `WS_WEBUI_ENABLED` will be gone in few weeks, However it will still work for now, if `WEBUI_ENABLED` is not set. +> [!IMPORTANT] +> This environment variable is **GREAT SECURITY RISK**, and we strongly recommend not to use it if `WatchState` is exposed to the internet. Refer to [NEWS](NEWS.md) for old updates. # Features -* **NEW** WebUI. (Preview). +* WebUI. * Sync backends play state (from many to many). * Backup your backends play state into `portable` format. * Receive Webhook events from media backends. diff --git a/composer.json b/composer.json index 40ca0620..0e008661 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 3b57b8e6..b3d2a012 100644 --- a/composer.lock +++ b/composer.lock @@ -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" diff --git a/config/config.php b/config/config.php index 4a23b1e9..ad10b744 100644 --- a/config/config.php +++ b/config/config.php @@ -34,6 +34,7 @@ 'prefix' => '/v1/api', 'key' => env('WS_API_KEY', null), 'secure' => (bool)env('WS_SECURE_API_ENDPOINTS', false), + 'auto' => (bool)env('WS_API_AUTO', false), 'pattern_match' => [ 'backend' => '[a-zA-Z0-9_-]+', ], diff --git a/config/env.spec.php b/config/env.spec.php index 3aacfb2e..b3319a9c 100644 --- a/config/env.spec.php +++ b/config/env.spec.php @@ -150,6 +150,12 @@ 'description' => 'Expose debug information in the API when an error occurs.', 'type' => 'bool', ], + [ + 'key' => 'WS_API_AUTO', + 'description' => 'PUBLICLY EXPOSE the api token for automated WebUI configuration. This should NEVER be enabled if WatchState is exposed to the internet.', + 'danger' => true, + 'type' => 'bool', + ], ]; $validateCronExpression = function (string $value): string { diff --git a/frontend/components/Markdown.vue b/frontend/components/Markdown.vue index 14f7a781..4fb0f941 100644 --- a/frontend/components/Markdown.vue +++ b/frontend/components/Markdown.vue @@ -26,7 +26,7 @@ onMounted(() => fetch(`${api_url.value}${props.file}`).then(response => response text = text.replace(/\[!IMPORTANT\]/g, ` - + Important `) diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index b25f47ad..3b69cd7d 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -252,13 +252,38 @@ + +
+ +

+ It's possible to automatically setup the API connection for this client and ALL VISITORS by setting the following environment variable WS_API_AUTO=true + in /config/.env file. Understand that this option PUBLICLY + EXPOSES YOUR API TOKEN to ALL VISITORS. Anyone who is able to reach this page will be + granted access to your WatchState API which exposes your other media backends data including + their secrets. this option is great security risk and SHOULD NEVER be used if + WatchState is exposed to the internet. +

+ +

Please visit + + + + + This link + + . to learn more, this environment variable is important enough to have its own section entry in the FAQ. +

+
+
- -

+
+

+
@@ -115,11 +117,23 @@
-
-
+
+

- {{ item.key }} + +

@@ -140,6 +154,10 @@ :class="{ 'is-masked': item.mask, 'is-unselectable': item.mask }" @click="(e) => e.target.classList.toggle('is-text-overflow')"> {{ item.value }}

+ +

+ {{ item.description }} +

@@ -181,6 +199,10 @@
  • Some values are too large to fit into the view, clicking on the value will show the full value.
  • These values are loaded from the {{ file }} file.
  • To add a new variable click on the button.
  • +
  • Environment variables with red borders and icon are considered + dangerous. Please be careful when editing them. +
  • @@ -359,6 +381,10 @@ const getHelp = key => { let text = `${data[0].description}` + if (data[0]?.danger) { + text = ` ${text}` + } + if (data[0]?.type) { text += ` Expects: ${data[0].type}` } diff --git a/frontend/pages/tasks.vue b/frontend/pages/tasks.vue index 0272b051..7d703396 100644 --- a/frontend/pages/tasks.vue +++ b/frontend/pages/tasks.vue @@ -55,15 +55,13 @@
    Timer:  - + {{ task.timer }}
    Args:  - + {{ task.args }}
    diff --git a/src/API/System/AutoConfig.php b/src/API/System/AutoConfig.php new file mode 100644 index 00000000..1d5072c7 --- /dev/null +++ b/src/API/System/AutoConfig.php @@ -0,0 +1,33 @@ + $data->get('origin', ag($_SERVER, 'HTTP_ORIGIN', 'localhost')), + 'path' => Config::get('api.prefix'), + 'token' => Config::get('api.key'), + ]); + } +} diff --git a/src/API/System/Env.php b/src/API/System/Env.php index e46ce4bf..1fb02035 100644 --- a/src/API/System/Env.php +++ b/src/API/System/Env.php @@ -127,14 +127,6 @@ public function envUpdate(iRequest $request, array $args = []): iResponse 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); } diff --git a/src/Commands/System/EnvCommand.php b/src/Commands/System/EnvCommand.php index b7608762..ce2701d0 100644 --- a/src/Commands/System/EnvCommand.php +++ b/src/Commands/System/EnvCommand.php @@ -29,13 +29,6 @@ protected function configure(): void { $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 @@ protected function configure(): void [ Environment variables rules ] ------------------------------- - * the key MUST be in CAPITAL LETTERS. For example [WS_CRON_IMPORT]. - * the key MUST start with [WS_]. For example [WS_CRON_EXPORT]. - * 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 [WS_CRON_IMPORT]. + * The key MUST start with [WS_]. For example [WS_CRON_EXPORT]. + * 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 @@ protected function configure(): void # How to load environment variables? - You can load environment variables in many ways. However, the recommended methods are: + For WatchState specific environment variables, we recommend using the WebUI, + to manage the environment variables. However, you can also use this command to manage the environment variables. - (1) Via Docker compose file + We use this file to load your environment variables: - You can load environment variables via [compose.yaml] file by adding them under the [environment] key. - For example, to enable import task, do the following: + - {path}/.env + To load container specific variables i,e, the keys that does not start with WS_ prefix, + you can use the compose.yaml file. + + For example, ------------------------------- services: watchstate: @@ -77,22 +75,48 @@ protected function configure(): void restart: unless-stopped container_name: watchstate environment: - - WS_CRON_IMPORT=1 + - HTTP_PORT=8080 + - DISABLE_CACHE=1 + ....... ------------------------------- - (2) Via .env file + # How to set environment variables? - We automatically look for [.env] in this path [{path}]. 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 key=value per line. For example, to enable import task, edit the [.env] and add + {cmd} {route} -k ENV_NAME -e ENV_VALUE - ------------------------------- - WS_CRON_IMPORT=1 - ------------------------------- + Note: if you are using a space within the value you need to use the long form --set, for example: + + {cmd} {route} -k ENV_NAME --set="ENV VALUE" + + As you can notice the spaced value is wrapped with double "" quotes. + + # How to see all possible environment variables? + + {cmd} {route} --list + + # How to delete environment variable? + + {cmd} {route} -d -k ENV_NAME + + # How to get specific environment variable value? + + {cmd} {route} -k ENV_NAME + + This will show the hidden value if the environment variable marked as sensitive. + + # How to expose the hidden values for secret environment variables? + + You can use the --expose flag to expose the hidden values. for both --list + or just the normal table display. For example: + + {cmd} {route} --expose HELP, [ + 'cmd' => trim(commandContext()), + 'route' => self::ROUTE, 'path' => after(Config::get('path') . '/config', ROOT_PATH), ] ) diff --git a/src/Libs/EnvFile.php b/src/Libs/EnvFile.php index c332ca38..9e6f88f3 100644 --- a/src/Libs/EnvFile.php +++ b/src/Libs/EnvFile.php @@ -25,18 +25,7 @@ public function __construct(public readonly string $file, bool $create = false) } } - if (false === ($data = @file($this->file, FILE_IGNORE_NEW_LINES))) { - $data = []; - } - - foreach ($data as $line) { - $line = trim($line); - if (empty($line) || str_starts_with($line, '#')) { - continue; - } - [$key, $value] = explode('=', $line, 2); - $this->data[$key] = $value; - } + $this->data = parseEnvFile($this->file); } /** diff --git a/src/Libs/Initializer.php b/src/Libs/Initializer.php index 4698d178..5adbbe6f 100644 --- a/src/Libs/Initializer.php +++ b/src/Libs/Initializer.php @@ -27,7 +27,6 @@ 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 @@ public function __construct() (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); } })(); diff --git a/src/Libs/Middlewares/APIKeyRequiredMiddleware.php b/src/Libs/Middlewares/APIKeyRequiredMiddleware.php index 876c7300..ea6b1852 100644 --- a/src/Libs/Middlewares/APIKeyRequiredMiddleware.php +++ b/src/Libs/Middlewares/APIKeyRequiredMiddleware.php @@ -4,6 +4,7 @@ namespace App\Libs\Middlewares; +use App\API\System\AutoConfig; use App\API\System\HealthCheck; use App\Libs\Config; use App\Libs\HTTP_STATUS; @@ -22,6 +23,7 @@ final class APIKeyRequiredMiddleware implements MiddlewareInterface */ private const array PUBLIC_ROUTES = [ HealthCheck::URL, + AutoConfig::URL, ]; /** diff --git a/src/Libs/helpers.php b/src/Libs/helpers.php index e649cbee..1b27923b 100644 --- a/src/Libs/helpers.php +++ b/src/Libs/helpers.php @@ -1506,3 +1506,89 @@ function getEnvSpec(string $env): array return []; } } + + +if (!function_exists('parseEnvFile')) { + /** + * Parse the environment file, and returns key/value pairs. + * + * @param string $file The file to load. + * + * @return array 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; + } + } + } +} diff --git a/tests/Fixtures/test_env_vars b/tests/Fixtures/test_env_vars new file mode 100644 index 00000000..2c485be6 --- /dev/null +++ b/tests/Fixtures/test_env_vars @@ -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 diff --git a/tests/Libs/HelpersTest.php b/tests/Libs/HelpersTest.php index e49e4cdf..1605e7f4 100644 --- a/tests/Libs/HelpersTest.php +++ b/tests/Libs/HelpersTest.php @@ -6,6 +6,7 @@ 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 @@ public function test_isValidURL(): void ); $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.'); + } + } }