diff options
author | Maxence Lange <maxence@artificial-owl.com> | 2025-01-14 15:12:36 -0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-14 15:12:36 -0100 |
commit | 93a64d67b58c4def5281c62e97760ae191fd664e (patch) | |
tree | 9d2d6857464e5048b21f6d471aa8de5942a5d801 | |
parent | f44f122456205975f3080028b13a7151e815d6bc (diff) | |
parent | 28acc002a2e4b1e54fb3a3c68af504166f89283c (diff) | |
download | nextcloud-server-93a64d67b58c4def5281c62e97760ae191fd664e.tar.gz nextcloud-server-93a64d67b58c4def5281c62e97760ae191fd664e.zip |
Merge pull request #49848 from nextcloud/feat/noid/lexicon-configurable-default-value
feat(lexicon): configurable default value
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | lib/private/AppFramework/Bootstrap/RegistrationContext.php | 3 | ||||
-rw-r--r-- | lib/private/Config/Lexicon/CoreConfigLexicon.php | 43 | ||||
-rw-r--r-- | lib/private/Config/UserConfig.php | 77 | ||||
-rw-r--r-- | lib/unstable/Config/Lexicon/ConfigLexiconEntry.php | 65 | ||||
-rw-r--r-- | tests/lib/Config/UserConfigTest.php | 4 |
7 files changed, 157 insertions, 37 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index ffa8da43873..9809ea53508 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1157,6 +1157,7 @@ return array( 'OC\\Comments\\Manager' => $baseDir . '/lib/private/Comments/Manager.php', 'OC\\Comments\\ManagerFactory' => $baseDir . '/lib/private/Comments/ManagerFactory.php', 'OC\\Config' => $baseDir . '/lib/private/Config.php', + 'OC\\Config\\Lexicon\\CoreConfigLexicon' => $baseDir . '/lib/private/Config/Lexicon/CoreConfigLexicon.php', 'OC\\Config\\UserConfig' => $baseDir . '/lib/private/Config/UserConfig.php', 'OC\\Console\\Application' => $baseDir . '/lib/private/Console/Application.php', 'OC\\Console\\TimestampFormatter' => $baseDir . '/lib/private/Console/TimestampFormatter.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 89c0cd4395b..f612e0d5fc6 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1198,6 +1198,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Comments\\Manager' => __DIR__ . '/../../..' . '/lib/private/Comments/Manager.php', 'OC\\Comments\\ManagerFactory' => __DIR__ . '/../../..' . '/lib/private/Comments/ManagerFactory.php', 'OC\\Config' => __DIR__ . '/../../..' . '/lib/private/Config.php', + 'OC\\Config\\Lexicon\\CoreConfigLexicon' => __DIR__ . '/../../..' . '/lib/private/Config/Lexicon/CoreConfigLexicon.php', 'OC\\Config\\UserConfig' => __DIR__ . '/../../..' . '/lib/private/Config/UserConfig.php', 'OC\\Console\\Application' => __DIR__ . '/../../..' . '/lib/private/Console/Application.php', 'OC\\Console\\TimestampFormatter' => __DIR__ . '/../../..' . '/lib/private/Console/TimestampFormatter.php', diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index f3b612edc38..77cc58c6468 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -11,6 +11,7 @@ namespace OC\AppFramework\Bootstrap; use Closure; use NCU\Config\Lexicon\IConfigLexicon; +use OC\Config\Lexicon\CoreConfigLexicon; use OC\Support\CrashReport\Registry; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IRegistrationContext; @@ -143,7 +144,7 @@ class RegistrationContext { private array $declarativeSettings = []; /** @var array<array-key, string> */ - private array $configLexiconClasses = []; + private array $configLexiconClasses = ['core' => CoreConfigLexicon::class]; /** @var ServiceRegistration<ITeamResourceProvider>[] */ private array $teamResourceProviders = []; diff --git a/lib/private/Config/Lexicon/CoreConfigLexicon.php b/lib/private/Config/Lexicon/CoreConfigLexicon.php new file mode 100644 index 00000000000..34a0b883c54 --- /dev/null +++ b/lib/private/Config/Lexicon/CoreConfigLexicon.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Config\Lexicon; + +use NCU\Config\Lexicon\ConfigLexiconEntry; +use NCU\Config\Lexicon\ConfigLexiconStrictness; +use NCU\Config\Lexicon\IConfigLexicon; +use NCU\Config\ValueType; + +/** + * ConfigLexicon for 'core' app/user configs + */ +class CoreConfigLexicon implements IConfigLexicon { + public function getStrictness(): ConfigLexiconStrictness { + return ConfigLexiconStrictness::IGNORE; + } + + /** + * @inheritDoc + * @return ConfigLexiconEntry[] + */ + public function getAppConfigs(): array { + return [ + new ConfigLexiconEntry('lastcron', ValueType::INT, 0, 'timestamp of last cron execution'), + ]; + } + + /** + * @inheritDoc + * @return ConfigLexiconEntry[] + */ + public function getUserConfigs(): array { + return [ + new ConfigLexiconEntry('lang', ValueType::STRING, null, 'language'), + ]; + } +} diff --git a/lib/private/Config/UserConfig.php b/lib/private/Config/UserConfig.php index 78c43fc4321..776cce08d67 100644 --- a/lib/private/Config/UserConfig.php +++ b/lib/private/Config/UserConfig.php @@ -71,6 +71,7 @@ class UserConfig implements IUserConfig { public function __construct( protected IDBConnection $connection, + protected IConfig $config, protected LoggerInterface $logger, protected ICrypto $crypto, ) { @@ -228,7 +229,8 @@ class UserConfig implements IUserConfig { // there is a huge probability the non-lazy config are already loaded // meaning that we can start by only checking if a current non-lazy key exists if ($this->hasKey($userId, $app, $key, false)) { - return false; // meaning key is not lazy. + // meaning key is not lazy. + return false; } // as key is not found as non-lazy, we load and search in the lazy config @@ -263,7 +265,8 @@ class UserConfig implements IUserConfig { $values = array_filter( $this->formatAppValues($userId, $app, ($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? []), $filtered), function (string $key) use ($prefix): bool { - return str_starts_with($key, $prefix); // filter values based on $prefix + // filter values based on $prefix + return str_starts_with($key, $prefix); }, ARRAY_FILTER_USE_KEY ); @@ -711,8 +714,9 @@ class UserConfig implements IUserConfig { ValueType $type, ): string { $this->assertParams($userId, $app, $key); - if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, default: $default)) { - return $default; // returns default if strictness of lexicon is set to WARNING (block and report) + if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, default: $default)) { + // returns default if strictness of lexicon is set to WARNING (block and report) + return $default; } $this->loadConfig($userId, $lazy); @@ -1046,8 +1050,9 @@ class UserConfig implements IUserConfig { ValueType $type, ): bool { $this->assertParams($userId, $app, $key); - if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $flags)) { - return false; // returns false as database is not updated + if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags)) { + // returns false as database is not updated + return false; } $this->loadConfig($userId, $lazy); @@ -1100,7 +1105,8 @@ class UserConfig implements IUserConfig { $inserted = true; } catch (DBException $e) { if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { - throw $e; // TODO: throw exception or just log and returns false !? + // TODO: throw exception or just log and returns false !? + throw $e; } } } @@ -1195,7 +1201,8 @@ class UserConfig implements IUserConfig { public function updateType(string $userId, string $app, string $key, ValueType $type = ValueType::MIXED): bool { $this->assertParams($userId, $app, $key); $this->loadConfigAll($userId); - $this->isLazy($userId, $app, $key); // confirm key exists + // confirm key exists + $this->isLazy($userId, $app, $key); $update = $this->connection->getQueryBuilder(); $update->update('preferences') @@ -1287,7 +1294,8 @@ class UserConfig implements IUserConfig { } } - $this->clearCacheAll(); // we clear all cache + // we clear all cache + $this->clearCacheAll(); } /** @@ -1370,7 +1378,8 @@ class UserConfig implements IUserConfig { } } - $this->clearCacheAll(); // we clear all cache + // we clear all cache + $this->clearCacheAll(); } /** @@ -1793,6 +1802,14 @@ class UserConfig implements IUserConfig { } + /** + * will change referenced $value with the decrypted value in case of encrypted (sensitive value) + * + * @param string $userId + * @param string $app + * @param string $key + * @param string $value + */ private function decryptSensitiveValue(string $userId, string $app, string $key, string &$value): void { if (!$this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags'] ?? 0)) { return; @@ -1820,8 +1837,10 @@ class UserConfig implements IUserConfig { * * @throws UnknownKeyException * @throws TypeConflictException + * @return bool FALSE if conflict with defined lexicon were observed in the process */ private function matchAndApplyLexiconDefinition( + string $userId, string $app, string $key, bool &$lazy, @@ -1837,20 +1856,50 @@ class UserConfig implements IUserConfig { /** @var ConfigLexiconEntry $configValue */ $configValue = $configDetails['entries'][$key]; if ($type === ValueType::MIXED) { - $type = $configValue->getValueType(); // we overwrite if value was requested as mixed + // we overwrite if value was requested as mixed + $type = $configValue->getValueType(); } elseif ($configValue->getValueType() !== $type) { throw new TypeConflictException('The user 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 $flags = $configValue->getFlags(); - if ($configValue->isDeprecated()) { $this->logger->notice('User config key ' . $app . '/' . $key . ' is set as deprecated.'); } - return true; + $enforcedValue = $this->config->getSystemValue('lexicon.default.userconfig.enforced', [])[$app][$key] ?? false; + if (!$enforcedValue && $this->hasKey($userId, $app, $key, $lazy)) { + // if key exists there should be no need to extract default + return true; + } + + // default from Lexicon got priority but it can still be overwritten by admin + $default = $this->getSystemDefault($app, $configValue) ?? $configValue->getDefault() ?? $default; + + // returning false will make get() returning $default and set() not changing value in database + return !$enforcedValue; + } + + /** + * get default value set in config/config.php if stored in key: + * + * 'lexicon.default.userconfig' => [ + * <appId> => [ + * <configKey> => 'my value', + * ] + * ], + * + * The entry is converted to string to fit the expected type when managing default value + */ + private function getSystemDefault(string $appId, ConfigLexiconEntry $configValue): ?string { + $default = $this->config->getSystemValue('lexicon.default.userconfig', [])[$appId][$configValue->getKey()] ?? null; + if ($default === null) { + // no system default, using default default. + return null; + } + + return $configValue->convertToString($default); } /** diff --git a/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php b/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php index e6c6579881d..d7d781d8e26 100644 --- a/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php +++ b/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace NCU\Config\Lexicon; @@ -35,25 +35,12 @@ class ConfigLexiconEntry { public function __construct( private readonly string $key, private readonly ValueType $type, - null|string|int|float|bool|array $default = null, + private null|string|int|float|bool|array $defaultRaw = null, string $definition = '', private readonly bool $lazy = false, private readonly int $flags = 0, private readonly bool $deprecated = false, ) { - if ($default !== null) { - // in case $default is array but is not expected to be an array... - $default = ($type !== ValueType::ARRAY && is_array($default)) ? json_encode($default) : $default; - $this->default = match ($type) { - ValueType::MIXED => (string)$default, - ValueType::STRING => $this->convertFromString((string)$default), - ValueType::INT => $this->convertFromInt((int)$default), - ValueType::FLOAT => $this->convertFromFloat((float)$default), - ValueType::BOOL => $this->convertFromBool((bool)$default), - ValueType::ARRAY => $this->convertFromArray((array)$default) - }; - } - /** @psalm-suppress UndefinedClass */ if (\OC::$CLI) { // only store definition if ran from CLI $this->definition = $definition; @@ -61,7 +48,7 @@ class ConfigLexiconEntry { } /** - * @inheritDoc + * returns the config key * * @return string config key * @experimental 31.0.0 @@ -71,7 +58,7 @@ class ConfigLexiconEntry { } /** - * @inheritDoc + * get expected type for config value * * @return ValueType * @experimental 31.0.0 @@ -126,17 +113,51 @@ class ConfigLexiconEntry { } /** - * @inheritDoc + * returns default value * * @return string|null NULL if no default is set * @experimental 31.0.0 */ public function getDefault(): ?string { + if ($this->defaultRaw === null) { + return null; + } + + if ($this->default === null) { + $this->default = $this->convertToString($this->defaultRaw); + } + return $this->default; } /** - * @inheritDoc + * convert $entry into string, based on the expected type for config value + * + * @param string|int|float|bool|array $entry + * + * @return string + * @experimental 31.0.0 + * @psalm-suppress PossiblyInvalidCast arrays are managed pre-cast + * @psalm-suppress RiskyCast + */ + public function convertToString(string|int|float|bool|array $entry): string { + // in case $default is array but is not expected to be an array... + if ($this->getValueType() !== ValueType::ARRAY && is_array($entry)) { + $entry = json_encode($entry, JSON_THROW_ON_ERROR); + } + + return match ($this->getValueType()) { + ValueType::MIXED => (string)$entry, + ValueType::STRING => $this->convertFromString((string)$entry), + ValueType::INT => $this->convertFromInt((int)$entry), + ValueType::FLOAT => $this->convertFromFloat((float)$entry), + ValueType::BOOL => $this->convertFromBool((bool)$entry), + ValueType::ARRAY => $this->convertFromArray((array)$entry) + }; + } + + /** + * returns definition * * @return string * @experimental 31.0.0 @@ -146,7 +167,7 @@ class ConfigLexiconEntry { } /** - * @inheritDoc + * returns if config key is set as lazy * * @see IAppConfig for details on lazy config values * @return bool TRUE if config value is lazy @@ -157,7 +178,7 @@ class ConfigLexiconEntry { } /** - * @inheritDoc + * returns flags * * @see IAppConfig for details on sensitive config values * @return int bitflag about the config value @@ -178,7 +199,7 @@ class ConfigLexiconEntry { } /** - * @inheritDoc + * returns if config key is set as deprecated * * @return bool TRUE if config si deprecated * @experimental 31.0.0 diff --git a/tests/lib/Config/UserConfigTest.php b/tests/lib/Config/UserConfigTest.php index 0f2aed4a288..f9a9b5c5272 100644 --- a/tests/lib/Config/UserConfigTest.php +++ b/tests/lib/Config/UserConfigTest.php @@ -12,6 +12,7 @@ use NCU\Config\Exceptions\UnknownKeyException; use NCU\Config\IUserConfig; use NCU\Config\ValueType; use OC\Config\UserConfig; +use OCP\IConfig; use OCP\IDBConnection; use OCP\Security\ICrypto; use Psr\Log\LoggerInterface; @@ -26,6 +27,7 @@ use Test\TestCase; */ class UserConfigTest extends TestCase { protected IDBConnection $connection; + private IConfig $config; private LoggerInterface $logger; private ICrypto $crypto; private array $originalPreferences; @@ -169,6 +171,7 @@ class UserConfigTest extends TestCase { parent::setUp(); $this->connection = \OCP\Server::get(IDBConnection::class); + $this->config = \OCP\Server::get(IConfig::class); $this->logger = \OCP\Server::get(LoggerInterface::class); $this->crypto = \OCP\Server::get(ICrypto::class); @@ -277,6 +280,7 @@ class UserConfigTest extends TestCase { private function generateUserConfig(array $preLoading = []): IUserConfig { $userConfig = new \OC\Config\UserConfig( $this->connection, + $this->config, $this->logger, $this->crypto, ); |