From 7f4e722c6b9967d9b405e5574758d1da5fc2e499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4me=20Chilliet?= Date: Mon, 22 Jun 2026 16:47:09 +0200 Subject: [PATCH] feat: Use a class to materialize user config entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Côme Chilliet --- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + lib/private/Config/UserConfig.php | 179 ++++++++------------ lib/private/Config/UserConfigEntry.php | 106 ++++++++++++ tests/lib/Config/UserConfigTest.php | 24 ++- 5 files changed, 194 insertions(+), 117 deletions(-) create mode 100644 lib/private/Config/UserConfigEntry.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index cd4bfedd8aa..793023b02ee 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1366,6 +1366,7 @@ return array( 'OC\\Config\\ConfigManager' => $baseDir . '/lib/private/Config/ConfigManager.php', 'OC\\Config\\PresetManager' => $baseDir . '/lib/private/Config/PresetManager.php', 'OC\\Config\\UserConfig' => $baseDir . '/lib/private/Config/UserConfig.php', + 'OC\\Config\\UserConfigEntry' => $baseDir . '/lib/private/Config/UserConfigEntry.php', 'OC\\Console\\Application' => $baseDir . '/lib/private/Console/Application.php', 'OC\\Console\\TimestampFormatter' => $baseDir . '/lib/private/Console/TimestampFormatter.php', 'OC\\ContactsManager' => $baseDir . '/lib/private/ContactsManager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 3d664c1bf06..20089dd536e 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1407,6 +1407,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Config\\ConfigManager' => __DIR__ . '/../../..' . '/lib/private/Config/ConfigManager.php', 'OC\\Config\\PresetManager' => __DIR__ . '/../../..' . '/lib/private/Config/PresetManager.php', 'OC\\Config\\UserConfig' => __DIR__ . '/../../..' . '/lib/private/Config/UserConfig.php', + 'OC\\Config\\UserConfigEntry' => __DIR__ . '/../../..' . '/lib/private/Config/UserConfigEntry.php', 'OC\\Console\\Application' => __DIR__ . '/../../..' . '/lib/private/Console/Application.php', 'OC\\Console\\TimestampFormatter' => __DIR__ . '/../../..' . '/lib/private/Console/TimestampFormatter.php', 'OC\\ContactsManager' => __DIR__ . '/../../..' . '/lib/private/ContactsManager.php', diff --git a/lib/private/Config/UserConfig.php b/lib/private/Config/UserConfig.php index f9d5e59628d..2e3b6baeb52 100644 --- a/lib/private/Config/UserConfig.php +++ b/lib/private/Config/UserConfig.php @@ -1,6 +1,7 @@ >> cache for normal config keys */ + /** @var CappedMemoryCache>> cache for normal config keys */ private CappedMemoryCache $fastCache; - /** @var CappedMemoryCache>> cache for lazy config keys */ + /** @var CappedMemoryCache>> cache for lazy config keys */ private CappedMemoryCache $lazyCache; /** @var array, aliases: array, strictness: Strictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */ private array $configLexiconDetails = []; @@ -203,9 +202,9 @@ class UserConfig implements IUserConfig { $this->matchAndApplyLexiconDefinition($userId, $app, $key); if (isset($this->fastCache[$userId][$app][$key])) { - return $this->isFlagged(self::FLAG_SENSITIVE, $this->fastCache[$userId][$app][$key]['flags']); + return $this->fastCache[$userId][$app][$key]->isSensitive(); } elseif (isset($this->lazyCache[$userId][$app][$key])) { - return $this->isFlagged(self::FLAG_SENSITIVE, $this->lazyCache[$userId][$app][$key]['flags']); + return $this->lazyCache[$userId][$app][$key]->isSensitive(); } else { throw new UnknownKeyException('Unknown config key ' . $app . '/' . $key); } @@ -230,9 +229,9 @@ class UserConfig implements IUserConfig { $this->matchAndApplyLexiconDefinition($userId, $app, $key); if (isset($this->fastCache[$userId][$app][$key])) { - return $this->isFlagged(self::FLAG_INDEXED, $this->fastCache[$userId][$app][$key]['flags']); + return $this->fastCache[$userId][$app][$key]->isIndexed(); } elseif (isset($this->lazyCache[$userId][$app][$key])) { - return $this->isFlagged(self::FLAG_INDEXED, $this->lazyCache[$userId][$app][$key]['flags']); + return $this->lazyCache[$userId][$app][$key]->isIndexed(); } else { throw new UnknownKeyException('Unknown config key ' . $app . '/' . $key); } @@ -352,12 +351,12 @@ class UserConfig implements IUserConfig { $values = []; foreach (array_keys($cache) as $app) { if (isset($cache[$app][$key])) { - $valueDetail = $cache[$app][$key]; + $entry = $cache[$app][$key]; try { - $this->decryptSensitiveValue($userId, $app, $key, $valueDetail); - $value = $this->convertTypedValue($valueDetail['value'], $typedAs ?? $this->getValueType($userId, $app, $key, $lazy)); + $value = $this->getDecryptedSensitiveValue($userId, $app, $key, $entry); + $value = $this->convertTypedValue($value, $typedAs ?? $entry->getType()); } catch (IncorrectTypeException|UnknownKeyException) { - $value = $valueDetail['value']; + $value = $entry->getRawValue(); } $values[$app] = $value; } @@ -803,9 +802,9 @@ class UserConfig implements IUserConfig { * This way, lazyCache will be empty until the load for lazy config value is requested. */ if (isset($this->lazyCache[$userId][$app][$key])) { - $valueDetail = $this->lazyCache[$userId][$app][$key]; + $entry = $this->lazyCache[$userId][$app][$key]; } elseif (isset($this->fastCache[$userId][$app][$key])) { - $valueDetail = $this->fastCache[$userId][$app][$key]; + $entry = $this->fastCache[$userId][$app][$key]; } else { return $default; } @@ -815,18 +814,15 @@ class UserConfig implements IUserConfig { * If type of stored value is set as mixed, we don't filter. * If type of stored value is defined, we compare with the one requested. */ - $knownType = $valueDetail['type'] ?? null; + $knownType = $entry->getType(); if ($type !== ValueType::MIXED - && $knownType !== null && $knownType !== ValueType::MIXED && $type !== $knownType) { $this->logger->warning('conflict with value type from database', ['app' => $app, 'key' => $key, 'type' => $type, 'knownType' => $knownType]); throw new TypeConflictException('conflict with value type from database'); } - $this->decryptSensitiveValue($userId, $app, $key, $valueDetail); - - $value = $valueDetail['value']; + $value = $this->getDecryptedSensitiveValue($userId, $app, $key, $entry); // in case the key was modified while running matchAndApplyLexiconDefinition() we are // interested to check options in case a modification of the value is needed @@ -857,9 +853,9 @@ class UserConfig implements IUserConfig { $this->matchAndApplyLexiconDefinition($userId, $app, $key); if (isset($this->fastCache[$userId][$app][$key])) { - return $this->fastCache[$userId][$app][$key]['type']; + return $this->fastCache[$userId][$app][$key]->getType(); } elseif (isset($this->lazyCache[$userId][$app][$key])) { - return $this->lazyCache[$userId][$app][$key]['type']; + return $this->lazyCache[$userId][$app][$key]->getType(); } else { throw new UnknownKeyException('Unknown config key ' . $app . '/' . $key); } @@ -885,9 +881,9 @@ class UserConfig implements IUserConfig { $this->matchAndApplyLexiconDefinition($userId, $app, $key); if (isset($this->fastCache[$userId][$app][$key])) { - return $this->fastCache[$userId][$app][$key]['flags']; + return $this->fastCache[$userId][$app][$key]->getFlags(); } elseif (isset($this->lazyCache[$userId][$app][$key])) { - return $this->lazyCache[$userId][$app][$key]['flags']; + return $this->lazyCache[$userId][$app][$key]->getFlags(); } else { throw new UnknownKeyException('Unknown config key ' . $app . '/' . $key); } @@ -1160,16 +1156,23 @@ class UserConfig implements IUserConfig { $inserted = $refreshCache = false; $origValue = $value; - $sensitive = $this->isFlagged(self::FLAG_SENSITIVE, $flags); - if ($sensitive || ($this->hasKey($userId, $app, $key, $lazy) && $this->isSensitive($userId, $app, $key, $lazy))) { - $value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value); - $flags |= self::FLAG_SENSITIVE; + $newEntry = new UserConfigEntry( + type:$type, + flags:$flags, + value:$value, + crypto:$this->crypto, + ); + - $sensitive = $newEntry->isSensitive(); + if ($newEntry->isSensitive() || ($this->hasKey($userId, $app, $key, $lazy) && $this->isSensitive($userId, $app, $key, $lazy))) { + $newEntry->setSensitive(true); } + $value = $newEntry->getDecryptedSensitiveValue(); + $flags = $newEntry->getFlags(); // if requested, we fill the 'indexed' field with current value $indexed = ''; - if ($type !== ValueType::ARRAY && $this->isFlagged(self::FLAG_INDEXED, $flags)) { - if ($this->isFlagged(self::FLAG_SENSITIVE, $flags)) { + if ($type !== ValueType::ARRAY && $newEntry->isIndexed()) { + if ($newEntry->isSensitive()) { $this->logger->warning('sensitive value are not to be indexed'); } elseif (strlen($value) > self::USER_MAX_LENGTH) { $this->logger->warning('value is too lengthy to be indexed'); @@ -1225,14 +1228,14 @@ class UserConfig implements IUserConfig { * We cannot insert a new row, meaning we need to update an already existing one */ if (!$inserted) { - $currType = $this->fastCache[$userId][$app][$key]['type'] ?? $this->lazyCache[$userId][$app][$key]['type'] ?? null; + $currType = ($this->fastCache[$userId][$app][$key] ?? $this->lazyCache[$userId][$app][$key] ?? null)?->getType(); if ($currType === null) { // this might happen when switching lazy loading status $this->loadConfigAll($userId); if (!isset($this->fastCache[$userId][$app][$key]) && !isset($this->lazyCache[$userId][$app][$key])) { throw new UnknownKeyException("unknown key $app $key for $userId even though $updateReason"); } - $currType = $this->fastCache[$userId][$app][$key]['type'] ?? $this->lazyCache[$userId][$app][$key]['type'] ?? null; + $currType = ($this->fastCache[$userId][$app][$key] ?? $this->lazyCache[$userId][$app][$key] ?? null)?->getType(); } /** @@ -1287,15 +1290,16 @@ class UserConfig implements IUserConfig { } // update local cache - $valueDetail = [ - 'value' => $value, - 'type' => $type, - 'flags' => $flags - ]; + $entry = new UserConfigEntry( + value: $value, + type: $type, + flags: $flags, + crypto:$this->crypto, + ); if ($lazy) { - $this->lazyCache[$userId][$app][$key] = $valueDetail; + $this->lazyCache[$userId][$app][$key] = $entry; } else { - $this->fastCache[$userId][$app][$key] = $valueDetail; + $this->fastCache[$userId][$app][$key] = $entry; } return true; @@ -1332,9 +1336,9 @@ class UserConfig implements IUserConfig { $update->executeStatement(); if ($lazy) { - $this->lazyCache[$userId][$app][$key]['type'] = $type; + $this->lazyCache[$userId][$app][$key]->setType($type); } else { - $this->fastCache[$userId][$app][$key]['type'] = $type; + $this->fastCache[$userId][$app][$key]->setType($type); } return true; @@ -1376,28 +1380,18 @@ class UserConfig implements IUserConfig { throw new UnknownKeyException('unknown config key'); } - $valueDetail = $cache[$userId][$app][$key]; - $flags = $this->getValueFlags($userId, $app, $key); - if ($sensitive) { - $flags |= self::FLAG_SENSITIVE; - $value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($valueDetail['value']); - } else { - $flags &= ~self::FLAG_SENSITIVE; - $this->decryptSensitiveValue($userId, $app, $key, $valueDetail); - $value = $valueDetail['value']; - } + $entry = $cache[$userId][$app][$key]; + $entry->setSensitive($sensitive); $update = $this->connection->getQueryBuilder(); $update->update('preferences') - ->set('flags', $update->createNamedParameter($flags, IQueryBuilder::PARAM_INT)) - ->set('configvalue', $update->createNamedParameter($value)) + ->set('flags', $update->createNamedParameter($entry->getFlags(), IQueryBuilder::PARAM_INT)) + ->set('configvalue', $update->createNamedParameter($entry->getRawValue())) ->where($update->expr()->eq('userid', $update->createNamedParameter($userId))) ->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app))) ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key))); $update->executeStatement(); - $cache[$userId][$app][$key]['flags'] = $flags; - return true; } @@ -1466,26 +1460,23 @@ class UserConfig implements IUserConfig { throw new UnknownKeyException('unknown config key'); } - $value = $cache[$userId][$app][$key]['value']; - $flags = $this->getValueFlags($userId, $app, $key); + $entry = $cache[$userId][$app][$key]; + $entry->setIndexed($indexed); if ($indexed) { - $indexed = $value; + $indexed = $entry->getRawValue(); } else { - $flags &= ~self::FLAG_INDEXED; $indexed = ''; } $update = $this->connection->getQueryBuilder(); $update->update('preferences') - ->set('flags', $update->createNamedParameter($flags, IQueryBuilder::PARAM_INT)) + ->set('flags', $update->createNamedParameter($entry->getFlags(), IQueryBuilder::PARAM_INT)) ->set('indexed', $update->createNamedParameter($indexed)) ->where($update->expr()->eq('userid', $update->createNamedParameter($userId))) ->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app))) ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key))); $update->executeStatement(); - $cache[$userId][$app][$key]['flags'] = $flags; - return true; } @@ -1634,19 +1625,18 @@ class UserConfig implements IUserConfig { throw new UnknownKeyException('unknown config key'); } - $valueDetail = $cache[$app][$key]; - $sensitive = $this->isSensitive($userId, $app, $key, null); - $this->decryptSensitiveValue($userId, $app, $key, $valueDetail); + $entry = $cache[$app][$key]; + $value = $this->getDecryptedSensitiveValue($userId, $app, $key, $entry); return [ 'userId' => $userId, 'app' => $app, 'key' => $key, - 'value' => $valueDetail['value'], + 'value' => $value, 'type' => $type->value, 'lazy' => $lazy, 'typeString' => $typeString, - 'sensitive' => $sensitive + 'sensitive' => $entry->isSensitive(), ]; } @@ -1775,16 +1765,6 @@ class UserConfig implements IUserConfig { ]; } - /** - * @param int $needle bitflag to search - * @param int $flags all flags - * - * @return bool TRUE if bitflag $needle is set in $flags - */ - private function isFlagged(int $needle, int $flags): bool { - return (($needle & $flags) !== 0); - } - /** * Confirm the string set for app and key fit the database description * @@ -1869,11 +1849,12 @@ class UserConfig implements IUserConfig { $rows = $result->fetchAllAssociative(); foreach ($rows as $row) { - $valueDetail = [ - 'value' => $row['configvalue'] ?? '', - 'type' => ValueType::from((int)($row['type'] ?? 0)), - 'flags' => (int)($row['flags'] ?? 0), - ]; + $valueDetail = new UserConfigEntry( + value: $row['configvalue'] ?? '', + type: ValueType::from((int)($row['type'] ?? 0)), + flags: (int)($row['flags'] ?? 0), + crypto:$this->crypto, + ); if ($this->migrationCompleted && (($row['lazy'] ?? ($lazy ?? 0) ? 1 : 0) === 1)) { $this->lazyCache[$userId][$row['appid']][$row['configkey']] = $valueDetail; } else { @@ -1912,22 +1893,16 @@ class UserConfig implements IUserConfig { * @return array */ private function formatAppValues(string $userId, string $app, array $values, bool $filtered = false): array { - foreach ($values as $key => $valueDetail) { - $value = $valueDetail['value']; - //$key = (string)$key; - try { - $type = $this->getValueType($userId, $app, (string)$key); - } catch (UnknownKeyException) { - continue; - } + foreach ($values as $key => $entry) { + $value = $entry->getRawValue(); + $type = $entry->getType(); - if ($this->isFlagged(self::FLAG_SENSITIVE, $valueDetail['flags'] ?? 0)) { + if ($entry->isSensitive()) { if ($filtered) { $value = IConfig::SENSITIVE_VALUE; $type = ValueType::STRING; } else { - $this->decryptSensitiveValue($userId, $app, (string)$key, $valueDetail); - $value = $valueDetail['value']; + $value = $this->getDecryptedSensitiveValue($userId, $app, (string)$key, $entry); } } @@ -1964,30 +1939,18 @@ class UserConfig implements IUserConfig { return $value; } - /** - * will change referenced $value with the decrypted value in case of encrypted (sensitive value) - * - * @param array{type: ValueType, flags: int, value: string} $valueDetail - */ - private function decryptSensitiveValue(string $userId, string $app, string $key, array &$valueDetail): void { - if (!$this->isFlagged(self::FLAG_SENSITIVE, $valueDetail['flags'] ?? 0)) { - return; - } - - if (!str_starts_with($valueDetail['value'], self::ENCRYPTION_PREFIX)) { - return; - } - + private function getDecryptedSensitiveValue(string $userId, string $app, string $key, UserConfigEntry $entry): string { try { - $valueDetail['value'] = $this->crypto->decrypt(substr($valueDetail['value'], self::ENCRYPTION_PREFIX_LENGTH)); + return $entry->getDecryptedSensitiveValue(); } catch (\Exception $e) { $this->logger->warning('could not decrypt sensitive value', [ 'userId' => $userId, 'app' => $app, 'key' => $key, - 'value' => $valueDetail['value'], + 'value' => $entry->getRawValue(), 'exception' => $e ]); + return $entry->getRawValue(); } } diff --git a/lib/private/Config/UserConfigEntry.php b/lib/private/Config/UserConfigEntry.php new file mode 100644 index 00000000000..f0a77567b30 --- /dev/null +++ b/lib/private/Config/UserConfigEntry.php @@ -0,0 +1,106 @@ +value; + } + + public function getType(): ValueType { + return $this->type; + } + + public function setType(ValueType $type): void { + $this->type = $type; + } + + public function getFlags(): int { + return $this->flags; + } + + public function isFlagged(int $mask): bool { + return (($mask & $this->flags) === $mask); + } + + public function isSensitive(): bool { + return $this->isFlagged(IUserConfig::FLAG_SENSITIVE); + } + + public function isIndexed(): bool { + return $this->isFlagged(IUserConfig::FLAG_INDEXED); + } + + /** + * will change referenced $value with the decrypted value in case of encrypted (sensitive value) + * + * @param array{type: ValueType, flags: int, value: string} $valueDetail + */ + public function getDecryptedSensitiveValue(): string { + if (!$this->isFlagged(IUserConfig::FLAG_SENSITIVE)) { + return $this->value; + } + + if ($this->decryptedValue !== null) { + return $this->decryptedValue; + } + + if (!str_starts_with($this->value, self::ENCRYPTION_PREFIX)) { + return $this->value; + } + + $this->decryptedValue = $this->crypto->decrypt(substr($this->value, self::ENCRYPTION_PREFIX_LENGTH)); + return $this->decryptedValue; + } + + public function setSensitive(bool $sensitive): void { + if ($sensitive) { + $this->flags |= IUserConfig::FLAG_SENSITIVE; + $this->decryptedValue = $this->value; + $this->value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($this->value); + } else { + $clearValue = $this->getDecryptedSensitiveValue(); + $this->flags &= ~IUserConfig::FLAG_SENSITIVE; + $this->value = $clearValue; + $this->decryptedValue = null; + } + } + + public function setIndexed(bool $indexed): void { + if ($indexed) { + $this->flags |= IUserConfig::FLAG_INDEXED; + $this->decryptedValue = $this->value; + $this->value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($this->value); + } else { + $clearValue = $this->getDecryptedSensitiveValue(); + $this->flags &= ~IUserConfig::FLAG_INDEXED; + $this->value = $clearValue; + $this->decryptedValue = null; + } + } +} diff --git a/tests/lib/Config/UserConfigTest.php b/tests/lib/Config/UserConfigTest.php index a5affdec858..7d0feb84f14 100644 --- a/tests/lib/Config/UserConfigTest.php +++ b/tests/lib/Config/UserConfigTest.php @@ -11,6 +11,7 @@ namespace Test\lib\Config; use OC\Config\ConfigManager; use OC\Config\PresetManager; use OC\Config\UserConfig; +use OC\Config\UserConfigEntry; use OCP\Config\Exceptions\TypeConflictException; use OCP\Config\Exceptions\UnknownKeyException; use OCP\Config\IUserConfig; @@ -230,7 +231,7 @@ class UserConfigTest extends TestCase { $flags = $row[4] ?? 0; if ((UserConfig::FLAG_SENSITIVE & $flags) !== 0) { if (!isset($this->basePreferences[$userId][$appId][$key]['encrypted'])) { - $value = self::invokePrivate(UserConfig::class, 'ENCRYPTION_PREFIX') + $value = self::invokePrivate(UserConfigEntry::class, 'ENCRYPTION_PREFIX') . $this->crypto->encrypt((string)$value); $this->basePreferences[$userId][$appId][$key]['encrypted'] = $value; } else { @@ -1422,10 +1423,13 @@ class UserConfigTest extends TestCase { $userConfig = $this->generateUserConfig($preload ?? []); $this->assertEquals($sensitive, $userConfig->isSensitive($userId, $app, $key)); if ($sensitive) { - $this->assertEquals(true, str_starts_with( - $userConfig->statusCache()['fastCache'][$userId][$app][$key]['value'] - ?? $userConfig->statusCache()['lazyCache'][$userId][$app][$key]['value'], - '$UserConfigEncryption$') + $statusCache = $userConfig->statusCache(); + $this->assertEquals( + true, + str_starts_with( + ($statusCache['fastCache'][$userId][$app][$key] + ?? $statusCache['lazyCache'][$userId][$app][$key])->getRawValue(), + '$UserConfigEncryption$') ); } } @@ -1450,10 +1454,11 @@ class UserConfigTest extends TestCase { $this->assertEquals($value, $userConfig->getValueString('user1', $app, $key)); foreach (['user1', 'user2', 'user3', 'user4'] as $userId) { $userConfig->getValueString($userId, $app, $key); // cache loading for userId + $statusCache = $userConfig->statusCache(); $this->assertEquals( !$sensitive, str_starts_with( - $userConfig->statusCache()['fastCache'][$userId][$app][$key]['value'] - ?? $userConfig->statusCache()['lazyCache'][$userId][$app][$key]['value'], + ($statusCache['fastCache'][$userId][$app][$key] + ?? $statusCache['lazyCache'][$userId][$app][$key])->getRawValue(), '$UserConfigEncryption$' ) ); @@ -1464,10 +1469,11 @@ class UserConfigTest extends TestCase { $this->assertEquals($value, $userConfig->getValueString('user1', $app, $key)); foreach (['user1', 'user2', 'user3', 'user4'] as $userId) { $this->assertEquals($sensitive, $userConfig->isSensitive($userId, $app, $key)); + $statusCache = $userConfig->statusCache(); // should only work if updateGlobalSensitive drop cache $this->assertEquals($sensitive, str_starts_with( - $userConfig->statusCache()['fastCache'][$userId][$app][$key]['value'] - ?? $userConfig->statusCache()['lazyCache'][$userId][$app][$key]['value'], + ($statusCache['fastCache'][$userId][$app][$key] + ?? $statusCache['lazyCache'][$userId][$app][$key])->getRawValue(), '$UserConfigEncryption$') ); }