diff options
Diffstat (limited to 'lib/private/Config/UserConfig.php')
-rw-r--r-- | lib/private/Config/UserConfig.php | 218 |
1 files changed, 173 insertions, 45 deletions
diff --git a/lib/private/Config/UserConfig.php b/lib/private/Config/UserConfig.php index 78c43fc4321..4ddad3ec2f2 100644 --- a/lib/private/Config/UserConfig.php +++ b/lib/private/Config/UserConfig.php @@ -11,14 +11,15 @@ namespace OC\Config; use Generator; use InvalidArgumentException; use JsonException; -use NCU\Config\Exceptions\IncorrectTypeException; -use NCU\Config\Exceptions\TypeConflictException; -use NCU\Config\Exceptions\UnknownKeyException; -use NCU\Config\IUserConfig; -use NCU\Config\Lexicon\ConfigLexiconEntry; -use NCU\Config\Lexicon\ConfigLexiconStrictness; -use NCU\Config\ValueType; use OC\AppFramework\Bootstrap\Coordinator; +use OCP\Config\Exceptions\IncorrectTypeException; +use OCP\Config\Exceptions\TypeConflictException; +use OCP\Config\Exceptions\UnknownKeyException; +use OCP\Config\IUserConfig; +use OCP\Config\Lexicon\Entry; +use OCP\Config\Lexicon\ILexicon; +use OCP\Config\Lexicon\Strictness; +use OCP\Config\ValueType; use OCP\DB\Exception as DBException; use OCP\DB\IResult; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -58,19 +59,19 @@ class UserConfig implements IUserConfig { private array $lazyCache = []; // cache for lazy config keys /** @var array<string, array<string, array<string, array<string, mixed>>>> ['user_id' => ['app_id' => ['key' => ['type' => ValueType, 'flags' => bitflag]]]] */ private array $valueDetails = []; // type for all config values - /** @var array<string, array<string, array<string, ValueType>>> ['user_id' => ['app_id' => ['key' => bitflag]]] */ - private array $valueTypes = []; // type for all config values - /** @var array<string, array<string, array<string, int>>> ['user_id' => ['app_id' => ['key' => bitflag]]] */ - private array $valueFlags = []; // type for all config values /** @var array<string, boolean> ['user_id' => bool] */ private array $fastLoaded = []; /** @var array<string, boolean> ['user_id' => bool] */ private array $lazyLoaded = []; - /** @var array<array-key, array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */ + /** @var array<string, array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */ private array $configLexiconDetails = []; + private bool $ignoreLexiconAliases = false; public function __construct( protected IDBConnection $connection, + protected IConfig $config, + private readonly ConfigManager $configManager, + private readonly PresetManager $presetManager, protected LoggerInterface $logger, protected ICrypto $crypto, ) { @@ -153,6 +154,7 @@ class UserConfig implements IUserConfig { public function hasKey(string $userId, string $app, string $key, ?bool $lazy = false): bool { $this->assertParams($userId, $app, $key); $this->loadConfig($userId, $lazy); + $this->matchAndApplyLexiconDefinition($userId, $app, $key); if ($lazy === null) { $appCache = $this->getValues($userId, $app); @@ -181,6 +183,7 @@ class UserConfig implements IUserConfig { public function isSensitive(string $userId, string $app, string $key, ?bool $lazy = false): bool { $this->assertParams($userId, $app, $key); $this->loadConfig($userId, $lazy); + $this->matchAndApplyLexiconDefinition($userId, $app, $key); if (!isset($this->valueDetails[$userId][$app][$key])) { throw new UnknownKeyException('unknown config key'); @@ -204,6 +207,7 @@ class UserConfig implements IUserConfig { public function isIndexed(string $userId, string $app, string $key, ?bool $lazy = false): bool { $this->assertParams($userId, $app, $key); $this->loadConfig($userId, $lazy); + $this->matchAndApplyLexiconDefinition($userId, $app, $key); if (!isset($this->valueDetails[$userId][$app][$key])) { throw new UnknownKeyException('unknown config key'); @@ -225,10 +229,13 @@ class UserConfig implements IUserConfig { * @since 31.0.0 */ public function isLazy(string $userId, string $app, string $key): bool { + $this->matchAndApplyLexiconDefinition($userId, $app, $key); + // there is a huge probability the non-lazy config are already loaded // meaning that we can start by only checking if a current non-lazy key exists if ($this->hasKey($userId, $app, $key, false)) { - return false; // meaning key is not lazy. + // meaning key is not lazy. + return false; } // as key is not found as non-lazy, we load and search in the lazy config @@ -263,7 +270,8 @@ class UserConfig implements IUserConfig { $values = array_filter( $this->formatAppValues($userId, $app, ($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? []), $filtered), function (string $key) use ($prefix): bool { - return str_starts_with($key, $prefix); // filter values based on $prefix + // filter values based on $prefix + return str_starts_with($key, $prefix); }, ARRAY_FILTER_USE_KEY ); @@ -350,6 +358,7 @@ class UserConfig implements IUserConfig { ?array $userIds = null, ): array { $this->assertParams('', $app, $key, allowEmptyUser: true); + $this->matchAndApplyLexiconDefinition('', $app, $key); $qb = $this->connection->getQueryBuilder(); $qb->select('userid', 'configvalue', 'type') @@ -465,6 +474,7 @@ class UserConfig implements IUserConfig { */ private function searchUsersByTypedValue(string $app, string $key, string|array $value, bool $caseInsensitive = false): Generator { $this->assertParams('', $app, $key, allowEmptyUser: true); + $this->matchAndApplyLexiconDefinition('', $app, $key); $qb = $this->connection->getQueryBuilder(); $qb->from('preferences'); @@ -542,6 +552,7 @@ class UserConfig implements IUserConfig { string $default = '', ?bool $lazy = false, ): string { + $this->matchAndApplyLexiconDefinition($userId, $app, $key); try { $lazy ??= $this->isLazy($userId, $app, $key); } catch (UnknownKeyException) { @@ -711,9 +722,18 @@ class UserConfig implements IUserConfig { ValueType $type, ): string { $this->assertParams($userId, $app, $key); - if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, default: $default)) { - return $default; // returns default if strictness of lexicon is set to WARNING (block and report) + $origKey = $key; + $matched = $this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, default: $default); + if ($default === null) { + // there is no logical reason for it to be null + throw new \Exception('default cannot be null'); + } + + // returns default if strictness of lexicon is set to WARNING (block and report) + if (!$matched) { + return $default; } + $this->loadConfig($userId, $lazy); /** @@ -746,6 +766,14 @@ class UserConfig implements IUserConfig { } $this->decryptSensitiveValue($userId, $app, $key, $value); + + // in case the key was modified while running matchAndApplyLexiconDefinition() we are + // interested to check options in case a modification of the value is needed + // ie inverting value from previous key when using lexicon option RENAME_INVERT_BOOLEAN + if ($origKey !== $key && $type === ValueType::BOOL) { + $value = ($this->configManager->convertToBool($value, $this->getLexiconEntry($app, $key))) ? '1' : '0'; + } + return $value; } @@ -764,6 +792,7 @@ class UserConfig implements IUserConfig { public function getValueType(string $userId, string $app, string $key, ?bool $lazy = null): ValueType { $this->assertParams($userId, $app, $key); $this->loadConfig($userId, $lazy); + $this->matchAndApplyLexiconDefinition($userId, $app, $key); if (!isset($this->valueDetails[$userId][$app][$key]['type'])) { throw new UnknownKeyException('unknown config key'); @@ -788,6 +817,7 @@ class UserConfig implements IUserConfig { public function getValueFlags(string $userId, string $app, string $key, bool $lazy = false): int { $this->assertParams($userId, $app, $key); $this->loadConfig($userId, $lazy); + $this->matchAndApplyLexiconDefinition($userId, $app, $key); if (!isset($this->valueDetails[$userId][$app][$key])) { throw new UnknownKeyException('unknown config key'); @@ -1045,9 +1075,15 @@ class UserConfig implements IUserConfig { 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($app, $key, $lazy, $type, $flags)) { - return false; // returns false as database is not updated + if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags)) { + // returns false as database is not updated + return false; } $this->loadConfig($userId, $lazy); @@ -1100,7 +1136,8 @@ class UserConfig implements IUserConfig { $inserted = true; } catch (DBException $e) { if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { - throw $e; // TODO: throw exception or just log and returns false !? + // TODO: throw exception or just log and returns false !? + throw $e; } } } @@ -1127,8 +1164,8 @@ class UserConfig implements IUserConfig { * we only accept a different type from the one stored in database * if the one stored in database is not-defined (VALUE_MIXED) */ - if ($currType !== ValueType::MIXED && - $currType !== $type) { + if ($currType !== ValueType::MIXED + && $currType !== $type) { try { $currTypeDef = $currType->getDefinition(); $typeDef = $type->getDefinition(); @@ -1195,6 +1232,7 @@ class UserConfig implements IUserConfig { public function updateType(string $userId, string $app, string $key, ValueType $type = ValueType::MIXED): bool { $this->assertParams($userId, $app, $key); $this->loadConfigAll($userId); + $this->matchAndApplyLexiconDefinition($userId, $app, $key); $this->isLazy($userId, $app, $key); // confirm key exists $update = $this->connection->getQueryBuilder(); @@ -1224,6 +1262,7 @@ class UserConfig implements IUserConfig { public function updateSensitive(string $userId, string $app, string $key, bool $sensitive): bool { $this->assertParams($userId, $app, $key); $this->loadConfigAll($userId); + $this->matchAndApplyLexiconDefinition($userId, $app, $key); try { if ($sensitive === $this->isSensitive($userId, $app, $key, null)) { @@ -1279,6 +1318,8 @@ class UserConfig implements IUserConfig { */ public function updateGlobalSensitive(string $app, string $key, bool $sensitive): void { $this->assertParams('', $app, $key, allowEmptyUser: true); + $this->matchAndApplyLexiconDefinition('', $app, $key); + foreach (array_keys($this->getValuesByUsers($app, $key)) as $userId) { try { $this->updateSensitive($userId, $app, $key, $sensitive); @@ -1287,7 +1328,8 @@ class UserConfig implements IUserConfig { } } - $this->clearCacheAll(); // we clear all cache + // we clear all cache + $this->clearCacheAll(); } /** @@ -1307,6 +1349,7 @@ class UserConfig implements IUserConfig { public function updateIndexed(string $userId, string $app, string $key, bool $indexed): bool { $this->assertParams($userId, $app, $key); $this->loadConfigAll($userId); + $this->matchAndApplyLexiconDefinition($userId, $app, $key); try { if ($indexed === $this->isIndexed($userId, $app, $key, null)) { @@ -1362,6 +1405,8 @@ class UserConfig implements IUserConfig { */ public function updateGlobalIndexed(string $app, string $key, bool $indexed): void { $this->assertParams('', $app, $key, allowEmptyUser: true); + $this->matchAndApplyLexiconDefinition('', $app, $key); + foreach (array_keys($this->getValuesByUsers($app, $key)) as $userId) { try { $this->updateIndexed($userId, $app, $key, $indexed); @@ -1370,7 +1415,8 @@ class UserConfig implements IUserConfig { } } - $this->clearCacheAll(); // we clear all cache + // we clear all cache + $this->clearCacheAll(); } /** @@ -1387,6 +1433,7 @@ class UserConfig implements IUserConfig { public function updateLazy(string $userId, string $app, string $key, bool $lazy): bool { $this->assertParams($userId, $app, $key); $this->loadConfigAll($userId); + $this->matchAndApplyLexiconDefinition($userId, $app, $key); try { if ($lazy === $this->isLazy($userId, $app, $key)) { @@ -1421,6 +1468,7 @@ class UserConfig implements IUserConfig { */ public function updateGlobalLazy(string $app, string $key, bool $lazy): void { $this->assertParams('', $app, $key, allowEmptyUser: true); + $this->matchAndApplyLexiconDefinition('', $app, $key); $update = $this->connection->getQueryBuilder(); $update->update('preferences') @@ -1446,6 +1494,8 @@ class UserConfig implements IUserConfig { public function getDetails(string $userId, string $app, string $key): array { $this->assertParams($userId, $app, $key); $this->loadConfigAll($userId); + $this->matchAndApplyLexiconDefinition($userId, $app, $key); + $lazy = $this->isLazy($userId, $app, $key); if ($lazy) { @@ -1493,6 +1543,8 @@ class UserConfig implements IUserConfig { */ public function deleteUserConfig(string $userId, string $app, string $key): void { $this->assertParams($userId, $app, $key); + $this->matchAndApplyLexiconDefinition($userId, $app, $key); + $qb = $this->connection->getQueryBuilder(); $qb->delete('preferences') ->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId))) @@ -1502,6 +1554,7 @@ class UserConfig implements IUserConfig { unset($this->lazyCache[$userId][$app][$key]); unset($this->fastCache[$userId][$app][$key]); + unset($this->valueDetails[$userId][$app][$key]); } /** @@ -1514,6 +1567,8 @@ class UserConfig implements IUserConfig { */ public function deleteKey(string $app, string $key): void { $this->assertParams('', $app, $key, allowEmptyUser: true); + $this->matchAndApplyLexiconDefinition('', $app, $key); + $qb = $this->connection->getQueryBuilder(); $qb->delete('preferences') ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app))) @@ -1532,6 +1587,7 @@ class UserConfig implements IUserConfig { */ public function deleteApp(string $app): void { $this->assertParams('', $app, allowEmptyUser: true); + $qb = $this->connection->getQueryBuilder(); $qb->delete('preferences') ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app))); @@ -1577,7 +1633,7 @@ class UserConfig implements IUserConfig { */ public function clearCacheAll(): void { $this->lazyLoaded = $this->fastLoaded = []; - $this->lazyCache = $this->fastCache = $this->valueDetails = []; + $this->lazyCache = $this->fastCache = $this->valueDetails = $this->configLexiconDetails = []; } /** @@ -1793,6 +1849,14 @@ class UserConfig implements IUserConfig { } + /** + * will change referenced $value with the decrypted value in case of encrypted (sensitive value) + * + * @param string $userId + * @param string $app + * @param string $key + * @param string $value + */ private function decryptSensitiveValue(string $userId, string $app, string $key, string &$value): void { if (!$this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags'] ?? 0)) { return; @@ -1816,68 +1880,113 @@ class UserConfig implements IUserConfig { } /** - * match and apply current use of config values with defined lexicon + * Match and apply current use of config values with defined lexicon. + * Set $lazy to NULL only if only interested into checking that $key is alias. * * @throws UnknownKeyException * @throws TypeConflictException + * @return bool FALSE if conflict with defined lexicon were observed in the process */ private function matchAndApplyLexiconDefinition( + string $userId, string $app, - string $key, - bool &$lazy, - ValueType &$type, + string &$key, + ?bool &$lazy = null, + ValueType &$type = ValueType::MIXED, int &$flags = 0, - string &$default = '', + ?string &$default = null, ): bool { $configDetails = $this->getConfigDetailsFromLexicon($app); + if (array_key_exists($key, $configDetails['aliases']) && !$this->ignoreLexiconAliases) { + // in case '$rename' is set in ConfigLexiconEntry, we use the new config key + $key = $configDetails['aliases'][$key]; + } + if (!array_key_exists($key, $configDetails['entries'])) { return $this->applyLexiconStrictness($configDetails['strictness'], 'The user config key ' . $app . '/' . $key . ' is not defined in the config lexicon'); } - /** @var ConfigLexiconEntry $configValue */ + // if lazy is NULL, we ignore all check on the type/lazyness/default from Lexicon + if ($lazy === null) { + return true; + } + + /** @var Entry $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; + } + + // 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; + } + + // 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 + */ + private function getSystemDefault(string $appId, Entry $configValue): ?string { + $default = $this->config->getSystemValue('lexicon.default.userconfig', [])[$appId][$configValue->getKey()] ?? null; + if ($default === null) { + // no system default, using default default. + return null; + } + + return $configValue->convertToString($default); } /** * manage ConfigLexicon behavior based on strictness set in IConfigLexicon * - * @see IConfigLexicon::getStrictness() - * @param ConfigLexiconStrictness|null $strictness + * @param Strictness|null $strictness * @param string $line * * @return bool TRUE if conflict can be fully ignored * @throws UnknownKeyException + *@see ILexicon::getStrictness() */ - private function applyLexiconStrictness(?ConfigLexiconStrictness $strictness, string $line = ''): bool { + private function applyLexiconStrictness(?Strictness $strictness, string $line = ''): bool { if ($strictness === null) { return true; } switch ($strictness) { - case ConfigLexiconStrictness::IGNORE: + case Strictness::IGNORE: return true; - case ConfigLexiconStrictness::NOTICE: + case Strictness::NOTICE: $this->logger->notice($line); return true; - case ConfigLexiconStrictness::WARNING: + case Strictness::WARNING: $this->logger->warning($line); return false; - case ConfigLexiconStrictness::EXCEPTION: + case Strictness::EXCEPTION: throw new UnknownKeyException($line); } @@ -1889,23 +1998,42 @@ class UserConfig implements IUserConfig { * * @param string $appId * - * @return array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness} + * @return array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness} + *@internal + * */ - private function getConfigDetailsFromLexicon(string $appId): array { + public function getConfigDetailsFromLexicon(string $appId): array { if (!array_key_exists($appId, $this->configLexiconDetails)) { - $entries = []; + $entries = $aliases = []; $bootstrapCoordinator = \OCP\Server::get(Coordinator::class); $configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId); foreach ($configLexicon?->getUserConfigs() ?? [] as $configEntry) { $entries[$configEntry->getKey()] = $configEntry; + if ($configEntry->getRename() !== null) { + $aliases[$configEntry->getRename()] = $configEntry->getKey(); + } } $this->configLexiconDetails[$appId] = [ 'entries' => $entries, - 'strictness' => $configLexicon?->getStrictness() ?? ConfigLexiconStrictness::IGNORE + 'aliases' => $aliases, + 'strictness' => $configLexicon?->getStrictness() ?? Strictness::IGNORE ]; } return $this->configLexiconDetails[$appId]; } + + private function getLexiconEntry(string $appId, string $key): ?Entry { + return $this->getConfigDetailsFromLexicon($appId)['entries'][$key] ?? null; + } + + /** + * if set to TRUE, ignore aliases defined in Config Lexicon during the use of the methods of this class + * + * @internal + */ + public function ignoreLexiconAliases(bool $ignore): void { + $this->ignoreLexiconAliases = $ignore; + } } |