Skip to content

Commit d7f9a2d

Browse files
committed
feat(lexicon): validate value on set
Signed-off-by: Maxence Lange <[email protected]>
1 parent 92d9ca6 commit d7f9a2d

File tree

4 files changed

+63
-10
lines changed

4 files changed

+63
-10
lines changed

lib/private/AppConfig.php

Lines changed: 9 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,17 @@ private function setTypedValue(
795797
int $type,
796798
): bool {
797799
$this->assertParams($app, $key);
798-
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type)) {
800+
$lexiconEntry = null;
801+
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, lexiconEntry: $lexiconEntry)) {
799802
return false; // returns false as database is not updated
800803
}
801804
$this->loadConfig(null, $lazy ?? true);
802805

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

lib/private/Config/UserConfig.php

Lines changed: 18 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,18 @@ private function setTypedValue(
11001102
}
11011103

11021104
$this->assertParams($userId, $app, $key);
1103-
if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags)) {
1105+
$lexiconEntry = null;
1106+
if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags, lexiconEntry: $lexiconEntry)) {
11041107
// returns false as database is not updated
11051108
return false;
11061109
}
11071110
$this->loadConfig($userId, $lazy);
11081111

1112+
// lexicon entry might have requested a check on the value
1113+
if ($lexiconEntry?->onSetConfirm() !== null && !$lexiconEntry->onSetConfirm()($value)) {
1114+
return false;
1115+
}
1116+
11091117
$inserted = $refreshCache = false;
11101118
$origValue = $value;
11111119
$sensitive = $this->isFlagged(self::FLAG_SENSITIVE, $flags);
@@ -1937,6 +1945,7 @@ private function matchAndApplyLexiconDefinition(
19371945
ValueType &$type = ValueType::MIXED,
19381946
int &$flags = 0,
19391947
?string &$default = null,
1948+
?Entry &$lexiconEntry = null,
19401949
): bool {
19411950
$configDetails = $this->getConfigDetailsFromLexicon($app);
19421951
if (array_key_exists($key, $configDetails['aliases']) && !$this->ignoreLexiconAliases) {
@@ -1953,18 +1962,18 @@ private function matchAndApplyLexiconDefinition(
19531962
return true;
19541963
}
19551964

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

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

@@ -1976,7 +1985,7 @@ private function matchAndApplyLexiconDefinition(
19761985

19771986
// only look for default if needed, default from Lexicon got priority if not overwritten by admin
19781987
if ($default !== null) {
1979-
$default = $this->getSystemDefault($app, $configValue) ?? $configValue->getDefault($this->presetManager->getLexiconPreset()) ?? $default;
1988+
$default = $this->getSystemDefault($app, $lexiconEntry) ?? $lexiconEntry->getDefault($this->presetManager->getLexiconPreset()) ?? $default;
19801989
}
19811990

19821991
// 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: 20 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,22 @@ 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 Callable|null
211+
* @throws UnacceptableValueException
212+
* @since 33.0.0
213+
*/
214+
public function onSetConfirmation(): ?Callable {
215+
return $this->onSetConfirm;
216+
}
217+
198218
/**
199219
* returns if config key is set as lazy
200220
*

0 commit comments

Comments
 (0)