Skip to content

Commit a9488d7

Browse files
committed
feat(lexicon): validate value on set
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
1 parent 929e165 commit a9488d7

File tree

6 files changed

+66
-10
lines changed

6 files changed

+66
-10
lines changed

lib/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@
286286
'OCP\\Config\\BeforePreferenceSetEvent' => $baseDir . '/lib/public/Config/BeforePreferenceSetEvent.php',
287287
'OCP\\Config\\Exceptions\\IncorrectTypeException' => $baseDir . '/lib/public/Config/Exceptions/IncorrectTypeException.php',
288288
'OCP\\Config\\Exceptions\\TypeConflictException' => $baseDir . '/lib/public/Config/Exceptions/TypeConflictException.php',
289+
'OCP\\Config\\Exceptions\\UnacceptableValueException' => $baseDir . '/lib/public/Config/Exceptions/UnacceptableValueException.php',
289290
'OCP\\Config\\Exceptions\\UnknownKeyException' => $baseDir . '/lib/public/Config/Exceptions/UnknownKeyException.php',
290291
'OCP\\Config\\IUserConfig' => $baseDir . '/lib/public/Config/IUserConfig.php',
291292
'OCP\\Config\\Lexicon\\Entry' => $baseDir . '/lib/public/Config/Lexicon/Entry.php',

lib/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
327327
'OCP\\Config\\BeforePreferenceSetEvent' => __DIR__ . '/../../..' . '/lib/public/Config/BeforePreferenceSetEvent.php',
328328
'OCP\\Config\\Exceptions\\IncorrectTypeException' => __DIR__ . '/../../..' . '/lib/public/Config/Exceptions/IncorrectTypeException.php',
329329
'OCP\\Config\\Exceptions\\TypeConflictException' => __DIR__ . '/../../..' . '/lib/public/Config/Exceptions/TypeConflictException.php',
330+
'OCP\\Config\\Exceptions\\UnacceptableValueException' => __DIR__ . '/../../..' . '/lib/public/Config/Exceptions/UnacceptableValueException.php',
330331
'OCP\\Config\\Exceptions\\UnknownKeyException' => __DIR__ . '/../../..' . '/lib/public/Config/Exceptions/UnknownKeyException.php',
331332
'OCP\\Config\\IUserConfig' => __DIR__ . '/../../..' . '/lib/public/Config/IUserConfig.php',
332333
'OCP\\Config\\Lexicon\\Entry' => __DIR__ . '/../../..' . '/lib/public/Config/Lexicon/Entry.php',

lib/private/AppConfig.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OC\Config\ConfigManager;
1616
use OC\Config\PresetManager;
1717
use OC\Memcache\Factory as CacheFactory;
18+
use OCP\Config\Exceptions\UnacceptableValueException;
1819
use OCP\Config\Lexicon\Entry;
1920
use OCP\Config\Lexicon\Strictness;
2021
use OCP\Config\ValueType;
@@ -784,6 +785,7 @@ public function setValueArray(
784785
* @param int $type value type {@see VALUE_STRING} {@see VALUE_INT} {@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
785786
*
786787
* @return bool TRUE if value was updated in database
788+
* @throws UnacceptableValueException if lexicon does not validate value
787789
* @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
788790
* @see IAppConfig for explanation about lazy loading
789791
*/
@@ -795,11 +797,18 @@ private function setTypedValue(
795797
int $type,
796798
): bool {
797799
$this->assertParams($app, $key);
798-
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type)) {
800+
/** @var ?Entry $lexiconEntry */
801+
$lexiconEntry = null;
802+
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, lexiconEntry: $lexiconEntry)) {
799803
return false; // returns false as database is not updated
800804
}
801805
$this->loadConfig(null, $lazy ?? true);
802806

807+
// lexicon entry might have requested a check on the value
808+
if ($lexiconEntry?->onSetConfirmation() !== null && !$lexiconEntry->onSetConfirmation()($value)) {
809+
return false;
810+
}
811+
803812
$sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type);
804813
$inserted = $refreshCache = false;
805814

lib/private/Config/UserConfig.php

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use OC\AppFramework\Bootstrap\Coordinator;
1515
use OCP\Config\Exceptions\IncorrectTypeException;
1616
use OCP\Config\Exceptions\TypeConflictException;
17+
use OCP\Config\Exceptions\UnacceptableValueException;
1718
use OCP\Config\Exceptions\UnknownKeyException;
1819
use OCP\Config\IUserConfig;
1920
use OCP\Config\Lexicon\Entry;
@@ -1082,6 +1083,7 @@ public function setValueArray(
10821083
* @param ValueType $type value type
10831084
*
10841085
* @return bool TRUE if value was updated in database
1086+
* @throws UnacceptableValueException if lexicon does not validate value
10851087
* @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
10861088
* @see IUserConfig for explanation about lazy loading
10871089
*/
@@ -1100,12 +1102,19 @@ private function setTypedValue(
11001102
}
11011103

11021104
$this->assertParams($userId, $app, $key);
1103-
if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags)) {
1105+
/** @var ?Entry $lexiconEntry */
1106+
$lexiconEntry = null;
1107+
if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags, lexiconEntry: $lexiconEntry)) {
11041108
// returns false as database is not updated
11051109
return false;
11061110
}
11071111
$this->loadConfig($userId, $lazy);
11081112

1113+
// lexicon entry might have requested a check on the value
1114+
if ($lexiconEntry?->onSetConfirmation() !== null && !$lexiconEntry->onSetConfirmation()($value)) {
1115+
return false;
1116+
}
1117+
11091118
$inserted = $refreshCache = false;
11101119
$origValue = $value;
11111120
$sensitive = $this->isFlagged(self::FLAG_SENSITIVE, $flags);
@@ -1937,6 +1946,7 @@ private function matchAndApplyLexiconDefinition(
19371946
ValueType &$type = ValueType::MIXED,
19381947
int &$flags = 0,
19391948
?string &$default = null,
1949+
?Entry &$lexiconEntry = null,
19401950
): bool {
19411951
$configDetails = $this->getConfigDetailsFromLexicon($app);
19421952
if (array_key_exists($key, $configDetails['aliases']) && !$this->ignoreLexiconAliases) {
@@ -1953,18 +1963,18 @@ private function matchAndApplyLexiconDefinition(
19531963
return true;
19541964
}
19551965

1956-
/** @var Entry $configValue */
1957-
$configValue = $configDetails['entries'][$key];
1966+
/** @var Entry $lexiconEntry */
1967+
$lexiconEntry = $configDetails['entries'][$key];
19581968
if ($type === ValueType::MIXED) {
19591969
// we overwrite if value was requested as mixed
1960-
$type = $configValue->getValueType();
1961-
} elseif ($configValue->getValueType() !== $type) {
1970+
$type = $lexiconEntry->getValueType();
1971+
} elseif ($lexiconEntry->getValueType() !== $type) {
19621972
throw new TypeConflictException('The user config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
19631973
}
19641974

1965-
$lazy = $configValue->isLazy();
1966-
$flags = $configValue->getFlags();
1967-
if ($configValue->isDeprecated()) {
1975+
$lazy = $lexiconEntry->isLazy();
1976+
$flags = $lexiconEntry->getFlags();
1977+
if ($lexiconEntry->isDeprecated()) {
19681978
$this->logger->notice('User config key ' . $app . '/' . $key . ' is set as deprecated.');
19691979
}
19701980

@@ -1976,7 +1986,7 @@ private function matchAndApplyLexiconDefinition(
19761986

19771987
// only look for default if needed, default from Lexicon got priority if not overwritten by admin
19781988
if ($default !== null) {
1979-
$default = $this->getSystemDefault($app, $configValue) ?? $configValue->getDefault($this->presetManager->getLexiconPreset()) ?? $default;
1989+
$default = $this->getSystemDefault($app, $lexiconEntry) ?? $lexiconEntry->getDefault($this->presetManager->getLexiconPreset()) ?? $default;
19801990
}
19811991

19821992
// returning false will make get() returning $default and set() not changing value in database
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCP\Config\Exceptions;
10+
11+
use Exception;
12+
use OCP\AppFramework\Attribute\Throwable;
13+
14+
#[Throwable(since: '33.0.0')]
15+
class UnacceptableValueException extends Exception {
16+
}

lib/public/Config/Lexicon/Entry.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
use Closure;
1212
use OCP\AppFramework\Attribute\Consumable;
13+
use OCP\Config\Exceptions\UnacceptableValueException;
1314
use OCP\Config\ValueType;
1415

1516
/**
@@ -35,8 +36,10 @@ class Entry {
3536
* @param string|null $rename source in case of a rename of a config key.
3637
* @param int $options additional bitflag options {@see self::RENAME_INVERT_BOOLEAN}
3738
* @param string $note additional note and warning related to the use of the config key.
39+
* @param Closure|null $onSetConfirm callback to be called when a config value is set.
3840
*
3941
* @since 32.0.0
42+
* @since 33.0.0 added $onSetConfirm
4043
* @psalm-suppress PossiblyInvalidCast
4144
* @psalm-suppress RiskyCast
4245
*/
@@ -51,6 +54,7 @@ public function __construct(
5154
private readonly ?string $rename = null,
5255
private readonly int $options = 0,
5356
private readonly string $note = '',
57+
private readonly ?Closure $onSetConfirm = null,
5458
) {
5559
// key can only contain alphanumeric chars and underscore "_"
5660
if (preg_match('/[^[:alnum:]_]/', $key)) {
@@ -195,6 +199,21 @@ public function getNote(): string {
195199
return $this->note;
196200
}
197201

202+
/**
203+
* Returns an optional callback to be called when a config value is set.
204+
* If not null, the callback will be called before the config value is set.
205+
* Callable must accept a typed (as defined using {@see ValueType}) parameter
206+
* containing the new value.
207+
* Callback must return a boolean indicating if the set operation should be allowed.
208+
* Callback should throw {@see UnacceptableValueException} to indicate a specific error message.
209+
*
210+
* @return Closure|null
211+
* @since 33.0.0
212+
*/
213+
public function onSetConfirmation(): ?Closure {
214+
return $this->onSetConfirm;
215+
}
216+
198217
/**
199218
* returns if config key is set as lazy
200219
*

0 commit comments

Comments
 (0)