From d52b218c1e9dabe8f7278551f34bce0d6a4aea1a Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Mon, 26 Jan 2026 18:14:19 -0100 Subject: [PATCH 1/2] feat(lexicon): validate value on set Signed-off-by: Maxence Lange --- lib/private/AppConfig.php | 10 +++++++++- lib/private/Config/UserConfig.php | 27 ++++++++++++++++++--------- lib/public/Config/Lexicon/Entry.php | 17 +++++++++++++++++ 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index be701f3da53d8..b59cf4cb9fda0 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -795,11 +795,19 @@ private function setTypedValue( int $type, ): bool { $this->assertParams($app, $key); - if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type)) { + /** @var ?Entry $lexiconEntry */ + $lexiconEntry = null; + if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, lexiconEntry: $lexiconEntry)) { return false; // returns false as database is not updated } $this->loadConfig(null, $lazy ?? true); + // lexicon entry might have requested a check on the value + $confirmationClosure = $lexiconEntry?->onSetConfirmation(); + if ($confirmationClosure !== null && !$confirmationClosure($value)) { + return false; + } + $sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type); $inserted = $refreshCache = false; diff --git a/lib/private/Config/UserConfig.php b/lib/private/Config/UserConfig.php index f7d135c134d7b..202e97d7aa17c 100644 --- a/lib/private/Config/UserConfig.php +++ b/lib/private/Config/UserConfig.php @@ -1100,12 +1100,20 @@ private function setTypedValue( } $this->assertParams($userId, $app, $key); - if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags)) { + /** @var ?Entry $lexiconEntry */ + $lexiconEntry = null; + if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags, lexiconEntry: $lexiconEntry)) { // returns false as database is not updated return false; } $this->loadConfig($userId, $lazy); + // lexicon entry might have requested a check on the value + $confirmationClosure = $lexiconEntry?->onSetConfirmation(); + if ($confirmationClosure !== null && !$confirmationClosure($value)) { + return false; + } + $inserted = $refreshCache = false; $origValue = $value; $sensitive = $this->isFlagged(self::FLAG_SENSITIVE, $flags); @@ -1937,6 +1945,7 @@ private function matchAndApplyLexiconDefinition( ValueType &$type = ValueType::MIXED, int &$flags = 0, ?string &$default = null, + ?Entry &$lexiconEntry = null, ): bool { $configDetails = $this->getConfigDetailsFromLexicon($app); if (array_key_exists($key, $configDetails['aliases']) && !$this->ignoreLexiconAliases) { @@ -1953,18 +1962,18 @@ private function matchAndApplyLexiconDefinition( return true; } - /** @var Entry $configValue */ - $configValue = $configDetails['entries'][$key]; + /** @var Entry $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.'); } @@ -1976,7 +1985,7 @@ private function matchAndApplyLexiconDefinition( // only look for default if needed, default from Lexicon got priority if not overwritten by admin if ($default !== null) { - $default = $this->getSystemDefault($app, $configValue) ?? $configValue->getDefault($this->presetManager->getLexiconPreset()) ?? $default; + $default = $this->getSystemDefault($app, $lexiconEntry) ?? $lexiconEntry->getDefault($this->presetManager->getLexiconPreset()) ?? $default; } // returning false will make get() returning $default and set() not changing value in database diff --git a/lib/public/Config/Lexicon/Entry.php b/lib/public/Config/Lexicon/Entry.php index aa35730c4f1c2..5dc71858c1592 100644 --- a/lib/public/Config/Lexicon/Entry.php +++ b/lib/public/Config/Lexicon/Entry.php @@ -35,8 +35,10 @@ class Entry { * @param string|null $rename source in case of a rename of a config key. * @param int $options additional bitflag options {@see self::RENAME_INVERT_BOOLEAN} * @param string $note additional note and warning related to the use of the config key. + * @param Closure|null $onSetConfirm callback to be called when a config value is set. {@see onSetConfirmation()} for more details. * * @since 32.0.0 + * @since 34.0.0 added $onSetConfirm * @psalm-suppress PossiblyInvalidCast * @psalm-suppress RiskyCast */ @@ -51,6 +53,7 @@ public function __construct( private readonly ?string $rename = null, private readonly int $options = 0, private readonly string $note = '', + private readonly ?Closure $onSetConfirm = null, ) { // key can only contain alphanumeric chars and underscore "_" if (preg_match('/[^[:alnum:]_]/', $key)) { @@ -195,6 +198,20 @@ public function getNote(): string { return $this->note; } + /** + * Returns an optional callback to be called when a config value is set. + * If not null, the callback will be called before the config value is set. + * Callable must accept a string-typed parameter containing the new value. + * String-typed parameter can be referenced and modified to a new value from the Callable. + * Callback must return a boolean indicating if the set operation should be allowed. + * + * @return (Closure(string $value): bool)|null + * @since 34.0.0 + */ + public function onSetConfirmation(): ?Closure { + return $this->onSetConfirm; + } + /** * returns if config key is set as lazy * From edff996188cc0806ed68102eda0b10e4eca4c976 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Thu, 29 Jan 2026 16:47:36 -0100 Subject: [PATCH 2/2] feat(lexicon): improve phpdoc Co-authored-by: Kate <26026535+provokateurin@users.noreply.github.com> Signed-off-by: Maxence Lange --- lib/public/Config/Lexicon/Entry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/public/Config/Lexicon/Entry.php b/lib/public/Config/Lexicon/Entry.php index 5dc71858c1592..c768e785d3d46 100644 --- a/lib/public/Config/Lexicon/Entry.php +++ b/lib/public/Config/Lexicon/Entry.php @@ -35,7 +35,7 @@ class Entry { * @param string|null $rename source in case of a rename of a config key. * @param int $options additional bitflag options {@see self::RENAME_INVERT_BOOLEAN} * @param string $note additional note and warning related to the use of the config key. - * @param Closure|null $onSetConfirm callback to be called when a config value is set. {@see onSetConfirmation()} for more details. + * @param (Closure(string $value): bool)|null $onSetConfirm callback to be called when a config value is set. {@see onSetConfirmation()} for more details. * * @since 32.0.0 * @since 34.0.0 added $onSetConfirm