diff options
-rw-r--r-- | apps/settings/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | apps/settings/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | apps/settings/lib/AppInfo/Application.php | 4 | ||||
-rw-r--r-- | apps/settings/lib/ConfigLexicon.php | 42 | ||||
-rw-r--r-- | lib/private/AppConfig.php | 34 | ||||
-rw-r--r-- | lib/private/Config/UserConfig.php | 41 | ||||
-rw-r--r-- | lib/unstable/Config/Lexicon/ConfigLexiconEntry.php | 45 | ||||
-rw-r--r-- | tests/lib/Config/LexiconTest.php | 29 | ||||
-rw-r--r-- | tests/lib/Config/TestConfigLexicon_I.php | 49 |
9 files changed, 220 insertions, 26 deletions
diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php index 1e8bc75c687..d180bf53c87 100644 --- a/apps/settings/composer/composer/autoload_classmap.php +++ b/apps/settings/composer/composer/autoload_classmap.php @@ -19,6 +19,7 @@ return array( '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 5dc337e158c..e10d498ec20 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 6e59e56fe82..6e47a8985fa 100644 --- a/apps/settings/lib/AppInfo/Application.php +++ b/apps/settings/lib/AppInfo/Application.php @@ -12,6 +12,7 @@ use OC\AppFramework\Utility\TimeFactory; 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 @@ class Application extends App implements IBootstrap { // 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 00000000000..3d94b23ac63 --- /dev/null +++ b/apps/settings/lib/ConfigLexicon.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Settings; + +use NCU\Config\Lexicon\ConfigLexiconEntry; +use NCU\Config\Lexicon\ConfigLexiconStrictness; +use NCU\Config\Lexicon\IConfigLexicon; +use NCU\Config\ValueType; + +/** + * ConfigLexicon for 'settings' app/user configs + */ +class ConfigLexicon implements IConfigLexicon { + public function getStrictness(): ConfigLexiconStrictness { + return ConfigLexiconStrictness::IGNORE; + } + + /** + * @inheritDoc + * @return ConfigLexiconEntry[] + */ + public function getAppConfigs(): array { + return [ + ]; + } + + /** + * @inheritDoc + * @return ConfigLexiconEntry[] + */ + public function getUserConfigs(): array { + return [ + (new ConfigLexiconEntry('email', ValueType::STRING, '', 'email'))->onSet(function (string &$value): void { $value = strtolower($value); }), + ]; + } +} diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index b6412b410bb..976c33a9644 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -438,7 +438,8 @@ class AppConfig implements IAppConfig { ): 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); @@ -469,6 +470,13 @@ class AppConfig implements IAppConfig { } elseif (isset($this->fastCache[$app][$key])) { $value = $this->fastCache[$app][$key]; } else { + // unknown value, might want to execute something instead of just returning default. + // default value will be stored in database, unless false is returned + if (($lexiconEntry?->executeOnInit() !== null) + && $lexiconEntry->executeOnInit()($default) !== false) { + $this->setTypedValue($app, $key, $default, $lazy, $type); + } + return $default; } @@ -748,11 +756,18 @@ class AppConfig implements IAppConfig { 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 +1608,7 @@ class AppConfig implements IAppConfig { ?bool &$lazy = null, int &$type = self::VALUE_MIXED, string &$default = '', + ?ConfigLexiconEntry &$lexiconEntry = null, ): bool { if (in_array($key, [ @@ -1617,23 +1633,23 @@ class AppConfig implements IAppConfig { 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 f8c59a13d3d..f1418124fbd 100644 --- a/lib/private/Config/UserConfig.php +++ b/lib/private/Config/UserConfig.php @@ -721,7 +721,8 @@ class UserConfig implements IUserConfig { ): 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; } @@ -753,6 +754,13 @@ class UserConfig implements IUserConfig { } elseif (isset($this->fastCache[$userId][$app][$key])) { $value = $this->fastCache[$userId][$app][$key]; } else { + // unknown value, might want to execute something instead of just returning default. + // default value will be stored in database, unless false is returned + if (($lexiconEntry?->executeOnInit() !== null) + && $lexiconEntry->executeOnInit()($default) !== false) { + $this->setTypedValue($userId, $app, $key, $default, $lazy, 0, $type); + } + return $default; } @@ -1067,18 +1075,20 @@ class UserConfig implements IUserConfig { 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 +1897,7 @@ class UserConfig implements IUserConfig { 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 +1914,18 @@ class UserConfig implements IUserConfig { 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 +1936,7 @@ class UserConfig implements IUserConfig { } // 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 d0d9b4cbd23..d63d4aafe46 100644 --- a/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php +++ b/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php @@ -8,6 +8,7 @@ declare(strict_types=1); namespace NCU\Config\Lexicon; +use Closure; use NCU\Config\ValueType; /** @@ -22,6 +23,8 @@ class ConfigLexiconEntry { private string $definition = ''; private ?string $default = null; + private ?Closure $initialize = null; + private ?Closure $onSet = null; /** * @param string $key config key @@ -137,6 +140,48 @@ class ConfigLexiconEntry { } /** + * set a closure to be executed when reading a non-set config value + * first param of the closure is the default value of the config key. + * + * @experimental 32.0.0 + */ + public function initialize(Closure $closure): self { + $this->initialize = $closure; + return $this; + } + + /** + * returns if something needs to be executed when reading a non-set config value + * + * @return Closure|null NULL if nothing is supposed to happens + * @experimental 32.0.0 + */ + public function executeOnInit(): ?Closure { + return $this->initialize; + } + + /** + * 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 * * @param string|int|float|bool|array $entry diff --git a/tests/lib/Config/LexiconTest.php b/tests/lib/Config/LexiconTest.php index 530767a7416..068984a56f3 100644 --- a/tests/lib/Config/LexiconTest.php +++ b/tests/lib/Config/LexiconTest.php @@ -203,4 +203,33 @@ class LexiconTest extends TestCase { $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')); + } + + public function testAppConfigInitialize() { + $this->assertSame('random_string', $this->appConfig->getValueString(TestConfigLexicon_I::APPID, 'key6')); + } + + public function testUserConfigInitialize() { + $this->assertSame('random_string', $this->userConfig->getValueString('user1', TestConfigLexicon_I::APPID, 'key6')); + } + } diff --git a/tests/lib/Config/TestConfigLexicon_I.php b/tests/lib/Config/TestConfigLexicon_I.php index 6fb7921b6e6..b9917e6a087 100644 --- a/tests/lib/Config/TestConfigLexicon_I.php +++ b/tests/lib/Config/TestConfigLexicon_I.php @@ -27,14 +27,59 @@ class TestConfigLexicon_I implements IConfigLexicon { 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('key6', ValueType::STRING, '', 'test key'))->initialize( + function (string &$d): void { + $d = 'random_string'; + } + ), + (new ConfigLexiconEntry('key6', ValueType::STRING, '', 'test key'))->initialize( + function (string &$d): void { + $d = 'random_string'; + } + ), + (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; + } + ), + (new ConfigLexiconEntry('key6', ValueType::STRING, '', 'test key'))->initialize( + function (string &$d): void { + $d = 'random_string'; + } + ), ]; } |