aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMaxence Lange <maxence@artificial-owl.com>2025-06-20 15:52:56 +0200
committerMaxence Lange <maxence@artificial-owl.com>2025-06-25 19:46:53 +0200
commitba11c713c05b66fc9466844b305c33bec67e33c9 (patch)
treea52f41f650a891ab4c73e806266d1562c4a6cfb5
parent0f94ceace12e96f2ffffba029cdb3d69dea46e14 (diff)
downloadnextcloud-server-feat/noid/lexicon-events.tar.gz
nextcloud-server-feat/noid/lexicon-events.zip
feat(lexicon): events onSet() and initialize()feat/noid/lexicon-events
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
-rw-r--r--apps/settings/composer/composer/autoload_classmap.php1
-rw-r--r--apps/settings/composer/composer/autoload_static.php1
-rw-r--r--apps/settings/lib/AppInfo/Application.php4
-rw-r--r--apps/settings/lib/ConfigLexicon.php42
-rw-r--r--lib/private/AppConfig.php34
-rw-r--r--lib/private/Config/UserConfig.php41
-rw-r--r--lib/unstable/Config/Lexicon/ConfigLexiconEntry.php45
-rw-r--r--tests/lib/Config/LexiconTest.php29
-rw-r--r--tests/lib/Config/TestConfigLexicon_I.php49
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';
+ }
+ ),
];
}