Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/settings/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'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',
Expand Down
1 change: 1 addition & 0 deletions apps/settings/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions apps/settings/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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;
Expand Down Expand Up @@ -129,6 +130,9 @@ public function register(IRegistrationContext $context): void {
// Register Settings Form(s)
$context->registerDeclarativeSettings(MailProvider::class);

// Register Config Lexicon
$context->registerConfigLexicon(ConfigLexicon::class);

/**
* Core class wrappers
*/
Expand Down
42 changes: 42 additions & 0 deletions apps/settings/lib/ConfigLexicon.php
Original file line number Diff line number Diff line change
@@ -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); }),
];
}
}
27 changes: 18 additions & 9 deletions lib/private/AppConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,8 @@ private function getTypedValue(
): 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);
Expand Down Expand Up @@ -748,11 +749,18 @@ private function setTypedValue(
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;

Expand Down Expand Up @@ -1593,6 +1601,7 @@ private function matchAndApplyLexiconDefinition(
?bool &$lazy = null,
int &$type = self::VALUE_MIXED,
string &$default = '',
?ConfigLexiconEntry &$lexiconEntry = null,
): bool {
if (in_array($key,
[
Expand All @@ -1617,23 +1626,23 @@ private function matchAndApplyLexiconDefinition(
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.');
}

Expand Down
34 changes: 19 additions & 15 deletions lib/private/Config/UserConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,8 @@ private function getTypedValue(
): 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;
}
Expand Down Expand Up @@ -1067,18 +1068,20 @@ private function setTypedValue(
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);
Expand Down Expand Up @@ -1887,6 +1890,7 @@ private function matchAndApplyLexiconDefinition(
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) {
Expand All @@ -1903,18 +1907,18 @@ private function matchAndApplyLexiconDefinition(
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.');
}

Expand All @@ -1925,7 +1929,7 @@ private function matchAndApplyLexiconDefinition(
}

// 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;
Expand Down
23 changes: 23 additions & 0 deletions lib/unstable/Config/Lexicon/ConfigLexiconEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace NCU\Config\Lexicon;

use Closure;
use NCU\Config\ValueType;

/**
Expand All @@ -22,6 +23,7 @@ class ConfigLexiconEntry {

private string $definition = '';
private ?string $default = null;
private ?Closure $onSet = null;

/**
* @param string $key config key
Expand Down Expand Up @@ -136,6 +138,27 @@ public function getDefault(): ?string {
return $this->default;
}

/**
* 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
*
Expand Down
20 changes: 20 additions & 0 deletions tests/lib/Config/LexiconTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,24 @@ public function testAppConfigLexiconRenameInvertBoolean() {
$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'));
}
}
34 changes: 32 additions & 2 deletions tests/lib/Config/TestConfigLexicon_I.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,44 @@ public function getAppConfigs(): array {
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('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;
}
),
];
}

Expand Down
Loading