diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php index 1e8bc75c687d5..d180bf53c8730 100644 --- a/apps/settings/composer/composer/autoload_classmap.php +++ b/apps/settings/composer/composer/autoload_classmap.php @@ -19,6 +19,7 @@ 'OCA\\Settings\\Command\\AdminDelegation\\Add' => $baseDir . '/../lib/Command/AdminDelegation/Add.php', 'OCA\\Settings\\Command\\AdminDelegation\\Remove' => $baseDir . '/../lib/Command/AdminDelegation/Remove.php', 'OCA\\Settings\\Command\\AdminDelegation\\Show' => $baseDir . '/../lib/Command/AdminDelegation/Show.php', + 'OCA\\Settings\\ConfigLexicon' => $baseDir . '/../lib/ConfigLexicon.php', 'OCA\\Settings\\Controller\\AISettingsController' => $baseDir . '/../lib/Controller/AISettingsController.php', 'OCA\\Settings\\Controller\\AdminSettingsController' => $baseDir . '/../lib/Controller/AdminSettingsController.php', 'OCA\\Settings\\Controller\\AppSettingsController' => $baseDir . '/../lib/Controller/AppSettingsController.php', diff --git a/apps/settings/composer/composer/autoload_static.php b/apps/settings/composer/composer/autoload_static.php index 5dc337e158c01..e10d498ec20c9 100644 --- a/apps/settings/composer/composer/autoload_static.php +++ b/apps/settings/composer/composer/autoload_static.php @@ -34,6 +34,7 @@ class ComposerStaticInitSettings 'OCA\\Settings\\Command\\AdminDelegation\\Add' => __DIR__ . '/..' . '/../lib/Command/AdminDelegation/Add.php', 'OCA\\Settings\\Command\\AdminDelegation\\Remove' => __DIR__ . '/..' . '/../lib/Command/AdminDelegation/Remove.php', 'OCA\\Settings\\Command\\AdminDelegation\\Show' => __DIR__ . '/..' . '/../lib/Command/AdminDelegation/Show.php', + 'OCA\\Settings\\ConfigLexicon' => __DIR__ . '/..' . '/../lib/ConfigLexicon.php', 'OCA\\Settings\\Controller\\AISettingsController' => __DIR__ . '/..' . '/../lib/Controller/AISettingsController.php', 'OCA\\Settings\\Controller\\AdminSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AdminSettingsController.php', 'OCA\\Settings\\Controller\\AppSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AppSettingsController.php', diff --git a/apps/settings/lib/AppInfo/Application.php b/apps/settings/lib/AppInfo/Application.php index 6e59e56fe8218..6e47a8985fab3 100644 --- a/apps/settings/lib/AppInfo/Application.php +++ b/apps/settings/lib/AppInfo/Application.php @@ -12,6 +12,7 @@ use OC\Authentication\Events\AppPasswordCreatedEvent; use OC\Authentication\Token\IProvider; use OC\Server; +use OCA\Settings\ConfigLexicon; use OCA\Settings\Hooks; use OCA\Settings\Listener\AppPasswordCreatedActivityListener; use OCA\Settings\Listener\GroupRemovedListener; @@ -129,6 +130,9 @@ public function register(IRegistrationContext $context): void { // Register Settings Form(s) $context->registerDeclarativeSettings(MailProvider::class); + // Register Config Lexicon + $context->registerConfigLexicon(ConfigLexicon::class); + /** * Core class wrappers */ diff --git a/apps/settings/lib/ConfigLexicon.php b/apps/settings/lib/ConfigLexicon.php new file mode 100644 index 0000000000000..3d94b23ac6383 --- /dev/null +++ b/apps/settings/lib/ConfigLexicon.php @@ -0,0 +1,42 @@ +onSet(function (string &$value): void { $value = strtolower($value); }), + ]; + } +} diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index b6412b410bb16..dae0c377ddd35 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -438,7 +438,8 @@ private function getTypedValue( ): string { $this->assertParams($app, $key, valueType: $type); $origKey = $key; - if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $default)) { + /** @var ConfigLexiconEntry $lexiconEntry */ + if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $default, lexiconEntry: $lexiconEntry)) { return $default; // returns default if strictness of lexicon is set to WARNING (block and report) } $this->loadConfig($app, $lazy); @@ -748,11 +749,18 @@ private function setTypedValue( int $type, ): bool { $this->assertParams($app, $key); - if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type)) { + /** @var ConfigLexiconEntry $lexiconEntry */ + if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, lexiconEntry: $lexiconEntry)) { return false; // returns false as database is not updated } $this->loadConfig(null, $lazy); + // might want to execute something before storing new value + // will cancel storing of new value in database if false is returned + if ($lexiconEntry?->executeOnSet() !== null && $lexiconEntry?->executeOnSet()($value) === false) { + return false; + } + $sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type); $inserted = $refreshCache = false; @@ -1593,6 +1601,7 @@ private function matchAndApplyLexiconDefinition( ?bool &$lazy = null, int &$type = self::VALUE_MIXED, string &$default = '', + ?ConfigLexiconEntry &$lexiconEntry = null, ): bool { if (in_array($key, [ @@ -1617,23 +1626,23 @@ private function matchAndApplyLexiconDefinition( return true; } - /** @var ConfigLexiconEntry $configValue */ - $configValue = $configDetails['entries'][$key]; + /** @var ConfigLexiconEntry $lexiconEntry */ + $lexiconEntry = $configDetails['entries'][$key]; $type &= ~self::VALUE_SENSITIVE; - $appConfigValueType = $configValue->getValueType()->toAppConfigFlag(); + $appConfigValueType = $lexiconEntry->getValueType()->toAppConfigFlag(); if ($type === self::VALUE_MIXED) { $type = $appConfigValueType; // we overwrite if value was requested as mixed } elseif ($appConfigValueType !== $type) { throw new AppConfigTypeConflictException('The app config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon'); } - $lazy = $configValue->isLazy(); - $default = $configValue->getDefault() ?? $default; // default from Lexicon got priority - if ($configValue->isFlagged(self::FLAG_SENSITIVE)) { + $lazy = $lexiconEntry->isLazy(); + $default = $lexiconEntry->getDefault() ?? $default; // default from Lexicon got priority + if ($lexiconEntry->isFlagged(self::FLAG_SENSITIVE)) { $type |= self::VALUE_SENSITIVE; } - if ($configValue->isDeprecated()) { + if ($lexiconEntry->isDeprecated()) { $this->logger->notice('App config key ' . $app . '/' . $key . ' is set as deprecated.'); } diff --git a/lib/private/Config/UserConfig.php b/lib/private/Config/UserConfig.php index f8c59a13d3da2..be170623fadcc 100644 --- a/lib/private/Config/UserConfig.php +++ b/lib/private/Config/UserConfig.php @@ -721,7 +721,8 @@ private function getTypedValue( ): string { $this->assertParams($userId, $app, $key); $origKey = $key; - if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, default: $default)) { + /** @var ConfigLexiconEntry $lexiconEntry */ + if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, default: $default, lexiconEntry: $lexiconEntry)) { // returns default if strictness of lexicon is set to WARNING (block and report) return $default; } @@ -1067,18 +1068,20 @@ private function setTypedValue( int $flags, ValueType $type, ): bool { - // Primary email addresses are always(!) expected to be lowercase - if ($app === 'settings' && $key === 'email') { - $value = strtolower($value); - } - $this->assertParams($userId, $app, $key); - if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags)) { + /** @var ConfigLexiconEntry $lexiconEntry */ + if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags, lexiconEntry: $lexiconEntry)) { // returns false as database is not updated return false; } $this->loadConfig($userId, $lazy); + // might want to execute something before storing new value + // will cancel storing of new value in database if false is returned + if ($lexiconEntry?->executeOnSet() !== null && $lexiconEntry?->executeOnSet()($value) === false) { + return false; + } + $inserted = $refreshCache = false; $origValue = $value; $sensitive = $this->isFlagged(self::FLAG_SENSITIVE, $flags); @@ -1887,6 +1890,7 @@ private function matchAndApplyLexiconDefinition( ValueType &$type = ValueType::MIXED, int &$flags = 0, string &$default = '', + ?ConfigLexiconEntry &$lexiconEntry = null, ): bool { $configDetails = $this->getConfigDetailsFromLexicon($app); if (array_key_exists($key, $configDetails['aliases']) && !$this->ignoreLexiconAliases) { @@ -1903,18 +1907,18 @@ private function matchAndApplyLexiconDefinition( return true; } - /** @var ConfigLexiconEntry $configValue */ - $configValue = $configDetails['entries'][$key]; + /** @var ConfigLexiconEntry $lexiconEntry */ + $lexiconEntry = $configDetails['entries'][$key]; if ($type === ValueType::MIXED) { // we overwrite if value was requested as mixed - $type = $configValue->getValueType(); - } elseif ($configValue->getValueType() !== $type) { + $type = $lexiconEntry->getValueType(); + } elseif ($lexiconEntry->getValueType() !== $type) { throw new TypeConflictException('The user config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon'); } - $lazy = $configValue->isLazy(); - $flags = $configValue->getFlags(); - if ($configValue->isDeprecated()) { + $lazy = $lexiconEntry->isLazy(); + $flags = $lexiconEntry->getFlags(); + if ($lexiconEntry->isDeprecated()) { $this->logger->notice('User config key ' . $app . '/' . $key . ' is set as deprecated.'); } @@ -1925,7 +1929,7 @@ private function matchAndApplyLexiconDefinition( } // default from Lexicon got priority but it can still be overwritten by admin - $default = $this->getSystemDefault($app, $configValue) ?? $configValue->getDefault() ?? $default; + $default = $this->getSystemDefault($app, $lexiconEntry) ?? $lexiconEntry->getDefault() ?? $default; // returning false will make get() returning $default and set() not changing value in database return !$enforcedValue; diff --git a/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php b/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php index d0d9b4cbd23e1..f531687611b7c 100644 --- a/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php +++ b/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php @@ -8,6 +8,7 @@ namespace NCU\Config\Lexicon; +use Closure; use NCU\Config\ValueType; /** @@ -22,6 +23,7 @@ class ConfigLexiconEntry { private string $definition = ''; private ?string $default = null; + private ?Closure $onSet = null; /** * @param string $key config key @@ -136,6 +138,27 @@ public function getDefault(): ?string { return $this->default; } + /** + * set a closure to be executed when setting a value to this config key + * first param of the closure is future value. + * + * @experimental 32.0.0 + */ + public function onSet(Closure $closure): self { + $this->onSet = $closure; + return $this; + } + + /** + * returns if something needs to be executed when writing a new value + * + * @return Closure|null NULL if nothing is supposed to happens + * @experimental 32.0.0 + */ + public function executeOnSet(): ?Closure { + return $this->onSet; + } + /** * convert $entry into string, based on the expected type for config value * diff --git a/tests/lib/Config/LexiconTest.php b/tests/lib/Config/LexiconTest.php index 530767a7416d5..9ff0a3e28f67e 100644 --- a/tests/lib/Config/LexiconTest.php +++ b/tests/lib/Config/LexiconTest.php @@ -203,4 +203,24 @@ public function testAppConfigLexiconRenameInvertBoolean() { $this->configManager->migrateConfigLexiconKeys(TestConfigLexicon_I::APPID); $this->assertSame(false, $this->appConfig->getValueBool(TestConfigLexicon_I::APPID, 'key4')); } + + public function testAppConfigOnSetEdit() { + $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'key5', 42); + $this->assertSame(52, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'key5')); + } + + public function testAppConfigOnSetIgnore() { + $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'key5', 142); + $this->assertSame(12, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'key5')); + } + + public function testUserConfigOnSetEdit() { + $this->userConfig->setValueInt('user1', TestConfigLexicon_I::APPID, 'key5', 42); + $this->assertSame(32, $this->userConfig->getValueInt('user1', TestConfigLexicon_I::APPID, 'key5')); + } + + public function testUserConfigOnSetIgnore() { + $this->userConfig->setValueInt('user1', TestConfigLexicon_I::APPID, 'key5', 142); + $this->assertSame(12, $this->userConfig->getValueInt('user1', TestConfigLexicon_I::APPID, 'key5')); + } } diff --git a/tests/lib/Config/TestConfigLexicon_I.php b/tests/lib/Config/TestConfigLexicon_I.php index 6fb7921b6e68d..fc2ce076388ed 100644 --- a/tests/lib/Config/TestConfigLexicon_I.php +++ b/tests/lib/Config/TestConfigLexicon_I.php @@ -27,14 +27,44 @@ public function getAppConfigs(): array { new ConfigLexiconEntry('key1', ValueType::STRING, 'abcde', 'test key', true, IAppConfig::FLAG_SENSITIVE), new ConfigLexiconEntry('key2', ValueType::INT, 12345, 'test key', false), new ConfigLexiconEntry('key3', ValueType::INT, 12345, 'test key', true, rename: 'old_key3'), - new ConfigLexiconEntry('key4', ValueType::BOOL, 12345, 'test key', true, rename: 'old_key4', options: ConfigLexiconEntry::RENAME_INVERT_BOOLEAN), + new ConfigLexiconEntry( + 'key4', ValueType::BOOL, 12345, 'test key', true, rename: 'old_key4', + options: ConfigLexiconEntry::RENAME_INVERT_BOOLEAN + ), + (new ConfigLexiconEntry('key5', ValueType::INT, 12, 'test key', true))->onSet( + function (string &$v): bool { + $i = (int)$v; + if ($i > 100) { + return false; + } + $v = (string)($i + 10); + + return true; + } + ), + (new ConfigLexiconEntry('email', ValueType::STRING, '', 'email'))->onSet( + function (string &$value): void { + $value = strtolower($value); + } + ), ]; } public function getUserConfigs(): array { return [ new ConfigLexiconEntry('key1', ValueType::STRING, 'abcde', 'test key', true, IUserConfig::FLAG_SENSITIVE), - new ConfigLexiconEntry('key2', ValueType::INT, 12345, 'test key', false) + new ConfigLexiconEntry('key2', ValueType::INT, 12345, 'test key', false), + (new ConfigLexiconEntry('key5', ValueType::INT, 12, 'test key', true))->onSet( + function (string &$v): bool { + $i = (int)$v; + if ($i > 100) { + return false; + } + $v = (string)($i - 10); + + return true; + } + ), ]; }