Skip to content

Commit 93a64d6

Browse files
Merge pull request #49848 from nextcloud/feat/noid/lexicon-configurable-default-value
feat(lexicon): configurable default value
2 parents f44f122 + 28acc00 commit 93a64d6

7 files changed

Lines changed: 157 additions & 37 deletions

File tree

lib/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,7 @@
11571157
'OC\\Comments\\Manager' => $baseDir . '/lib/private/Comments/Manager.php',
11581158
'OC\\Comments\\ManagerFactory' => $baseDir . '/lib/private/Comments/ManagerFactory.php',
11591159
'OC\\Config' => $baseDir . '/lib/private/Config.php',
1160+
'OC\\Config\\Lexicon\\CoreConfigLexicon' => $baseDir . '/lib/private/Config/Lexicon/CoreConfigLexicon.php',
11601161
'OC\\Config\\UserConfig' => $baseDir . '/lib/private/Config/UserConfig.php',
11611162
'OC\\Console\\Application' => $baseDir . '/lib/private/Console/Application.php',
11621163
'OC\\Console\\TimestampFormatter' => $baseDir . '/lib/private/Console/TimestampFormatter.php',

lib/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
11981198
'OC\\Comments\\Manager' => __DIR__ . '/../../..' . '/lib/private/Comments/Manager.php',
11991199
'OC\\Comments\\ManagerFactory' => __DIR__ . '/../../..' . '/lib/private/Comments/ManagerFactory.php',
12001200
'OC\\Config' => __DIR__ . '/../../..' . '/lib/private/Config.php',
1201+
'OC\\Config\\Lexicon\\CoreConfigLexicon' => __DIR__ . '/../../..' . '/lib/private/Config/Lexicon/CoreConfigLexicon.php',
12011202
'OC\\Config\\UserConfig' => __DIR__ . '/../../..' . '/lib/private/Config/UserConfig.php',
12021203
'OC\\Console\\Application' => __DIR__ . '/../../..' . '/lib/private/Console/Application.php',
12031204
'OC\\Console\\TimestampFormatter' => __DIR__ . '/../../..' . '/lib/private/Console/TimestampFormatter.php',

lib/private/AppFramework/Bootstrap/RegistrationContext.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
use Closure;
1313
use NCU\Config\Lexicon\IConfigLexicon;
14+
use OC\Config\Lexicon\CoreConfigLexicon;
1415
use OC\Support\CrashReport\Registry;
1516
use OCP\AppFramework\App;
1617
use OCP\AppFramework\Bootstrap\IRegistrationContext;
@@ -143,7 +144,7 @@ class RegistrationContext {
143144
private array $declarativeSettings = [];
144145

145146
/** @var array<array-key, string> */
146-
private array $configLexiconClasses = [];
147+
private array $configLexiconClasses = ['core' => CoreConfigLexicon::class];
147148

148149
/** @var ServiceRegistration<ITeamResourceProvider>[] */
149150
private array $teamResourceProviders = [];
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OC\Config\Lexicon;
10+
11+
use NCU\Config\Lexicon\ConfigLexiconEntry;
12+
use NCU\Config\Lexicon\ConfigLexiconStrictness;
13+
use NCU\Config\Lexicon\IConfigLexicon;
14+
use NCU\Config\ValueType;
15+
16+
/**
17+
* ConfigLexicon for 'core' app/user configs
18+
*/
19+
class CoreConfigLexicon implements IConfigLexicon {
20+
public function getStrictness(): ConfigLexiconStrictness {
21+
return ConfigLexiconStrictness::IGNORE;
22+
}
23+
24+
/**
25+
* @inheritDoc
26+
* @return ConfigLexiconEntry[]
27+
*/
28+
public function getAppConfigs(): array {
29+
return [
30+
new ConfigLexiconEntry('lastcron', ValueType::INT, 0, 'timestamp of last cron execution'),
31+
];
32+
}
33+
34+
/**
35+
* @inheritDoc
36+
* @return ConfigLexiconEntry[]
37+
*/
38+
public function getUserConfigs(): array {
39+
return [
40+
new ConfigLexiconEntry('lang', ValueType::STRING, null, 'language'),
41+
];
42+
}
43+
}

lib/private/Config/UserConfig.php

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class UserConfig implements IUserConfig {
7171

7272
public function __construct(
7373
protected IDBConnection $connection,
74+
protected IConfig $config,
7475
protected LoggerInterface $logger,
7576
protected ICrypto $crypto,
7677
) {
@@ -228,7 +229,8 @@ public function isLazy(string $userId, string $app, string $key): bool {
228229
// there is a huge probability the non-lazy config are already loaded
229230
// meaning that we can start by only checking if a current non-lazy key exists
230231
if ($this->hasKey($userId, $app, $key, false)) {
231-
return false; // meaning key is not lazy.
232+
// meaning key is not lazy.
233+
return false;
232234
}
233235

234236
// as key is not found as non-lazy, we load and search in the lazy config
@@ -263,7 +265,8 @@ public function getValues(
263265
$values = array_filter(
264266
$this->formatAppValues($userId, $app, ($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? []), $filtered),
265267
function (string $key) use ($prefix): bool {
266-
return str_starts_with($key, $prefix); // filter values based on $prefix
268+
// filter values based on $prefix
269+
return str_starts_with($key, $prefix);
267270
}, ARRAY_FILTER_USE_KEY
268271
);
269272

@@ -711,8 +714,9 @@ private function getTypedValue(
711714
ValueType $type,
712715
): string {
713716
$this->assertParams($userId, $app, $key);
714-
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, default: $default)) {
715-
return $default; // returns default if strictness of lexicon is set to WARNING (block and report)
717+
if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, default: $default)) {
718+
// returns default if strictness of lexicon is set to WARNING (block and report)
719+
return $default;
716720
}
717721
$this->loadConfig($userId, $lazy);
718722

@@ -1046,8 +1050,9 @@ private function setTypedValue(
10461050
ValueType $type,
10471051
): bool {
10481052
$this->assertParams($userId, $app, $key);
1049-
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $flags)) {
1050-
return false; // returns false as database is not updated
1053+
if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags)) {
1054+
// returns false as database is not updated
1055+
return false;
10511056
}
10521057
$this->loadConfig($userId, $lazy);
10531058

@@ -1100,7 +1105,8 @@ private function setTypedValue(
11001105
$inserted = true;
11011106
} catch (DBException $e) {
11021107
if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
1103-
throw $e; // TODO: throw exception or just log and returns false !?
1108+
// TODO: throw exception or just log and returns false !?
1109+
throw $e;
11041110
}
11051111
}
11061112
}
@@ -1195,7 +1201,8 @@ private function setTypedValue(
11951201
public function updateType(string $userId, string $app, string $key, ValueType $type = ValueType::MIXED): bool {
11961202
$this->assertParams($userId, $app, $key);
11971203
$this->loadConfigAll($userId);
1198-
$this->isLazy($userId, $app, $key); // confirm key exists
1204+
// confirm key exists
1205+
$this->isLazy($userId, $app, $key);
11991206

12001207
$update = $this->connection->getQueryBuilder();
12011208
$update->update('preferences')
@@ -1287,7 +1294,8 @@ public function updateGlobalSensitive(string $app, string $key, bool $sensitive)
12871294
}
12881295
}
12891296

1290-
$this->clearCacheAll(); // we clear all cache
1297+
// we clear all cache
1298+
$this->clearCacheAll();
12911299
}
12921300

12931301
/**
@@ -1370,7 +1378,8 @@ public function updateGlobalIndexed(string $app, string $key, bool $indexed): vo
13701378
}
13711379
}
13721380

1373-
$this->clearCacheAll(); // we clear all cache
1381+
// we clear all cache
1382+
$this->clearCacheAll();
13741383
}
13751384

13761385
/**
@@ -1793,6 +1802,14 @@ private function convertTypedValue(string $value, ValueType $type): string|int|f
17931802
}
17941803

17951804

1805+
/**
1806+
* will change referenced $value with the decrypted value in case of encrypted (sensitive value)
1807+
*
1808+
* @param string $userId
1809+
* @param string $app
1810+
* @param string $key
1811+
* @param string $value
1812+
*/
17961813
private function decryptSensitiveValue(string $userId, string $app, string $key, string &$value): void {
17971814
if (!$this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags'] ?? 0)) {
17981815
return;
@@ -1820,8 +1837,10 @@ private function decryptSensitiveValue(string $userId, string $app, string $key,
18201837
*
18211838
* @throws UnknownKeyException
18221839
* @throws TypeConflictException
1840+
* @return bool FALSE if conflict with defined lexicon were observed in the process
18231841
*/
18241842
private function matchAndApplyLexiconDefinition(
1843+
string $userId,
18251844
string $app,
18261845
string $key,
18271846
bool &$lazy,
@@ -1837,20 +1856,50 @@ private function matchAndApplyLexiconDefinition(
18371856
/** @var ConfigLexiconEntry $configValue */
18381857
$configValue = $configDetails['entries'][$key];
18391858
if ($type === ValueType::MIXED) {
1840-
$type = $configValue->getValueType(); // we overwrite if value was requested as mixed
1859+
// we overwrite if value was requested as mixed
1860+
$type = $configValue->getValueType();
18411861
} elseif ($configValue->getValueType() !== $type) {
18421862
throw new TypeConflictException('The user config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
18431863
}
18441864

18451865
$lazy = $configValue->isLazy();
1846-
$default = $configValue->getDefault() ?? $default; // default from Lexicon got priority
18471866
$flags = $configValue->getFlags();
1848-
18491867
if ($configValue->isDeprecated()) {
18501868
$this->logger->notice('User config key ' . $app . '/' . $key . ' is set as deprecated.');
18511869
}
18521870

1853-
return true;
1871+
$enforcedValue = $this->config->getSystemValue('lexicon.default.userconfig.enforced', [])[$app][$key] ?? false;
1872+
if (!$enforcedValue && $this->hasKey($userId, $app, $key, $lazy)) {
1873+
// if key exists there should be no need to extract default
1874+
return true;
1875+
}
1876+
1877+
// default from Lexicon got priority but it can still be overwritten by admin
1878+
$default = $this->getSystemDefault($app, $configValue) ?? $configValue->getDefault() ?? $default;
1879+
1880+
// returning false will make get() returning $default and set() not changing value in database
1881+
return !$enforcedValue;
1882+
}
1883+
1884+
/**
1885+
* get default value set in config/config.php if stored in key:
1886+
*
1887+
* 'lexicon.default.userconfig' => [
1888+
* <appId> => [
1889+
* <configKey> => 'my value',
1890+
* ]
1891+
* ],
1892+
*
1893+
* The entry is converted to string to fit the expected type when managing default value
1894+
*/
1895+
private function getSystemDefault(string $appId, ConfigLexiconEntry $configValue): ?string {
1896+
$default = $this->config->getSystemValue('lexicon.default.userconfig', [])[$appId][$configValue->getKey()] ?? null;
1897+
if ($default === null) {
1898+
// no system default, using default default.
1899+
return null;
1900+
}
1901+
1902+
return $configValue->convertToString($default);
18541903
}
18551904

18561905
/**

lib/unstable/Config/Lexicon/ConfigLexiconEntry.php

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
declare(strict_types=1);
44
/**
55
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
6-
* SPDX-License-Identifier: AGPL-3.0-only
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
77
*/
88

99
namespace NCU\Config\Lexicon;
@@ -35,33 +35,20 @@ class ConfigLexiconEntry {
3535
public function __construct(
3636
private readonly string $key,
3737
private readonly ValueType $type,
38-
null|string|int|float|bool|array $default = null,
38+
private null|string|int|float|bool|array $defaultRaw = null,
3939
string $definition = '',
4040
private readonly bool $lazy = false,
4141
private readonly int $flags = 0,
4242
private readonly bool $deprecated = false,
4343
) {
44-
if ($default !== null) {
45-
// in case $default is array but is not expected to be an array...
46-
$default = ($type !== ValueType::ARRAY && is_array($default)) ? json_encode($default) : $default;
47-
$this->default = match ($type) {
48-
ValueType::MIXED => (string)$default,
49-
ValueType::STRING => $this->convertFromString((string)$default),
50-
ValueType::INT => $this->convertFromInt((int)$default),
51-
ValueType::FLOAT => $this->convertFromFloat((float)$default),
52-
ValueType::BOOL => $this->convertFromBool((bool)$default),
53-
ValueType::ARRAY => $this->convertFromArray((array)$default)
54-
};
55-
}
56-
5744
/** @psalm-suppress UndefinedClass */
5845
if (\OC::$CLI) { // only store definition if ran from CLI
5946
$this->definition = $definition;
6047
}
6148
}
6249

6350
/**
64-
* @inheritDoc
51+
* returns the config key
6552
*
6653
* @return string config key
6754
* @experimental 31.0.0
@@ -71,7 +58,7 @@ public function getKey(): string {
7158
}
7259

7360
/**
74-
* @inheritDoc
61+
* get expected type for config value
7562
*
7663
* @return ValueType
7764
* @experimental 31.0.0
@@ -126,17 +113,51 @@ private function convertFromArray(array $default): string {
126113
}
127114

128115
/**
129-
* @inheritDoc
116+
* returns default value
130117
*
131118
* @return string|null NULL if no default is set
132119
* @experimental 31.0.0
133120
*/
134121
public function getDefault(): ?string {
122+
if ($this->defaultRaw === null) {
123+
return null;
124+
}
125+
126+
if ($this->default === null) {
127+
$this->default = $this->convertToString($this->defaultRaw);
128+
}
129+
135130
return $this->default;
136131
}
137132

138133
/**
139-
* @inheritDoc
134+
* convert $entry into string, based on the expected type for config value
135+
*
136+
* @param string|int|float|bool|array $entry
137+
*
138+
* @return string
139+
* @experimental 31.0.0
140+
* @psalm-suppress PossiblyInvalidCast arrays are managed pre-cast
141+
* @psalm-suppress RiskyCast
142+
*/
143+
public function convertToString(string|int|float|bool|array $entry): string {
144+
// in case $default is array but is not expected to be an array...
145+
if ($this->getValueType() !== ValueType::ARRAY && is_array($entry)) {
146+
$entry = json_encode($entry, JSON_THROW_ON_ERROR);
147+
}
148+
149+
return match ($this->getValueType()) {
150+
ValueType::MIXED => (string)$entry,
151+
ValueType::STRING => $this->convertFromString((string)$entry),
152+
ValueType::INT => $this->convertFromInt((int)$entry),
153+
ValueType::FLOAT => $this->convertFromFloat((float)$entry),
154+
ValueType::BOOL => $this->convertFromBool((bool)$entry),
155+
ValueType::ARRAY => $this->convertFromArray((array)$entry)
156+
};
157+
}
158+
159+
/**
160+
* returns definition
140161
*
141162
* @return string
142163
* @experimental 31.0.0
@@ -146,7 +167,7 @@ public function getDefinition(): string {
146167
}
147168

148169
/**
149-
* @inheritDoc
170+
* returns if config key is set as lazy
150171
*
151172
* @see IAppConfig for details on lazy config values
152173
* @return bool TRUE if config value is lazy
@@ -157,7 +178,7 @@ public function isLazy(): bool {
157178
}
158179

159180
/**
160-
* @inheritDoc
181+
* returns flags
161182
*
162183
* @see IAppConfig for details on sensitive config values
163184
* @return int bitflag about the config value
@@ -178,7 +199,7 @@ public function isFlagged(int $flag): bool {
178199
}
179200

180201
/**
181-
* @inheritDoc
202+
* returns if config key is set as deprecated
182203
*
183204
* @return bool TRUE if config si deprecated
184205
* @experimental 31.0.0

0 commit comments

Comments
 (0)