diff --git a/app/code/core/Mage/Adminhtml/Block/System/Config/Form.php b/app/code/core/Mage/Adminhtml/Block/System/Config/Form.php index 9c1e38d14f8..b8cdcf6ab40 100644 --- a/app/code/core/Mage/Adminhtml/Block/System/Config/Form.php +++ b/app/code/core/Mage/Adminhtml/Block/System/Config/Form.php @@ -17,6 +17,7 @@ class Mage_Adminhtml_Block_System_Config_Form extends Mage_Adminhtml_Block_Widge public const SCOPE_DEFAULT = 'default'; public const SCOPE_WEBSITES = 'websites'; public const SCOPE_STORES = 'stores'; + public const SCOPE_ENV = 'env'; /** * Config data array @@ -71,6 +72,7 @@ public function __construct() self::SCOPE_DEFAULT => Mage::helper('adminhtml')->__('[GLOBAL]'), self::SCOPE_WEBSITES => Mage::helper('adminhtml')->__('[WEBSITE]'), self::SCOPE_STORES => Mage::helper('adminhtml')->__('[STORE VIEW]'), + self::SCOPE_ENV => Mage::helper('adminhtml')->__('[ENV]'), ]; } @@ -368,7 +370,7 @@ public function initFields($fieldset, $group, $section, $fieldPrefix = '', $labe } } - $field = $fieldset->addField($id, $fieldType, [ + $elementFieldData = [ 'name' => $name, 'label' => $label, 'comment' => $comment, @@ -384,7 +386,14 @@ public function initFields($fieldset, $group, $section, $fieldPrefix = '', $labe 'scope_label' => $this->getScopeLabel($element), 'can_use_default_value' => $this->canUseDefaultValue((int) $element->show_in_default), 'can_use_website_value' => $this->canUseWebsiteValue((int) $element->show_in_website), - ]); + ]; + if ($this->isOverwrittenByEnvVariable($path)) { + $elementFieldData['scope_label'] = $this->_scopeLabels[static::SCOPE_ENV]; + $elementFieldData['disabled'] = 1; + $elementFieldData['can_use_default_value'] = 0; + $elementFieldData['can_use_website_value'] = 0; + } + $field = $fieldset->addField($id, $fieldType, $elementFieldData); $this->_prepareFieldOriginalData($field, $element); if (isset($element->validate)) { @@ -622,6 +631,23 @@ public function getScope() return $scope; } + /** + * Returns true if element was overwritten by ENV variable + */ + public function isOverwrittenByEnvVariable(string $path): bool + { + /** @var Mage_Core_Helper_EnvironmentConfigLoader $environmentConfigLoaderHelper */ + $environmentConfigLoaderHelper = Mage::helper('core/environmentConfigLoader'); + $store = Mage::app()->getRequest()->getParam('store'); + if ($store) { + $scope = $this->getScope(); + $path = "$scope/$store/$path"; + return $environmentConfigLoaderHelper->hasPath($path); + } + $path = "default/$path"; + return $environmentConfigLoaderHelper->hasPath($path); + } + /** * Retrieve label for scope * diff --git a/app/code/core/Mage/Adminhtml/Model/Config/Data.php b/app/code/core/Mage/Adminhtml/Model/Config/Data.php index edc85b37bda..52fee450ee8 100644 --- a/app/code/core/Mage/Adminhtml/Model/Config/Data.php +++ b/app/code/core/Mage/Adminhtml/Model/Config/Data.php @@ -341,6 +341,14 @@ protected function _getPathConfig($path, $full = true) $config[$data->getPath()] = $data->getValue(); } } + + if (!$full) { + /** @var Mage_Core_Helper_EnvironmentConfigLoader $environmentConfigLoaderHelper */ + $environmentConfigLoaderHelper = Mage::helper('core/environmentConfigLoader'); + $store = $this->getStore(); + $envConfig = $environmentConfigLoaderHelper->getAsArray($store); + $config = array_merge($config, $envConfig); + } return $config; } diff --git a/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader.php b/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader.php index 33d78ea3d2b..1f3c6d2d7d0 100644 --- a/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader.php +++ b/app/code/core/Mage/Core/Helper/EnvironmentConfigLoader.php @@ -15,6 +15,7 @@ class Mage_Core_Helper_EnvironmentConfigLoader extends Mage_Core_Helper_Abstract { protected const ENV_STARTS_WITH = 'OPENMAGE_CONFIG'; + protected const ENV_FEATURE_ENABLED = 'OPENMAGE_CONFIG_OVERRIDE_ALLOWED'; protected const ENV_KEY_SEPARATOR = '__'; protected const CONFIG_KEY_DEFAULT = 'DEFAULT'; protected const CONFIG_KEY_WEBSITES = 'WEBSITES'; @@ -50,6 +51,10 @@ class Mage_Core_Helper_EnvironmentConfigLoader extends Mage_Core_Helper_Abstract */ public function overrideEnvironment(Varien_Simplexml_Config $xmlConfig) { + $data = Mage::registry('current_env_config'); + if ($data) { + return; + } $env = $this->getEnv(); foreach ($env as $configKey => $value) { @@ -63,18 +68,120 @@ public function overrideEnvironment(Varien_Simplexml_Config $xmlConfig) case static::CONFIG_KEY_DEFAULT: [$unused1, $unused2, $section, $group, $field] = $configKeyParts; $path = $this->buildPath($section, $group, $field); - $xmlConfig->setNode($this->buildNodePath($scope, $path), $value); + $nodePath = $this->buildNodePath($scope, $path); + $xmlConfig->setNode($nodePath, $value); + try { + foreach (['0', 'admin'] as $store) { + $store = Mage::app()->getStore($store); + $this->setCache($store, $value, $path); + } + } catch (Throwable $exception) { + // invalid store, intentionally empty + } break; case static::CONFIG_KEY_WEBSITES: case static::CONFIG_KEY_STORES: - [$unused1, $unused2, $code, $section, $group, $field] = $configKeyParts; + [$unused1, $unused2, $storeCode, $section, $group, $field] = $configKeyParts; $path = $this->buildPath($section, $group, $field); - $nodePath = sprintf('%s/%s/%s', strtolower($scope), strtolower($code), $path); + $storeCode = strtolower($storeCode); + $scope = strtolower($scope); + $nodePath = sprintf('%s/%s/%s', $scope, $storeCode, $path); $xmlConfig->setNode($nodePath, $value); + try { + if (!str_contains($nodePath, 'websites')) { + foreach ([$storeCode, 'admin'] as $store) { + $store = Mage::app()->getStore($store); + $this->setCache($store, $value, $path); + } + } + } catch (Throwable $exception) { + // invalid store, intentionally empty + } + break; + } + } + Mage::register('current_env_config', true, true); + } + + public function hasPath(string $wantedPath): bool + { + $data = Mage::registry("config_env_has_path_$wantedPath"); + if ($data !== null) { + return $data; + } + $env = $this->getEnv(); + $config = []; + + foreach ($env as $configKey => $value) { + if (!$this->isConfigKeyValid($configKey)) { + continue; + } + + [$configKeyParts, $scope] = $this->getConfigKey($configKey); + + switch ($scope) { + case static::CONFIG_KEY_DEFAULT: + [$unused1, $unused2, $section, $group, $field] = $configKeyParts; + $path = $this->buildPath($section, $group, $field); + $nodePath = $this->buildNodePath($scope, $path); + $config[$nodePath] = $value; + break; + + case static::CONFIG_KEY_WEBSITES: + case static::CONFIG_KEY_STORES: + [$unused1, $unused2, $storeCode, $section, $group, $field] = $configKeyParts; + $path = $this->buildPath($section, $group, $field); + $storeCode = strtolower($storeCode); + $scope = strtolower($scope); + $nodePath = sprintf('%s/%s/%s', $scope, $storeCode, $path); + $config[$nodePath] = $value; break; } } + $hasConfig = array_key_exists($wantedPath, $config); + Mage::register("config_env_has_path_$wantedPath", $hasConfig); + return $hasConfig; + } + + public function getAsArray(string $wantedStore): array + { + if (empty($wantedStore)) { + $wantedStore = 'default'; + } + $data = Mage::registry("config_env_array_$wantedStore"); + if ($data !== null) { + return $data; + } + $env = $this->getEnv(); + $config = []; + + foreach ($env as $configKey => $value) { + if (!$this->isConfigKeyValid($configKey)) { + continue; + } + + [$configKeyParts, $scope] = $this->getConfigKey($configKey); + + switch ($scope) { + case static::CONFIG_KEY_DEFAULT: + [$unused1, $unused2, $section, $group, $field] = $configKeyParts; + $path = $this->buildPath($section, $group, $field); + $config[$path] = $value; + break; + case static::CONFIG_KEY_WEBSITES: + case static::CONFIG_KEY_STORES: + [$unused1, $unused2, $storeCode, $section, $group, $field] = $configKeyParts; + if (strtolower($storeCode) !== strtolower($wantedStore)) { + break; + } + $path = $this->buildPath($section, $group, $field); + $config[$path] = $value; + break; + } + } + Mage::register("config_env_array_$wantedStore", $config); + return $config; } /** @@ -88,11 +195,34 @@ public function setEnvStore(array $envStorage): void public function getEnv(): array { if (empty($this->envStore)) { - $this->envStore = getenv(); + $env = getenv(); + $env = array_filter($env, function ($key) { + return str_starts_with($key, static::ENV_STARTS_WITH); + }, ARRAY_FILTER_USE_KEY); + $this->envStore = $env; + } + if (!isset($this->envStore[static::ENV_FEATURE_ENABLED]) || + (bool) $this->envStore[static::ENV_FEATURE_ENABLED] === false + ) { + $this->envStore = []; + return $this->envStore; } return $this->envStore; } + protected function setCache(Mage_Core_Model_Store $store, $value, string $path): void + { + $refObject = new ReflectionObject($store); + $refProperty = $refObject->getProperty('_configCache'); + $refProperty->setAccessible(true); + $configCache = $refProperty->getValue($store); + if (!is_array($configCache)) { + $configCache = []; + } + $configCache[$path] = $value; + $store->setConfigCache($configCache); + } + protected function getConfigKey(string $configKey): array { $configKeyParts = array_filter( @@ -108,10 +238,6 @@ protected function getConfigKey(string $configKey): array protected function isConfigKeyValid(string $configKey): bool { - if (!str_starts_with($configKey, static::ENV_STARTS_WITH)) { - return false; - } - $sectionGroupFieldRegexp = sprintf('([%s]*)', implode('', static::ALLOWED_CHARS)); $allowedChars = sprintf('[%s]', implode('', static::ALLOWED_CHARS)); $regexp = '/' . static::ENV_STARTS_WITH . static::ENV_KEY_SEPARATOR . '(WEBSITES' . static::ENV_KEY_SEPARATOR diff --git a/app/code/core/Mage/Core/Model/App.php b/app/code/core/Mage/Core/Model/App.php index 422c2830cb8..a5f10bba2e3 100644 --- a/app/code/core/Mage/Core/Model/App.php +++ b/app/code/core/Mage/Core/Model/App.php @@ -622,6 +622,7 @@ public function reinitStores() */ protected function _initStores() { + Mage::unregister('current_env_config'); $this->_stores = []; $this->_groups = []; $this->_website = null; diff --git a/app/code/core/Mage/Core/Model/Store.php b/app/code/core/Mage/Core/Model/Store.php index b0670a29402..cde4b284ae5 100644 --- a/app/code/core/Mage/Core/Model/Store.php +++ b/app/code/core/Mage/Core/Model/Store.php @@ -336,6 +336,9 @@ public function getConfig($path) } $config = Mage::getConfig(); + /** @var Mage_Core_Helper_EnvironmentConfigLoader $environmentConfigLoaderHelper */ + $environmentConfigLoaderHelper = Mage::helper('core/environmentConfigLoader'); + $environmentConfigLoaderHelper->overrideEnvironment($config); $fullPath = 'stores/' . $this->getCode() . '/' . $path; $data = $config->getNode($fullPath); diff --git a/tests/unit/Mage/Core/Helper/EnvironmentConfigLoaderTest.php b/tests/unit/Mage/Core/Helper/EnvironmentConfigLoaderTest.php index 2029fe89bde..662b434c2bc 100644 --- a/tests/unit/Mage/Core/Helper/EnvironmentConfigLoaderTest.php +++ b/tests/unit/Mage/Core/Helper/EnvironmentConfigLoaderTest.php @@ -18,6 +18,9 @@ use OpenMage\Tests\Unit\OpenMageTest; use Varien_Simplexml_Config; +/** + * @group Mage_Core_EnvLoader + */ class EnvironmentConfigLoaderTest extends OpenMageTest { public const XML_PATH_GENERAL = 'general/store_information/name'; @@ -49,6 +52,32 @@ public function testBuildPath(): void /** * @group Helper */ + public function testEnvFilter(): void + { + $environmentConfigLoaderHelper = new EnvironmentConfigLoaderTestHelper(); + /** @phpstan-ignore method.internal */ + $environmentConfigLoaderHelper->setEnvStore([ + 'OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME' => 'some_value', + ]); + // empty because env flag is not set + $env = $environmentConfigLoaderHelper->getEnv(); + static::assertIsArray($env); + static::assertEmpty($env); + /** @phpstan-ignore method.internal */ + $environmentConfigLoaderHelper->setEnvStore([ + 'OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME' => 'some_value', + 'OPENMAGE_CONFIG_OVERRIDE_ALLOWED' => 1, // enable feature + ]); + // flag is set => feature is enabled + $env = $environmentConfigLoaderHelper->getEnv(); + static::assertIsArray($env); + static::assertNotEmpty($env); + } + + /** + * @group Mage_Core + * @group Mage_Core_Helper + */ public function testBuildNodePath(): void { $environmentConfigLoaderHelper = new EnvironmentConfigLoaderTestHelper(); @@ -70,6 +99,7 @@ public function testXmlHasTestStrings(): void } /** + * @runInSeparateProcess * @dataProvider envOverridesCorrectConfigKeysDataProvider * @group Helper * @@ -84,11 +114,14 @@ public function testEnvOverridesForValidConfigKeys(array $config): void $xml = new Varien_Simplexml_Config(); $xml->loadString($xmlStruct); + $loader = new Mage_Core_Helper_EnvironmentConfigLoader(); /** @phpstan-ignore method.internal */ $loader->setEnvStore([ + 'OPENMAGE_CONFIG_OVERRIDE_ALLOWED' => 1, $config['env_path'] => $config['value'], ]); + Mage::unregister('current_env_config'); $loader->overrideEnvironment($xml); $configPath = $config['xml_path']; @@ -96,7 +129,9 @@ public function testEnvOverridesForValidConfigKeys(array $config): void $valueAfterOverride = $xml->getNode($configPath); // assert - static::assertNotSame((string) $defaultValue, (string) $valueAfterOverride, 'Default value was not overridden.'); + $expected = (string) $defaultValue; + $actual = (string) $valueAfterOverride; + static::assertNotSame($expected, $actual, 'Default value was not overridden.'); } public function envOverridesCorrectConfigKeysDataProvider(): Generator @@ -170,6 +205,104 @@ public function envOverridesCorrectConfigKeysDataProvider(): Generator } /** + * @runInSeparateProcess + * @dataProvider envAsArrayDataProvider + * @group Mage_Core + * + * @param array $config + */ + public function testAsArray(array $config): void + { + // phpcs:ignore Ecg.Classes.ObjectInstantiation.DirectInstantiation + $loader = new Mage_Core_Helper_EnvironmentConfigLoader(); + /** @phpstan-ignore method.internal */ + $loader->setEnvStore([ + 'OPENMAGE_CONFIG_OVERRIDE_ALLOWED' => 1, + $config['env_path'] => 1, + ]); + $store = $config['store']; + $actual = $loader->getAsArray($store); + $expected = $config['expected']; + static::assertSame($expected, $actual); + } + + public function envAsArrayDataProvider(): Generator + { + yield 'default' => [ + [ + 'env_path' => 'OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME', + 'store' => '', // or 'default', which will be used internally, but this is how \Mage_Adminhtml_Model_Config_Data::_validate defines it + 'expected' => [ + self::XML_PATH_GENERAL => 1, + ], + ], + ]; + yield 'store' => [ + [ + 'env_path' => 'OPENMAGE_CONFIG__STORES__GERMAN__GENERAL__STORE_INFORMATION__NAME', + 'store' => 'german', + 'expected' => [ + self::XML_PATH_GENERAL => 1, + ], + ], + ]; + yield 'invalidStore' => [ + [ + 'env_path' => '', + 'store' => 'foo', + 'expected' => [], + ], + ]; + } + + /** + * @runInSeparateProcess + * @dataProvider envHasPathDataProvider + * @group Mage_Core + * + * @param array $config + */ + public function testHasPath(array $config): void + { + // phpcs:ignore Ecg.Classes.ObjectInstantiation.DirectInstantiation + $loader = new Mage_Core_Helper_EnvironmentConfigLoader(); + /** @phpstan-ignore method.internal */ + $loader->setEnvStore([ + 'OPENMAGE_CONFIG_OVERRIDE_ALLOWED' => 1, + $config['env_path'] => 1, + ]); + $actual = $loader->hasPath($config['xml_path']); + $expected = $config['expected']; + static::assertSame($expected, $actual); + } + + public function envHasPathDataProvider(): Generator + { + yield 'hasPath default' => [ + [ + 'env_path' => 'OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME', + 'xml_path' => 'default/general/store_information/name', + 'expected' => true, + ], + ]; + yield 'hasPath store' => [ + [ + 'env_path' => 'OPENMAGE_CONFIG__STORES__GERMAN__GENERAL__STORE_INFORMATION__NAME', + 'xml_path' => 'stores/german/general/store_information/name', + 'expected' => true, + ], + ]; + yield 'hasNotPath' => [ + [ + 'env_path' => 'OPENMAGE_CONFIG__DEFAULT__GENERAL__STORE_INFORMATION__NAME', + 'xml_path' => 'foo/foo/foo', + 'expected' => false, + ], + ]; + } + + /** + * @runInSeparateProcess * @dataProvider envDoesNotOverrideOnWrongConfigKeysDataProvider * @group Helper * @@ -185,15 +318,19 @@ public function testEnvDoesNotOverrideForInvalidConfigKeys(array $config): void $xml->loadString($xmlStruct); $defaultValue = 'test_default'; - static::assertSame($defaultValue, (string) $xml->getNode(self::XML_PATH_DEFAULT)); + $actual = (string) $xml->getNode(self::XML_PATH_DEFAULT); + static::assertSame($defaultValue, $actual); $defaultWebsiteValue = 'test_website'; - static::assertSame($defaultWebsiteValue, (string) $xml->getNode(self::XML_PATH_WEBSITE)); + $actual = (string) $xml->getNode(self::XML_PATH_WEBSITE); + static::assertSame($defaultWebsiteValue, $actual); $defaultStoreValue = 'test_store'; - static::assertSame($defaultStoreValue, (string) $xml->getNode(self::XML_PATH_STORE)); + $actual = (string) $xml->getNode(self::XML_PATH_STORE); + static::assertSame($defaultStoreValue, $actual); $loader = new Mage_Core_Helper_EnvironmentConfigLoader(); /** @phpstan-ignore method.internal */ $loader->setEnvStore([ + 'OPENMAGE_CONFIG_OVERRIDE_ALLOWED' => 1, $config['path'] => $config['value'], ]); $loader->overrideEnvironment($xml);