Skip to content

Commit

Permalink
feat(lexicon): configurable default value
Browse files Browse the repository at this point in the history
Signed-off-by: Maxence Lange <[email protected]>
  • Loading branch information
ArtificialOwl committed Jan 9, 2025
1 parent d3ec3de commit 824f954
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 28 deletions.
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,7 @@
'OC\\Comments\\Manager' => $baseDir . '/lib/private/Comments/Manager.php',
'OC\\Comments\\ManagerFactory' => $baseDir . '/lib/private/Comments/ManagerFactory.php',
'OC\\Config' => $baseDir . '/lib/private/Config.php',
'OC\\Config\\Lexicon\\CoreConfigLexicon' => $baseDir . '/lib/private/Config/Lexicon/CoreConfigLexicon.php',
'OC\\Config\\UserConfig' => $baseDir . '/lib/private/Config/UserConfig.php',
'OC\\Console\\Application' => $baseDir . '/lib/private/Console/Application.php',
'OC\\Console\\TimestampFormatter' => $baseDir . '/lib/private/Console/TimestampFormatter.php',
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Comments\\Manager' => __DIR__ . '/../../..' . '/lib/private/Comments/Manager.php',
'OC\\Comments\\ManagerFactory' => __DIR__ . '/../../..' . '/lib/private/Comments/ManagerFactory.php',
'OC\\Config' => __DIR__ . '/../../..' . '/lib/private/Config.php',
'OC\\Config\\Lexicon\\CoreConfigLexicon' => __DIR__ . '/../../..' . '/lib/private/Config/Lexicon/CoreConfigLexicon.php',
'OC\\Config\\UserConfig' => __DIR__ . '/../../..' . '/lib/private/Config/UserConfig.php',
'OC\\Console\\Application' => __DIR__ . '/../../..' . '/lib/private/Console/Application.php',
'OC\\Console\\TimestampFormatter' => __DIR__ . '/../../..' . '/lib/private/Console/TimestampFormatter.php',
Expand Down
3 changes: 2 additions & 1 deletion lib/private/AppFramework/Bootstrap/RegistrationContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use Closure;
use NCU\Config\Lexicon\IConfigLexicon;
use OC\Config\Lexicon\CoreConfigLexicon;
use OC\Support\CrashReport\Registry;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
Expand Down Expand Up @@ -143,7 +144,7 @@ class RegistrationContext {
private array $declarativeSettings = [];

/** @var array<array-key, string> */
private array $configLexiconClasses = [];
private array $configLexiconClasses = ['core' => CoreConfigLexicon::class];

/** @var ServiceRegistration<ITeamResourceProvider>[] */
private array $teamResourceProviders = [];
Expand Down
50 changes: 50 additions & 0 deletions lib/private/Config/Lexicon/CoreConfigLexicon.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

namespace OC\Config\Lexicon;

use NCU\Config\Lexicon\ConfigLexiconEntry;
use NCU\Config\Lexicon\ConfigLexiconStrictness;
use NCU\Config\Lexicon\IConfigLexicon;
use NCU\Config\ValueType;

/**
* ConfigLexicon for 'core' app/user configs
*/
class CoreConfigLexicon implements IConfigLexicon {
/**
* @inheritDoc
* @return ConfigLexiconStrictness
* @since 31.0.0
*/
public function getStrictness(): ConfigLexiconStrictness {
return ConfigLexiconStrictness::IGNORE;
}

/**
* @inheritDoc
* @return ConfigLexiconEntry[]
* @since 31.0.0
*/
public function getAppConfigs(): array {
return [
new ConfigLexiconEntry('lastcron', ValueType::INT, 0, 'timestamp of last cron execution'),
];
}

/**
* @inheritDoc
* @return ConfigLexiconEntry[]
* @since 31.0.0
*/
public function getUserConfigs(): array {
return [
new ConfigLexiconEntry('lang', ValueType::STRING, null, 'language'),
];
}
}
48 changes: 42 additions & 6 deletions lib/private/Config/UserConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class UserConfig implements IUserConfig {

public function __construct(
protected IDBConnection $connection,
protected IConfig $config,
protected LoggerInterface $logger,
protected ICrypto $crypto,
) {
Expand Down Expand Up @@ -711,7 +712,7 @@ private function getTypedValue(
ValueType $type,
): string {
$this->assertParams($userId, $app, $key);
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, default: $default)) {
if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, default: $default)) {
return $default; // returns default if strictness of lexicon is set to WARNING (block and report)
}
$this->loadConfig($userId, $lazy);
Expand Down Expand Up @@ -1046,7 +1047,7 @@ private function setTypedValue(
ValueType $type,
): bool {
$this->assertParams($userId, $app, $key);
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $flags)) {
if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags)) {
return false; // returns false as database is not updated
}
$this->loadConfig($userId, $lazy);
Expand Down Expand Up @@ -1822,6 +1823,7 @@ private function decryptSensitiveValue(string $userId, string $app, string $key,
* @throws TypeConflictException
*/
private function matchAndApplyLexiconDefinition(
string $userId,
string $app,
string $key,
bool &$lazy,
Expand All @@ -1837,20 +1839,54 @@ private function matchAndApplyLexiconDefinition(
/** @var ConfigLexiconEntry $configValue */
$configValue = $configDetails['entries'][$key];
if ($type === ValueType::MIXED) {
$type = $configValue->getValueType(); // we overwrite if value was requested as mixed
// we overwrite if value was requested as mixed
$type = $configValue->getValueType();
} elseif ($configValue->getValueType() !== $type) {
throw new TypeConflictException('The user 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
$flags = $configValue->getFlags();

if ($configValue->isDeprecated()) {
$this->logger->notice('User config key ' . $app . '/' . $key . ' is set as deprecated.');
}

return true;
$enforcedValue = $this->config->getSystemValue('lexicon.default.userconfig.enforced', [])[$app][$key] ?? false;
if (!$enforcedValue && $this->hasKey($userId, $app, $key, $lazy)) {
// if key exists there should be no need to extract default
return true;
}

// default from Lexicon got priority but it can still be overwritten by admin
$default = $this->getSystemDefault($app, $configValue) ?? $configValue->getDefault() ?? $default;

// returning false will make get() returning $default and set() not changing value in database
return !$enforcedValue;
}

/**
* get default value set in config/config.php if stored in key:
*
* 'lexicon.default.userconfig' => [
* <appId> => [
* <configKey> => 'my value',
* ]
* ],
*
* The entry is converted to string to fit the expected type when managing default value
*
* @param string $appId
* @param ConfigLexiconEntry $configValue
*
* @return string|null
*/
private function getSystemDefault(string $appId, ConfigLexiconEntry $configValue): ?string {
$default = $this->config->getSystemValue('lexicon.default.userconfig', [])[$appId][$configValue->getKey()] ?? null;
if ($default === null) {
return null; // no system default, using default default.
}

return $configValue->convertToString($default);
}

/**
Expand Down
63 changes: 42 additions & 21 deletions lib/unstable/Config/Lexicon/ConfigLexiconEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,33 +35,20 @@ class ConfigLexiconEntry {
public function __construct(
private readonly string $key,
private readonly ValueType $type,
null|string|int|float|bool|array $default = null,
private null|string|int|float|bool|array $defaultRaw = null,
string $definition = '',
private readonly bool $lazy = false,
private readonly int $flags = 0,
private readonly bool $deprecated = false,
) {
if ($default !== null) {
// in case $default is array but is not expected to be an array...
$default = ($type !== ValueType::ARRAY && is_array($default)) ? json_encode($default) : $default;
$this->default = match ($type) {
ValueType::MIXED => (string)$default,
ValueType::STRING => $this->convertFromString((string)$default),
ValueType::INT => $this->convertFromInt((int)$default),
ValueType::FLOAT => $this->convertFromFloat((float)$default),
ValueType::BOOL => $this->convertFromBool((bool)$default),
ValueType::ARRAY => $this->convertFromArray((array)$default)
};
}

/** @psalm-suppress UndefinedClass */
if (\OC::$CLI) { // only store definition if ran from CLI
$this->definition = $definition;
}
}

/**
* @inheritDoc
* returns the config key
*
* @return string config key
* @experimental 31.0.0
Expand All @@ -71,7 +58,7 @@ public function getKey(): string {
}

/**
* @inheritDoc
* get expected type for config value
*
* @return ValueType
* @experimental 31.0.0
Expand Down Expand Up @@ -126,17 +113,51 @@ private function convertFromArray(array $default): string {
}

/**
* @inheritDoc
* returns default value
*
* @return string|null NULL if no default is set
* @experimental 31.0.0
*/
public function getDefault(): ?string {
if ($this->defaultRaw === null) {
return null;
}

if ($this->default === null) {
$this->default = $this->convertToString($this->defaultRaw);
}

return $this->default;
}

/**
* @inheritDoc
* convert $entry into string, based on the expected type for config value
*
* @param string|int|float|bool|array $entry
*
* @return string
* @experimental 31.0.0
* @psalm-suppress PossiblyInvalidCast arrays are managed pre-cast
* @psalm-suppress RiskyCast
*/
public function convertToString(string|int|float|bool|array $entry): string {
// in case $default is array but is not expected to be an array...
if ($this->getValueType() !== ValueType::ARRAY && is_array($entry)) {
$entry = json_encode($entry, JSON_THROW_ON_ERROR);
}

return match ($this->getValueType()) {
ValueType::MIXED => (string)$entry,
ValueType::STRING => $this->convertFromString((string)$entry),
ValueType::INT => $this->convertFromInt((int)$entry),
ValueType::FLOAT => $this->convertFromFloat((float)$entry),
ValueType::BOOL => $this->convertFromBool((bool)$entry),
ValueType::ARRAY => $this->convertFromArray((array)$entry)
};
}

/**
* returns definition
*
* @return string
* @experimental 31.0.0
Expand All @@ -146,7 +167,7 @@ public function getDefinition(): string {
}

/**
* @inheritDoc
* returns if config key is set as lazy
*
* @see IAppConfig for details on lazy config values
* @return bool TRUE if config value is lazy
Expand All @@ -157,7 +178,7 @@ public function isLazy(): bool {
}

/**
* @inheritDoc
* returns flags
*
* @see IAppConfig for details on sensitive config values
* @return int bitflag about the config value
Expand All @@ -178,7 +199,7 @@ public function isFlagged(int $flag): bool {
}

/**
* @inheritDoc
* returns if config key is set as deprecated
*
* @return bool TRUE if config si deprecated
* @experimental 31.0.0
Expand Down
5 changes: 5 additions & 0 deletions tests/Core/Command/Config/App/GetConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ public function testGet($configName, $value, $configExists, $defaultValue, $hasD
->method('getDetails')
->with('app-name', $configName)
->willReturn(['value' => $value]);
} else {
$this->config->expects($this->once())
->method('getValueMixed')
->with('app-name', $configName, $defaultValue)
->willReturn($defaultValue);
}
}

Expand Down
4 changes: 4 additions & 0 deletions tests/lib/Config/UserConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use NCU\Config\IUserConfig;
use NCU\Config\ValueType;
use OC\Config\UserConfig;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Security\ICrypto;
use Psr\Log\LoggerInterface;
Expand All @@ -26,6 +27,7 @@
*/
class UserConfigTest extends TestCase {
protected IDBConnection $connection;
private IConfig $config;
private LoggerInterface $logger;
private ICrypto $crypto;
private array $originalPreferences;
Expand Down Expand Up @@ -169,6 +171,7 @@ protected function setUp(): void {
parent::setUp();

$this->connection = \OCP\Server::get(IDBConnection::class);
$this->config = \OCP\Server::get(IConfig::class);
$this->logger = \OCP\Server::get(LoggerInterface::class);
$this->crypto = \OCP\Server::get(ICrypto::class);

Expand Down Expand Up @@ -277,6 +280,7 @@ protected function tearDown(): void {
private function generateUserConfig(array $preLoading = []): IUserConfig {
$userConfig = new \OC\Config\UserConfig(
$this->connection,
$this->config,
$this->logger,
$this->crypto,
);
Expand Down

0 comments on commit 824f954

Please sign in to comment.