aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMaxence Lange <maxence@artificial-owl.com>2025-01-14 15:12:36 -0100
committerGitHub <noreply@github.com>2025-01-14 15:12:36 -0100
commit93a64d67b58c4def5281c62e97760ae191fd664e (patch)
tree9d2d6857464e5048b21f6d471aa8de5942a5d801
parentf44f122456205975f3080028b13a7151e815d6bc (diff)
parent28acc002a2e4b1e54fb3a3c68af504166f89283c (diff)
downloadnextcloud-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.php1
-rw-r--r--lib/composer/composer/autoload_static.php1
-rw-r--r--lib/private/AppFramework/Bootstrap/RegistrationContext.php3
-rw-r--r--lib/private/Config/Lexicon/CoreConfigLexicon.php43
-rw-r--r--lib/private/Config/UserConfig.php77
-rw-r--r--lib/unstable/Config/Lexicon/ConfigLexiconEntry.php65
-rw-r--r--tests/lib/Config/UserConfigTest.php4
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,
);