diff --git a/App/Config/config-with-env.json b/App/Config/config-with-env.json new file mode 100644 index 000000000..f4624fd38 --- /dev/null +++ b/App/Config/config-with-env.json @@ -0,0 +1,69 @@ +{ + "base-url":"DYNAMIC", + "theme":null, + "home-page":"BASE_URL", + "primary-lang":"env:LANG", + "titles":{ + "AR":"افتراضي", + "EN":"Default" + }, + "name-separator":"|", + "scheduler-password":"env:SCHEDULER_PASS", + "app-names":{ + "AR":"تطبيق", + "EN":"Application" + }, + "app-descriptions":{ + "AR":"", + "EN":"" + }, + "version-info":{ + "version":"1.0", + "version-type":"Stable", + "release-date":"2023-10-30" + }, + "env-vars":{ + "WF_VERBOSE_2":{ + "value":"env:VERBOSE", + "description":"Configure the verbosity of error messsages at run-time. This should be set to true in testing and false in production." + }, + "CLI_HTTP_HOST_2":{ + "value":"example.com", + "description":"Host name that will be used when runing the application as command line utility." + }, + "HOST":"env:HOST" + }, + "smtp-connections":{ + "conn00": { + "host":"smtp.outlook.com", + "port":"244", + "username":"env:SMTP_00_USER", + "password":"env:SMTP_00_PASS", + "address":"env:SMTP_00_ADDRESS", + "sender-name":"env:SMTP_00_NAME", + "access-token":"env:SMTP_TOKEN" + } + }, + "database-connections":{ + "New_Connection":{ + "type":"mysql", + "host":"env:DB_HOST_1", + "port":3306, + "username":"cool", + "database":"env:DB_NAME_1", + "password":"env:DB_PASS_1", + "extras":{ + } + }, + "New_Connection_2":{ + "type":"mysql", + "host":"env:DB_HOST_2", + "port":3306, + "database":"env:DB_NAME_2", + "password":"env:DB_PASS_2", + "username":"cool", + "extras":{ + } + } + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fa8d739f..9e24c82e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,70 @@ # Changelog +## [3.0.0-beta.31](https://github.com/WebFiori/framework/compare/v3.0.0-Beta.30...v3.0.0-beta.31) (2025-10-27) + + +### Bug Fixes + +* Bug in Finding Class Loader Path ([6831a96](https://github.com/WebFiori/framework/commit/6831a96d91962480b097561b559e2ecf07af1c0b)) +* Show Framework Logo ([e47f1a5](https://github.com/WebFiori/framework/commit/e47f1a5d70b5d5982ff87247de3feb98ce75d558)) + + +### Miscellaneous Chores + +* Merge pull request [#271](https://github.com/WebFiori/framework/issues/271) from WebFiori/dev ([6bf9c26](https://github.com/WebFiori/framework/commit/6bf9c26ab448e60bcdfee5f6f91b2cdc97304006)) +* Updated Framework Version ([3d719ea](https://github.com/WebFiori/framework/commit/3d719ea32aea00d973dbb61e5ddf98194a5e7e0f)) +* Updated Index ([0b128ec](https://github.com/WebFiori/framework/commit/0b128ec53339304b351df9d1953e192dc233a24a)) + +## [3.0.0-Beta.30](https://github.com/WebFiori/framework/compare/v3.0.0-Beta.29...v3.0.0-Beta.30) (2025-10-21) + + +### Features + +* Added a Script to Update Version ([e998c47](https://github.com/WebFiori/framework/commit/e998c476b2629b6b8307d6ad52813cdc1a63eb05)) + + +### Bug Fixes + +* Metadata ([84facc3](https://github.com/WebFiori/framework/commit/84facc3638ce6989d9b3efcf6dc05f9b7f87944b)) + + +### Miscellaneous Chores + +* Merge pull request [#269](https://github.com/WebFiori/framework/issues/269) from WebFiori/dev ([8ffb523](https://github.com/WebFiori/framework/commit/8ffb523559ab609095da446b2d592607dac1f20e)) + +## [3.0.0-Beta.29](https://github.com/WebFiori/framework/compare/v3.0.0-Beta.28...v3.0.0-Beta.29) (2025-10-09) + + +### Bug Fixes + +* API Creation ([d3c384f](https://github.com/WebFiori/framework/commit/d3c384f48f61c828cf9358cbb1675e26be25b0ee)) +* Command Writer ([338e8d2](https://github.com/WebFiori/framework/commit/338e8d2d7e72e66463bb8d57a6b34f20aa38ff3b)) +* Fix Test Case ([6b26e2f](https://github.com/WebFiori/framework/commit/6b26e2f023cea94eb7fdf2a665f1a721e5e190eb)) +* Fix Test Cases ([1086336](https://github.com/WebFiori/framework/commit/108633622e1938699360b45d65d3565bcf611e85)) +* Fix Test Cases ([0c914de](https://github.com/WebFiori/framework/commit/0c914de97f37eef1bf862c4b37f323470dfcf5ef)) +* Fixes and Tests Refactoring ([5ff31ab](https://github.com/WebFiori/framework/commit/5ff31ab5dc2c33b8a9389f3bf33001a525f57a7d)) +* Getting Arg Value in CLI ([c56b9bf](https://github.com/WebFiori/framework/commit/c56b9bf6f34617336ad1f750943f705d84357ba0)) +* Migrations Command ([ce54748](https://github.com/WebFiori/framework/commit/ce5474803a26352b75eaea66604b630378b98aa9)) +* Namespaces ([e51d354](https://github.com/WebFiori/framework/commit/e51d354a2be0843f55e70ced529c45341988ba2e)) +* References to Classes ([a140646](https://github.com/WebFiori/framework/commit/a1406462197a32a8bd16a6d12168c8c2a4fda016)) +* Tasks Names Check ([3c80893](https://github.com/WebFiori/framework/commit/3c80893d4c793e330f571205a702b853540d9a36)) +* Test Cases ([f78c6d5](https://github.com/WebFiori/framework/commit/f78c6d5a2b9fa10baa98ce9c229295d95f5b0fef)) +* Test Classes ([cd68b49](https://github.com/WebFiori/framework/commit/cd68b49acbc7dab3d8abc25fa3df21b129d19bf4)) +* Theme Creation ([6864c23](https://github.com/WebFiori/framework/commit/6864c236e7174113a4b4dc26e8289ed2645bb278)) +* Theme Resources Creation ([e9f1025](https://github.com/WebFiori/framework/commit/e9f10258f4433982e922cac494dec69b06f34e8b)) +* Writing Classes ([2fcb0c5](https://github.com/WebFiori/framework/commit/2fcb0c5993d7eb47c270d8113f58a2cf13f76046)) + + +### Miscellaneous Chores + +* Merge pull request [#266](https://github.com/WebFiori/framework/issues/266) from WebFiori/dev ([19fc94a](https://github.com/WebFiori/framework/commit/19fc94a9166ecafb2572e0926f9992b93a170341)) +* Merge pull request [#268](https://github.com/WebFiori/framework/issues/268) from WebFiori/dev ([fb1e6a3](https://github.com/WebFiori/framework/commit/fb1e6a3bb3d5b69642641fd323e82785f28c72f9)) +* Updated Database Library ([7f853fd](https://github.com/WebFiori/framework/commit/7f853fd25fd9f0b1211fb18fc807a2436f403946)) +* Updated Database to v1.0.0 ([c35b109](https://github.com/WebFiori/framework/commit/c35b109d292df38b32965ceed0bd7e0a90c8cc08)) +* Updated Dependencies ([ff05d95](https://github.com/WebFiori/framework/commit/ff05d95b8923ee631649c4610bf7dd348dcd4869)) +* Updated HTTP to v4 ([ab525fc](https://github.com/WebFiori/framework/commit/ab525fc9a37db07ae454b53e961cddba628b82e3)) +* Updated Version Number ([c2bac79](https://github.com/WebFiori/framework/commit/c2bac791aa6aca1bd9e8742c017d75e8574fd38d)) + ## [3.0.0-Beta.26](https://github.com/WebFiori/framework/compare/v3.0.0-Beta.26...v3.0.0-Beta.26) (2025-04-07) diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php index 0962a778b..b3f62091b 100644 --- a/WebFiori/Framework/App.php +++ b/WebFiori/Framework/App.php @@ -726,6 +726,7 @@ private static function getRoot() { * @throws Exception */ private static function initAutoLoader() { + Ini::createAppDirs(); /** * Initialize autoloader. */ @@ -744,7 +745,6 @@ private static function initAutoLoader() { } if (!class_exists(APP_DIR.'\\Ini\\AutoLoad')) { - Ini::createAppDirs(); Ini::get()->createIniClass('AutoLoad', 'Add user-defined directories to the set of directories at which the framework will search for classes.'); } self::call(APP_DIR.'\\Ini\\AutoLoad::initialize'); diff --git a/WebFiori/Framework/Config/ClassDriver.php b/WebFiori/Framework/Config/ClassDriver.php index 3c4647836..437d0c793 100644 --- a/WebFiori/Framework/Config/ClassDriver.php +++ b/WebFiori/Framework/Config/ClassDriver.php @@ -15,6 +15,18 @@ * create a class called 'AppConfig' on the directory APP_DIR/Config and * use it to read and write configurations. * + * ## Environment Variable Support + * + * This driver supports environment variable substitution using the 'env:' prefix. + * The following configuration sections support this feature: + * - Database connections (all properties) + * - SMTP connections (all properties) + * - Environment variables (value field) + * - Scheduler password + * + * Values stored in the generated PHP class can use the 'env:' prefix, and they + * will be resolved to their environment variable values when read. + * * @author Ibrahim */ class ClassDriver implements ConfigurationDriver { @@ -60,6 +72,9 @@ public static function a($file, $str, $tabSize = 0) { * a named constant at run time using the function 'define'. This means * the constant will be accesaable anywhere within the appllication's environment. * + * Note: The value parameter supports the 'env:' prefix for referencing + * system environment variables (e.g., "env:MY_VAR"). + * * @param string $name The name of the named constant such as 'MY_CONSTANT'. * * @param mixed $value The value of the constant. @@ -77,6 +92,10 @@ public function addEnvVar(string $name, mixed $value = null, ?string $descriptio /** * Adds new database connections information or update existing connection. * + * Note: When using this driver, connection properties support environment variable + * substitution using the 'env:' prefix. Values will be stored in the generated PHP class + * and resolved when read. + * * @param ConnectionInfo $dbConnectionsInfo An object which holds connection information. */ public function addOrUpdateDBConnection(ConnectionInfo $dbConnectionsInfo) { @@ -86,6 +105,10 @@ public function addOrUpdateDBConnection(ConnectionInfo $dbConnectionsInfo) { /** * Adds new SMTP account or Updates an existing one. * + * Note: When using this driver, SMTP account properties support environment variable + * substitution using the 'env:' prefix. Values will be stored in the generated PHP class + * and resolved when read. + * * @param SMTPAccount $emailAccount An instance of 'SMTPAccount'. */ public function addOrUpdateSMTPAccount(SMTPAccount $emailAccount) { @@ -157,6 +180,9 @@ public function getBaseURL(): string { /** * Returns database connection information given connection name. * + * Note: Connection properties that use the 'env:' prefix will be + * automatically resolved to their environment variable values. + * * @param string $conName The name of the connection. * * @return ConnectionInfo|null The method will return an object of type @@ -174,10 +200,30 @@ public function getDBConnection(string $conName) { /** * Returns an associative array that contain the information of database connections. * + * Note: Connection properties that use the 'env:' prefix will be + * automatically resolved to their environment variable values. + * * @return array An associative array of objects of type ConnectionInfo. */ public function getDBConnections(): array { - return $this->configVars['database-connections']; + $connections = $this->configVars['database-connections']; + + foreach ($connections as $name => $connObj) { + if ($connObj instanceof ConnectionInfo) { + $connObj->setHost(Controller::resolveEnvValue($connObj->getHost())); + $connObj->setUsername(Controller::resolveEnvValue($connObj->getUsername())); + $connObj->setPassword(Controller::resolveEnvValue($connObj->getPassword())); + $connObj->setDBName(Controller::resolveEnvValue($connObj->getDBName())); + + $extras = $connObj->getExtars(); + foreach ($extras as $key => $value) { + $extras[$key] = Controller::resolveEnvValue($value); + } + $connObj->setExtras($extras); + } + } + + return $connections; } public function getDescription(string $langCode) { @@ -198,12 +244,25 @@ public function getDescriptions(): array { /** * Returns an associative array of application constants. * + * Note: Environment variable values that use the 'env:' prefix will be + * automatically resolved to their system environment variable values. + * * @return array The indices of the array are names of the constants and * values are sub-associative arrays. Each sub-array will have two indices, * 'value' and 'description'. */ public function getEnvVars(): array { - return $this->configVars['env-vars']; + $vars = $this->configVars['env-vars']; + + foreach ($vars as $name => $varData) { + if (is_array($varData) && isset($varData['value'])) { + $vars[$name]['value'] = Controller::resolveEnvValue($varData['value']); + } else { + $vars[$name] = Controller::resolveEnvValue($varData); + } + } + + return $vars; } /** * Returns a string that represents the URL of home page of the application. @@ -221,9 +280,17 @@ public function getHomePage() : string { public function getPrimaryLanguage() : string { return $this->configVars['site']['primary-lang']; } - + /** + * Returns sha256 hash of the password which is used to prevent unauthorized + * access to run the tasks or access scheduler web interface. + * + * Note: The scheduler password value supports environment variable substitution + * using the 'env:' prefix (e.g., "env:SCHEDULER_PASS"). + * + * @return string Password hash or the string 'NO_PASSWORD' if there is no password. + */ public function getSchedulerPassword(): string { - return $this->configVars['scheduler-password']; + return Controller::resolveEnvValue($this->configVars['scheduler-password']); } public function getSMTPAccount(string $name) { @@ -242,15 +309,48 @@ public function getSMTPAccount(string $name) { * will return an object of type SMTPAccount. Else, the * method will return null. * + */ /** + * Returns SMTP connection given its name. + * + * Note: SMTP account properties that use the 'env:' prefix will be + * automatically resolved to their environment variable values. + * + * @param string $name The name of the account. + * + * @return SMTPAccount|null If the account is found, The method + * will return an object of type SMTPAccount. Else, the + * method will return null. + * */ public function getSMTPConnection(string $name) { if (isset($this->getSMTPConnections()[$name])) { return $this->getSMTPConnections()[$name]; } } - + /** + * Returns an array that contains all added SMTP accounts. + * + * Note: SMTP account properties that use the 'env:' prefix will be + * automatically resolved to their environment variable values. + * + * @return array An associative array of SMTPAccount objects. + */ public function getSMTPConnections(): array { - return $this->configVars['smtp-connections']; + $connections = $this->configVars['smtp-connections']; + + foreach ($connections as $name => $smtpObj) { + if ($smtpObj instanceof SMTPAccount) { + $smtpObj->setServerAddress(Controller::resolveEnvValue($smtpObj->getServerAddress())); + $smtpObj->setPort(Controller::resolveEnvValue($smtpObj->getPort())); + $smtpObj->setUsername(Controller::resolveEnvValue($smtpObj->getUsername())); + $smtpObj->setPassword(Controller::resolveEnvValue($smtpObj->getPassword())); + $smtpObj->setAddress(Controller::resolveEnvValue($smtpObj->getAddress())); + $smtpObj->setSenderName(Controller::resolveEnvValue($smtpObj->getSenderName())); + $smtpObj->setAccessToken(Controller::resolveEnvValue($smtpObj->getAccessToken())); + } + } + + return $connections; } public function getTheme(): string { diff --git a/WebFiori/Framework/Config/ConfigurationDriver.php b/WebFiori/Framework/Config/ConfigurationDriver.php index 93382a08b..9c68f8229 100644 --- a/WebFiori/Framework/Config/ConfigurationDriver.php +++ b/WebFiori/Framework/Config/ConfigurationDriver.php @@ -20,6 +20,9 @@ interface ConfigurationDriver { * the constant will be accesaable anywhere within the application's environment. * Additionally, it will be added as environment variable using 'putenv()'. * + * Note: The value parameter supports the 'env:' prefix for referencing + * system environment variables (e.g., "env:MY_VAR"). + * * @param string $name The name of the named constant such as 'MY_CONSTANT'. * * @param mixed|null $value The value of the constant. @@ -31,6 +34,9 @@ public function addEnvVar(string $name, mixed $value = null, ?string $descriptio /** * Adds new database connections information or update existing connections. * + * Note: When using JsonDriver, connection properties support environment variable + * substitution using the 'env:' prefix. For example, you can set host to "env:DB_HOST" + * in the JSON configuration file, and it will be resolved at runtime. * * @param ConnectionInfo $dbConnectionsInfo An object which holds connection information. */ @@ -38,6 +44,10 @@ public function addOrUpdateDBConnection(ConnectionInfo $dbConnectionsInfo); /** * Adds new SMTP account or Updates an existing one. * + * Note: When using JsonDriver, SMTP account properties support environment variable + * substitution using the 'env:' prefix. For example, you can set password to "env:SMTP_PASS" + * in the JSON configuration file, and it will be resolved at runtime. + * * @param SMTPAccount $emailAccount An instance of 'SMTPAccount'. */ public function addOrUpdateSMTPAccount(SMTPAccount $emailAccount); @@ -92,6 +102,9 @@ public function getBaseURL() : string; /** * Returns database connection information given connection name. * + * Note: Connection properties that use the 'env:' prefix in configuration + * will be automatically resolved to their environment variable values. + * * @param string $conName The name of the connection. * * @return ConnectionInfo|null The method should return an object of type @@ -107,6 +120,9 @@ public function getDBConnection(string $conName); * The keys of the array should be the name of database connection and the * value of each key should be an object of type ConnectionInfo. * + * Note: Connection properties that use the 'env:' prefix in configuration + * will be automatically resolved to their environment variable values. + * * @return array An associative array. */ public function getDBConnections() : array; @@ -133,6 +149,9 @@ public function getDescriptions() : array; /** * Returns an associative array of application constants. * + * Note: Environment variable values that use the 'env:' prefix in configuration + * will be automatically resolved to their system environment variable values. + * * @return array The indices of the array are names of the constants and * values are sub-associative arrays. Each sub-array must have two indices, * 'value' and 'description'. @@ -158,6 +177,9 @@ public function getPrimaryLanguage() : string; * return the hashed value. If no password is set, this method should return the * string 'NO_PASSWORD'. * + * Note: The scheduler password value supports environment variable substitution + * using the 'env:' prefix (e.g., "env:SCHEDULER_PASS" in configuration). + * * @return string Password hash or the string 'NO_PASSWORD' if there is no * password. */ @@ -165,6 +187,9 @@ public function getSchedulerPassword() : string; /** * Returns SMTP connection given its name. * + * Note: SMTP account properties that use the 'env:' prefix in configuration + * will be automatically resolved to their environment variable values. + * * The method should be implemented in a way that it searches * for an account with the given name in the set * of added accounts. If no account was found, null is returned. @@ -184,6 +209,9 @@ public function getSMTPConnection(string $name); * The indices of the array should act as the names of the accounts, * and the value of the index should be an object of type SMTPAccount. * + * Note: SMTP account properties that use the 'env:' prefix in configuration + * will be automatically resolved to their environment variable values. + * * @return array An associative array that contains all added SMTP accounts. * */ diff --git a/WebFiori/Framework/Config/Controller.php b/WebFiori/Framework/Config/Controller.php index 367a44b5d..9ba8fbb62 100644 --- a/WebFiori/Framework/Config/Controller.php +++ b/WebFiori/Framework/Config/Controller.php @@ -109,6 +109,49 @@ public static function setDriver(ConfigurationDriver $driver) { self::get()->driver = $driver; self::init($driver); } + /** + * Resolves environment variable references in configuration values. + * + * This method enables the use of environment variables in configuration files + * by using the 'env:' prefix. When a configuration value starts with 'env:', + * the method attempts to read the corresponding environment variable. + * + * Example usage in JSON configuration: + * + * { + * "database-connections": { + * "production": { + * "host": "env:DB_HOST", + * "password": "env:DB_PASS" + * } + * } + * } + * + * + * The method will: + * - Check if the value starts with 'env:' + * - Extract the environment variable name (e.g., 'DB_HOST' from 'env:DB_HOST') + * - Attempt to read from getenv() first, then $_ENV + * - Fall back to the original value if the environment variable doesn't exist + * + * @param mixed $value The value to resolve. Can be any type, but only strings + * starting with 'env:' will be processed. + * + * @return mixed The resolved value. Returns the environment variable value if found, + * otherwise returns the original value unchanged. + */ + public static function resolveEnvValue($value) { + if (!is_string($value)) { + return $value; + } + + if (str_starts_with($value, 'env:')) { + $envVar = substr($value, 4); + + return getenv($envVar) ?: ($_ENV[$envVar] ?? $value); + } + return $value; + } /** * Reads application environment variables and updates the class which holds * application environment variables. @@ -117,6 +160,12 @@ public static function setDriver(ConfigurationDriver $driver) { */ public static function updateEnv() { foreach (self::getDriver()->getEnvVars() as $name => $envVar) { + if (is_string($envVar) && str_starts_with($envVar, 'env:')) { + $envVar = self::resolveEnvValue($envVar); + } else if (is_array($envVar) && isset($envVar['value']) && str_starts_with($envVar['value'], 'env:')) { + $envVar['value'] = self::resolveEnvValue($envVar['value']); + } + if (!defined($name)) { if (isset($envVar['value'])) { define($name, $envVar['value']); diff --git a/WebFiori/Framework/Config/JsonDriver.php b/WebFiori/Framework/Config/JsonDriver.php index 831a0594d..272b46d91 100644 --- a/WebFiori/Framework/Config/JsonDriver.php +++ b/WebFiori/Framework/Config/JsonDriver.php @@ -81,6 +81,9 @@ public function __construct() { * a named constant at run time using the function 'define'. This means * the constant will be accessable anywhere within the application's environment. * + * Note: The value parameter supports the 'env:' prefix for referencing + * system environment variables (e.g., "env:MY_VAR"). + * * @param string $name The name of the named constant such as 'MY_CONSTANT'. * * @param mixed $value The value of the constant. @@ -95,6 +98,15 @@ public function addEnvVar(string $name, mixed $value = null, ?string $descriptio ], 'none', 'same')); $this->writeJson(); } + /** + * Adds new database connections information or update existing connection. + * + * Note: When using this driver, connection properties support environment variable + * substitution using the 'env:' prefix. For example, you can set host to "env:DB_HOST" + * in the JSON configuration file, and it will be resolved at runtime. + * + * @param ConnectionInfo $dbConnectionsInfo An object which holds connection information. + */ public function addOrUpdateDBConnection(ConnectionInfo $dbConnectionsInfo) { $connectionJAsJson = new Json([ 'type' => $dbConnectionsInfo->getDatabaseType(), @@ -108,7 +120,15 @@ public function addOrUpdateDBConnection(ConnectionInfo $dbConnectionsInfo) { $this->json->get('database-connections')->add($dbConnectionsInfo->getName(), $connectionJAsJson); $this->writeJson(); } - + /** + * Adds new SMTP account or Updates an existing one. + * + * Note: When using this driver, SMTP account properties support environment variable + * substitution using the 'env:' prefix. For example, you can set password to "env:SMTP_PASS" + * in the JSON configuration file, and it will be resolved at runtime. + * + * @param SMTPAccount $emailAccount An instance of 'SMTPAccount'. + */ public function addOrUpdateSMTPAccount(SMTPAccount $emailAccount) { $connectionAsJson = new Json([ 'host' => $emailAccount->getServerAddress(), @@ -117,7 +137,7 @@ public function addOrUpdateSMTPAccount(SMTPAccount $emailAccount) { 'password' => $emailAccount->getPassword(), 'address' => $emailAccount->getAddress(), 'sender-name' => $emailAccount->getSenderName(), - + 'access-token' => $emailAccount->getAccessToken() ], 'none', 'same'); $this->json->get('smtp-connections')->add($emailAccount->getAccountName(), $connectionAsJson); $this->writeJson(); @@ -190,6 +210,16 @@ public static function getConfigFileName() : string { return self::$configFileName; } + /** + * Returns database connection information given connection name. + * + * Note: Connection properties that use the 'env:' prefix in configuration + * will be automatically resolved to their environment variable values. + * + * @param string $conName The name of the connection. + * + * @return ConnectionInfo|null Returns connection info if found, null otherwise. + */ public function getDBConnection(string $conName) { $jsonObj = $this->json->get('database-connections')->get($conName); @@ -206,18 +236,21 @@ public function getDBConnection(string $conName) { } return new ConnectionInfo( - $jsonObj->get('type'), - $jsonObj->get('username'), - $jsonObj->get('password'), - $jsonObj->get('database'), - $jsonObj->get('host'), - $jsonObj->get('port'), + $this->getProp($jsonObj, 'type', $conName), + $this->getProp($jsonObj, 'username', $conName), + $this->getProp($jsonObj, 'password', $conName), + $this->getProp($jsonObj, 'database', $conName), + $this->getProp($jsonObj, 'host', $conName), + $this->getProp($jsonObj, 'port', $conName), $extrasArr); } } /** * Returns an associative array that contain the information of database connections. * + * Note: Connection properties that use the 'env:' prefix in configuration + * will be automatically resolved to their environment variable values. + * * @return array An associative array. The indices are connections names and * values are objects of type 'ConnectionInfo'. */ @@ -281,6 +314,9 @@ public function getDescriptions(): array { * Returns an array that holds the information of defined application environment * variables. * + * Note: Environment variable values that use the 'env:' prefix in configuration + * will be automatically resolved to their system environment variable values. + * * @return array The returned array will be associative. The key will represent * the name of the variable and its value is a sub-associative array with * two indices, 'description' and 'value'. The description index is a text that describes @@ -291,9 +327,19 @@ public function getEnvVars(): array { $vars = $this->json->get('env-vars'); foreach ($vars->getPropsNames() as $name) { + $value = ''; + $desc = null; + $val = $this->json->get('env-vars')->get($name); + + if (gettype($val) == 'object') { + $value = $val->get('value'); + $desc = $val->get('description'); + } else { + $value = $val; + } $retVal[$name] = [ - 'value' => $this->json->get('env-vars')->get($name)->get('value'), - 'description' => $this->json->get('env-vars')->get($name)->get('description') + 'value' => Controller::resolveEnvValue($value), + 'description' => $desc ]; } @@ -328,25 +374,30 @@ public function getPrimaryLanguage(): string { * return the hashed value. If no password is set, this method will return the * string 'NO_PASSWORD'. * + * Note: The scheduler password value supports environment variable substitution + * using the 'env:' prefix (e.g., "env:SCHEDULER_PASS" in configuration). + * * @return string Password hash or the string 'NO_PASSWORD' if there is no * password. */ public function getSchedulerPassword(): string { - $pass = $this->json->get('scheduler-password') ?? 'NO_PASSWORD'; + $pass = ''.$this->json->get('scheduler-password') ?? 'NO_PASSWORD'; - if (strlen($pass.'') == 0 || $pass == 'NO_PASSWORD') { + if (strlen($pass) == 0 || $pass == 'NO_PASSWORD') { return 'NO_PASSWORD'; } - return $pass; + return Controller::resolveEnvValue($pass); } /** * Returns SMTP connection given its name. * - * The method will search - * for an account with the given name in the set + * The method will search for an account with the given name in the set * of added accounts. If no account was found, null is returned. * + * Note: SMTP account properties that use the 'env:' prefix in configuration + * will be automatically resolved to their environment variable values. + * * @param string $name The name of the account. * * @return SMTPAccount|null If the account is found, The method @@ -365,13 +416,17 @@ public function getSMTPConnection(string $name) { 'sender-name' => $this->getProp($jsonObj, 'sender-name', $name), 'server-address' => $this->getProp($jsonObj, 'host', $name), 'user' => $this->getProp($jsonObj, 'username', $name), - 'account-name' => $name + 'account-name' => $name, + 'access-token' => $this->getProp($jsonObj, 'access-token', $name, false) ]); } } /** * Returns an array that contains all added SMTP accounts. * + * Note: SMTP account properties that use the 'env:' prefix in configuration + * will be automatically resolved to their environment variable values. + * * @return array An array that contains all added SMTP accounts. */ public function getSMTPConnections(): array { @@ -390,6 +445,7 @@ public function getSMTPConnections(): array { $acc->setSenderName($this->getProp($jsonObj, 'sender-name', $name)); $acc->setServerAddress($this->getProp($jsonObj, 'host', $name)); $acc->setUsername($this->getProp($jsonObj, 'username', $name)); + $acc->setAccessToken($this->getProp($jsonObj, 'access-token', $name, false)); $retVal[$name] = $acc; } @@ -728,14 +784,14 @@ public function setTitleSeparator(string $separator) { public function toJSON() : Json { return $this->json; } - private function getProp(Json $j, $name, string $connName) { + private function getProp(Json $j, $name, string $connName, bool $requred = true) { $val = $j->get($name); - if ($val === null) { + if ($val === null && $requred) { throw new InitializationException('The property "'.$name.'" of the connection "'.$connName.'" is missing.'); } - return $val; + return Controller::resolveEnvValue($val); } private function isValidLangCode($langCode) { $code = strtoupper(trim($langCode)); diff --git a/composer.json b/composer.json index 70b113ede..499287414 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "webfiori/framework", "description": "WebFiori framework. Made to make the web bloom.", "homepage": "https://webfiori.com", - "version": "3.0.0-Beta.27", + "version": "3.0.0-beta.31", "type": "library", "support": { "issues": "https://github.com/webfiori/framework/issues", diff --git a/tests/WebFiori/Framework/Tests/Config/ClassDriverEnvTest.php b/tests/WebFiori/Framework/Tests/Config/ClassDriverEnvTest.php new file mode 100644 index 000000000..51e579948 --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Config/ClassDriverEnvTest.php @@ -0,0 +1,128 @@ +initialize(true); + + // Add connection with env: values + $conn = new ConnectionInfo('mysql', 'env:TEST_DB_USER', 'env:TEST_DB_PASS', 'env:TEST_DB_NAME'); + $conn->setHost('env:TEST_DB_HOST'); + $conn->setName('test_conn'); + $driver->addOrUpdateDBConnection($conn); + + // Retrieve and verify resolution + $retrieved = $driver->getDBConnection('test_conn'); + $this->assertEquals('192.168.1.100', $retrieved->getHost()); + $this->assertEquals('testuser', $retrieved->getUsername()); + $this->assertEquals('testpass123', $retrieved->getPassword()); + $this->assertEquals('testdb', $retrieved->getDBName()); + + $driver->remove(); + } + + /** + * @test + */ + public function testEnvVarsInSMTPConnection() { + putenv('TEST_SMTP_HOST=smtp.test.com'); + putenv('TEST_SMTP_USER=test@example.com'); + putenv('TEST_SMTP_PASS=smtppass'); + putenv('TEST_SMTP_FROM=noreply@test.com'); + + $driver = new ClassDriver(); + $driver->initialize(true); + + // Add SMTP with env: values + $smtp = new SMTPAccount(); + $smtp->setAccountName('test_smtp'); + $smtp->setServerAddress('env:TEST_SMTP_HOST'); + $smtp->setUsername('env:TEST_SMTP_USER'); + $smtp->setPassword('env:TEST_SMTP_PASS'); + $smtp->setAddress('env:TEST_SMTP_FROM'); + $smtp->setSenderName('Test Sender'); + $smtp->setPort(587); + $driver->addOrUpdateSMTPAccount($smtp); + + // Retrieve and verify resolution + $retrieved = $driver->getSMTPConnection('test_smtp'); + $this->assertEquals('smtp.test.com', $retrieved->getServerAddress()); + $this->assertEquals('test@example.com', $retrieved->getUsername()); + $this->assertEquals('smtppass', $retrieved->getPassword()); + $this->assertEquals('noreply@test.com', $retrieved->getAddress()); + + $driver->remove(); + } + + /** + * @test + */ + public function testEnvVarsInEnvVars() { + putenv('TEST_API_KEY=secret_key_123'); + + $driver = new ClassDriver(); + $driver->initialize(true); + + $driver->addEnvVar('MY_API_KEY', 'env:TEST_API_KEY', 'API Key from environment'); + + $vars = $driver->getEnvVars(); + $this->assertEquals('secret_key_123', $vars['MY_API_KEY']['value']); + + $driver->remove(); + } + + /** + * @test + */ + public function testEnvVarsInSchedulerPassword() { + putenv('TEST_SCHEDULER_PASS=hashed_password_123'); + + $driver = new ClassDriver(); + $driver->initialize(true); + + $driver->setSchedulerPassword('env:TEST_SCHEDULER_PASS'); + + $this->assertEquals('hashed_password_123', $driver->getSchedulerPassword()); + + $driver->remove(); + } + + /** + * @test + */ + public function testFallbackWhenEnvNotSet() { + $driver = new ClassDriver(); + $driver->initialize(true); + + // Add connection with env: that doesn't exist + $conn = new ConnectionInfo('mysql', 'env:NONEXISTENT_USER', 'pass', 'db'); + $conn->setHost('env:NONEXISTENT_HOST'); + $conn->setName('fallback_test'); + $driver->addOrUpdateDBConnection($conn); + + // Should fallback to original value + $retrieved = $driver->getDBConnection('fallback_test'); + $this->assertEquals('env:NONEXISTENT_HOST', $retrieved->getHost()); + $this->assertEquals('env:NONEXISTENT_USER', $retrieved->getUsername()); + + $driver->remove(); + } +} diff --git a/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php b/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php index e714dbb02..e8ce8ebf6 100644 --- a/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php +++ b/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php @@ -628,4 +628,42 @@ public function testSMTPConnections04() { $account = $driver->getSMTPConnection('Cool2'); $this->assertNotNull($account); } + public function testEnvVars00() { + $driver = new JsonDriver(); + $driver->setConfigFileName('config-with-env.json'); + putenv('HOST=22.22.22.22'); + putenv('VERBOSE=1'); + putenv('SMTP_00_USER=test@example.com'); + putenv('SMTP_00_PASS=3241'); + putenv('SMTP_00_ADDRESS=test2@example.com'); + putenv('SMTP_00_NAME=Ibrahim'); + putenv('SMTP_TOKEN=some_tkn'); + + putenv('DB_HOST_2=122.76.76.87'); + putenv('DB_NAME_2=Ibrahim'); + putenv('DB_PASS_2=some_pass'); + + + $driver->initialize(); + $vars = $driver->getEnvVars(); + + $this->assertEquals('22.22.22.22', $vars['HOST']['value']); + $this->assertEquals('1', $vars['WF_VERBOSE_2']['value']); + + $smtp = $driver->getSMTPConnection('conn00'); + $this->assertEquals('test2@example.com', $smtp->getAddress()); + $this->assertEquals('some_tkn', $smtp->getAccessToken()); + $this->assertEquals('3241', $smtp->getPassword()); + $this->assertEquals('test@example.com', $smtp->getUsername()); + $this->assertEquals('Ibrahim', $smtp->getSenderName()); + + $dbConn00 = $driver->getDBConnection('New_Connection_2'); + $this->assertEquals('122.76.76.87', $dbConn00->getHost()); + $this->assertEquals('Ibrahim', $dbConn00->getDBName()); + $this->assertEquals('some_pass', $dbConn00->getPassword()); + + putenv('SCHEDULER_PASS=my_secure_hash'); + $this->assertEquals('my_secure_hash', $driver->getSchedulerPassword()); + $driver->setConfigFileName('app-config.json'); + } }