diff --git a/core/Command/Config/App/Base.php b/core/Command/Config/App/Base.php index 07341c4faf931..e90a8e78f5be2 100644 --- a/core/Command/Config/App/Base.php +++ b/core/Command/Config/App/Base.php @@ -7,12 +7,14 @@ */ namespace OC\Core\Command\Config\App; +use OC\Config\ConfigManager; use OCP\IAppConfig; use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; abstract class Base extends \OC\Core\Command\Base { public function __construct( protected IAppConfig $appConfig, + protected readonly ConfigManager $configManager, ) { parent::__construct(); } diff --git a/core/Command/Config/App/SetConfig.php b/core/Command/Config/App/SetConfig.php index 345067cfd45cd..1f4ab81bf051e 100644 --- a/core/Command/Config/App/SetConfig.php +++ b/core/Command/Config/App/SetConfig.php @@ -9,7 +9,6 @@ namespace OC\Core\Command\Config\App; use OC\AppConfig; -use OCP\Exceptions\AppConfigIncorrectTypeException; use OCP\Exceptions\AppConfigUnknownKeyException; use OCP\IAppConfig; use Symfony\Component\Console\Helper\QuestionHelper; @@ -161,7 +160,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $value = (string)$input->getOption('value'); - switch ($type) { case IAppConfig::VALUE_MIXED: $updated = $this->appConfig->setValueMixed($appName, $configName, $value, $lazy, $sensitive); @@ -172,34 +170,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int break; case IAppConfig::VALUE_INT: - if ($value !== ((string)((int)$value))) { - throw new AppConfigIncorrectTypeException('Value is not an integer'); - } - $updated = $this->appConfig->setValueInt($appName, $configName, (int)$value, $lazy, $sensitive); + $updated = $this->appConfig->setValueInt($appName, $configName, $this->configManager->convertToInt($value), $lazy, $sensitive); break; case IAppConfig::VALUE_FLOAT: - if ($value !== ((string)((float)$value))) { - throw new AppConfigIncorrectTypeException('Value is not a float'); - } - $updated = $this->appConfig->setValueFloat($appName, $configName, (float)$value, $lazy, $sensitive); + $updated = $this->appConfig->setValueFloat($appName, $configName, $this->configManager->convertToFloat($value), $lazy, $sensitive); break; case IAppConfig::VALUE_BOOL: - if (in_array(strtolower($value), ['true', '1', 'on', 'yes'])) { - $valueBool = true; - } elseif (in_array(strtolower($value), ['false', '0', 'off', 'no'])) { - $valueBool = false; - } else { - throw new AppConfigIncorrectTypeException('Value is not a boolean, please use \'true\' or \'false\''); - } - $updated = $this->appConfig->setValueBool($appName, $configName, $valueBool, $lazy); + $updated = $this->appConfig->setValueBool($appName, $configName, $this->configManager->convertToBool($value), $lazy); break; case IAppConfig::VALUE_ARRAY: - $valueArray = json_decode($value, true, flags: JSON_THROW_ON_ERROR); - $valueArray = (is_array($valueArray)) ? $valueArray : throw new AppConfigIncorrectTypeException('Value is not an array'); - $updated = $this->appConfig->setValueArray($appName, $configName, $valueArray, $lazy, $sensitive); + $updated = $this->appConfig->setValueArray($appName, $configName, $this->configManager->convertToArray($value), $lazy, $sensitive); break; } } diff --git a/core/Command/Config/ListConfigs.php b/core/Command/Config/ListConfigs.php index 094348dd9ba51..b81bfbf4d18d8 100644 --- a/core/Command/Config/ListConfigs.php +++ b/core/Command/Config/ListConfigs.php @@ -7,6 +7,7 @@ */ namespace OC\Core\Command\Config; +use OC\Config\ConfigManager; use OC\Core\Command\Base; use OC\SystemConfig; use OCP\IAppConfig; @@ -22,6 +23,7 @@ class ListConfigs extends Base { public function __construct( protected SystemConfig $systemConfig, protected IAppConfig $appConfig, + protected ConfigManager $configManager, ) { parent::__construct(); } @@ -44,6 +46,7 @@ protected function configure() { InputOption::VALUE_NONE, 'Use this option when you want to include sensitive configs like passwords, salts, ...' ) + ->addOption('migrate', null, InputOption::VALUE_NONE, 'Rename config keys of all enabled apps, based on ConfigLexicon') ; } @@ -51,6 +54,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $app = $input->getArgument('app'); $noSensitiveValues = !$input->getOption('private'); + if ($input->getOption('migrate')) { + $this->configManager->migrateConfigLexiconKeys(($app === 'all') ? null : $app); + } + if (!is_string($app)) { $output->writeln('Invalid app value given'); return 1; diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index c97549e395f51..ccac5c4cebab0 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1190,6 +1190,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\\ConfigManager' => $baseDir . '/lib/private/Config/ConfigManager.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', @@ -1901,6 +1902,7 @@ 'OC\\Repair\\ClearGeneratedAvatarCache' => $baseDir . '/lib/private/Repair/ClearGeneratedAvatarCache.php', 'OC\\Repair\\ClearGeneratedAvatarCacheJob' => $baseDir . '/lib/private/Repair/ClearGeneratedAvatarCacheJob.php', 'OC\\Repair\\Collation' => $baseDir . '/lib/private/Repair/Collation.php', + 'OC\\Repair\\ConfigKeyMigration' => $baseDir . '/lib/private/Repair/ConfigKeyMigration.php', 'OC\\Repair\\Events\\RepairAdvanceEvent' => $baseDir . '/lib/private/Repair/Events/RepairAdvanceEvent.php', 'OC\\Repair\\Events\\RepairErrorEvent' => $baseDir . '/lib/private/Repair/Events/RepairErrorEvent.php', 'OC\\Repair\\Events\\RepairFinishEvent' => $baseDir . '/lib/private/Repair/Events/RepairFinishEvent.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 39880f3366fe8..326e6af70eac8 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1231,6 +1231,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\\ConfigManager' => __DIR__ . '/../../..' . '/lib/private/Config/ConfigManager.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', @@ -1942,6 +1943,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Repair\\ClearGeneratedAvatarCache' => __DIR__ . '/../../..' . '/lib/private/Repair/ClearGeneratedAvatarCache.php', 'OC\\Repair\\ClearGeneratedAvatarCacheJob' => __DIR__ . '/../../..' . '/lib/private/Repair/ClearGeneratedAvatarCacheJob.php', 'OC\\Repair\\Collation' => __DIR__ . '/../../..' . '/lib/private/Repair/Collation.php', + 'OC\\Repair\\ConfigKeyMigration' => __DIR__ . '/../../..' . '/lib/private/Repair/ConfigKeyMigration.php', 'OC\\Repair\\Events\\RepairAdvanceEvent' => __DIR__ . '/../../..' . '/lib/private/Repair/Events/RepairAdvanceEvent.php', 'OC\\Repair\\Events\\RepairErrorEvent' => __DIR__ . '/../../..' . '/lib/private/Repair/Events/RepairErrorEvent.php', 'OC\\Repair\\Events\\RepairFinishEvent' => __DIR__ . '/../../..' . '/lib/private/Repair/Events/RepairFinishEvent.php', diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index 303a88e9cebca..eb4700020d2f4 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -8,6 +8,7 @@ use OC\AppConfig; use OC\AppFramework\Bootstrap\Coordinator; +use OC\Config\ConfigManager; use OCP\Activity\IManager as IActivityManager; use OCP\App\AppPathNotFoundException; use OCP\App\Events\AppDisableEvent; @@ -27,6 +28,7 @@ use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; +use OCP\Server; use OCP\ServerVersion; use OCP\Settings\IManager as ISettingsManager; use Psr\Log\LoggerInterface; @@ -82,12 +84,13 @@ public function __construct( private IEventDispatcher $dispatcher, private LoggerInterface $logger, private ServerVersion $serverVersion, + private ConfigManager $configManager, ) { } private function getNavigationManager(): INavigationManager { if ($this->navigationManager === null) { - $this->navigationManager = \OCP\Server::get(INavigationManager::class); + $this->navigationManager = Server::get(INavigationManager::class); } return $this->navigationManager; } @@ -113,7 +116,7 @@ private function getAppConfig(): AppConfig { if (!$this->config->getSystemValueBool('installed', false)) { throw new \Exception('Nextcloud is not installed yet, AppConfig is not available'); } - $this->appConfig = \OCP\Server::get(AppConfig::class); + $this->appConfig = Server::get(AppConfig::class); return $this->appConfig; } @@ -124,7 +127,7 @@ private function getUrlGenerator(): IURLGenerator { if (!$this->config->getSystemValueBool('installed', false)) { throw new \Exception('Nextcloud is not installed yet, AppConfig is not available'); } - $this->urlGenerator = \OCP\Server::get(IURLGenerator::class); + $this->urlGenerator = Server::get(IURLGenerator::class); return $this->urlGenerator; } @@ -459,7 +462,7 @@ public function loadApp(string $app): void { ]); } - $coordinator = \OCP\Server::get(Coordinator::class); + $coordinator = Server::get(Coordinator::class); $coordinator->bootApp($app); $eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it"); @@ -568,6 +571,8 @@ public function enableApp(string $appId, bool $forceEnable = false): void { ManagerEvent::EVENT_APP_ENABLE, $appId )); $this->clearAppsCache(); + + $this->configManager->migrateConfigLexiconKeys($appId); } /** @@ -626,6 +631,8 @@ public function enableAppForGroups(string $appId, array $groups, bool $forceEnab ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups )); $this->clearAppsCache(); + + $this->configManager->migrateConfigLexiconKeys($appId); } /** diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index adbfc58978bcd..b6412b410bb16 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -15,6 +15,7 @@ use NCU\Config\Lexicon\ConfigLexiconStrictness; use NCU\Config\Lexicon\IConfigLexicon; use OC\AppFramework\Bootstrap\Coordinator; +use OC\Config\ConfigManager; use OCP\DB\Exception as DBException; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Exceptions\AppConfigIncorrectTypeException; @@ -24,6 +25,7 @@ use OCP\IConfig; use OCP\IDBConnection; use OCP\Security\ICrypto; +use OCP\Server; use Psr\Log\LoggerInterface; /** @@ -59,8 +61,9 @@ class AppConfig implements IAppConfig { private array $valueTypes = []; // type for all config values private bool $fastLoaded = false; private bool $lazyLoaded = false; - /** @var array, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */ + /** @var array, aliases: array, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */ private array $configLexiconDetails = []; + private bool $ignoreLexiconAliases = false; /** @var ?array */ private ?array $appVersionsCache = null; @@ -117,6 +120,7 @@ public function getKeys(string $app): array { public function hasKey(string $app, string $key, ?bool $lazy = false): bool { $this->assertParams($app, $key); $this->loadConfig($app, $lazy); + $this->matchAndApplyLexiconDefinition($app, $key); if ($lazy === null) { $appCache = $this->getAllValues($app); @@ -142,6 +146,7 @@ public function hasKey(string $app, string $key, ?bool $lazy = false): bool { public function isSensitive(string $app, string $key, ?bool $lazy = false): bool { $this->assertParams($app, $key); $this->loadConfig(null, $lazy); + $this->matchAndApplyLexiconDefinition($app, $key); if (!isset($this->valueTypes[$app][$key])) { throw new AppConfigUnknownKeyException('unknown config key'); @@ -162,6 +167,9 @@ public function isSensitive(string $app, string $key, ?bool $lazy = false): bool * @since 29.0.0 */ public function isLazy(string $app, string $key): bool { + $this->assertParams($app, $key); + $this->matchAndApplyLexiconDefinition($app, $key); + // there is a huge probability the non-lazy config are already loaded if ($this->hasKey($app, $key, false)) { return false; @@ -284,7 +292,7 @@ public function getValueMixed( ): string { try { $lazy = ($lazy === null) ? $this->isLazy($app, $key) : $lazy; - } catch (AppConfigUnknownKeyException $e) { + } catch (AppConfigUnknownKeyException) { return $default; } @@ -429,6 +437,7 @@ private function getTypedValue( int $type, ): string { $this->assertParams($app, $key, valueType: $type); + $origKey = $key; if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $default)) { return $default; // returns default if strictness of lexicon is set to WARNING (block and report) } @@ -469,6 +478,14 @@ private function getTypedValue( $value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH)); } + // 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 === self::VALUE_BOOL) { + $configManager = Server::get(ConfigManager::class); + $value = ($configManager->convertToBool($value, $this->getLexiconEntry($app, $key))) ? '1' : '0'; + } + return $value; } @@ -863,7 +880,8 @@ private function setTypedValue( public function updateType(string $app, string $key, int $type = self::VALUE_MIXED): bool { $this->assertParams($app, $key); $this->loadConfigAll(); - $lazy = $this->isLazy($app, $key); + $this->matchAndApplyLexiconDefinition($app, $key); + $this->isLazy($app, $key); // confirm key exists // type can only be one type if (!in_array($type, [self::VALUE_MIXED, self::VALUE_STRING, self::VALUE_INT, self::VALUE_FLOAT, self::VALUE_BOOL, self::VALUE_ARRAY])) { @@ -905,6 +923,7 @@ public function updateType(string $app, string $key, int $type = self::VALUE_MIX public function updateSensitive(string $app, string $key, bool $sensitive): bool { $this->assertParams($app, $key); $this->loadConfigAll(); + $this->matchAndApplyLexiconDefinition($app, $key); try { if ($sensitive === $this->isSensitive($app, $key, null)) { @@ -964,6 +983,7 @@ public function updateSensitive(string $app, string $key, bool $sensitive): bool public function updateLazy(string $app, string $key, bool $lazy): bool { $this->assertParams($app, $key); $this->loadConfigAll(); + $this->matchAndApplyLexiconDefinition($app, $key); try { if ($lazy === $this->isLazy($app, $key)) { @@ -999,6 +1019,7 @@ public function updateLazy(string $app, string $key, bool $lazy): bool { public function getDetails(string $app, string $key): array { $this->assertParams($app, $key); $this->loadConfigAll(); + $this->matchAndApplyLexiconDefinition($app, $key); $lazy = $this->isLazy($app, $key); if ($lazy) { @@ -1086,6 +1107,8 @@ public function convertTypeToString(int $type): string { */ public function deleteKey(string $app, string $key): void { $this->assertParams($app, $key); + $this->matchAndApplyLexiconDefinition($app, $key); + $qb = $this->connection->getQueryBuilder(); $qb->delete('appconfig') ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app))) @@ -1293,6 +1316,7 @@ private function setAsLoaded(?bool $lazy): void { */ public function getValue($app, $key, $default = null) { $this->loadConfig($app); + $this->matchAndApplyLexiconDefinition($app, $key); return $this->fastCache[$app][$key] ?? $default; } @@ -1372,7 +1396,7 @@ private function formatAppValues(string $app, array $values, ?bool $lazy = null) foreach ($values as $key => $value) { try { $type = $this->getValueType($app, $key, $lazy); - } catch (AppConfigUnknownKeyException $e) { + } catch (AppConfigUnknownKeyException) { continue; } @@ -1556,7 +1580,8 @@ public function clearCachedConfig(): void { } /** - * 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 AppConfigUnknownKeyException * @throws AppConfigTypeConflictException @@ -1564,9 +1589,9 @@ public function clearCachedConfig(): void { */ private function matchAndApplyLexiconDefinition( string $app, - string $key, - bool &$lazy, - int &$type, + string &$key, + ?bool &$lazy = null, + int &$type = self::VALUE_MIXED, string &$default = '', ): bool { if (in_array($key, @@ -1578,11 +1603,18 @@ private function matchAndApplyLexiconDefinition( return true; // we don't break stuff for this list of config keys. } $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 app config key ' . $app . '/' . $key . ' is not defined in the config lexicon' - ); + return $this->applyLexiconStrictness($configDetails['strictness'], 'The app config key ' . $app . '/' . $key . ' is not defined in the config lexicon'); + } + + // if lazy is NULL, we ignore all check on the type/lazyness/default from Lexicon + if ($lazy === null) { + return true; } /** @var ConfigLexiconEntry $configValue */ @@ -1644,20 +1676,25 @@ private function applyLexiconStrictness( * extract details from registered $appId's config lexicon * * @param string $appId + * @internal * - * @return array{entries: array, strictness: ConfigLexiconStrictness} + * @return array{entries: array, aliases: array, strictness: ConfigLexiconStrictness} */ - 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?->getAppConfigs() ?? [] as $configEntry) { $entries[$configEntry->getKey()] = $configEntry; + if ($configEntry->getRename() !== null) { + $aliases[$configEntry->getRename()] = $configEntry->getKey(); + } } $this->configLexiconDetails[$appId] = [ 'entries' => $entries, + 'aliases' => $aliases, 'strictness' => $configLexicon?->getStrictness() ?? ConfigLexiconStrictness::IGNORE ]; } @@ -1665,6 +1702,19 @@ private function getConfigDetailsFromLexicon(string $appId): array { return $this->configLexiconDetails[$appId]; } + private function getLexiconEntry(string $appId, string $key): ?ConfigLexiconEntry { + 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; + } + /** * Returns the installed versions of all apps * diff --git a/lib/private/Config/ConfigManager.php b/lib/private/Config/ConfigManager.php new file mode 100644 index 0000000000000..1980269e2caf1 --- /dev/null +++ b/lib/private/Config/ConfigManager.php @@ -0,0 +1,250 @@ +migrateConfigLexiconKeys('core'); + $appManager = Server::get(IAppManager::class); + foreach ($appManager->getEnabledApps() as $app) { + $this->migrateConfigLexiconKeys($app); + } + + return; + } + + $this->loadConfigServices(); + + // it is required to ignore aliases when moving config values + $this->appConfig->ignoreLexiconAliases(true); + $this->userConfig->ignoreLexiconAliases(true); + + $this->migrateAppConfigKeys($appId); + $this->migrateUserConfigKeys($appId); + + // switch back to normal behavior + $this->appConfig->ignoreLexiconAliases(false); + $this->userConfig->ignoreLexiconAliases(false); + } + + /** + * config services cannot be load at __construct() or install will fail + */ + private function loadConfigServices(): void { + if ($this->appConfig === null) { + $this->appConfig = Server::get(IAppConfig::class); + } + if ($this->userConfig === null) { + $this->userConfig = Server::get(IUserConfig::class); + } + } + + /** + * Get details from lexicon related to AppConfig and search for entries with rename to initiate + * a migration to new config key + */ + private function migrateAppConfigKeys(string $appId): void { + $lexicon = $this->appConfig->getConfigDetailsFromLexicon($appId); + foreach ($lexicon['entries'] as $entry) { + // only interested in entries with rename set + if ($entry->getRename() === null) { + continue; + } + + // only migrate if rename config key has a value and the new config key hasn't + if ($this->appConfig->hasKey($appId, $entry->getRename()) + && !$this->appConfig->hasKey($appId, $entry->getKey())) { + try { + $this->migrateAppConfigValue($appId, $entry); + } catch (TypeConflictException $e) { + $this->logger->error('could not migrate AppConfig value', ['appId' => $appId, 'entry' => $entry, 'exception' => $e]); + continue; + } + } + + // we only delete previous config value if migration went fine. + $this->appConfig->deleteKey($appId, $entry->getRename()); + } + } + + /** + * Get details from lexicon related to UserConfig and search for entries with rename to initiate + * a migration to new config key + */ + private function migrateUserConfigKeys(string $appId): void { + $lexicon = $this->userConfig->getConfigDetailsFromLexicon($appId); + foreach ($lexicon['entries'] as $entry) { + // only interested in keys with rename set + if ($entry->getRename() === null) { + continue; + } + + foreach ($this->userConfig->getValuesByUsers($appId, $entry->getRename()) as $userId => $value) { + if ($this->userConfig->hasKey($userId, $appId, $entry->getKey())) { + continue; + } + + try { + $this->migrateUserConfigValue($userId, $appId, $entry); + } catch (TypeConflictException $e) { + $this->logger->error('could not migrate UserConfig value', ['userId' => $userId, 'appId' => $appId, 'entry' => $entry, 'exception' => $e]); + continue; + } + + $this->userConfig->deleteUserConfig($userId, $appId, $entry->getRename()); + } + } + } + + + /** + * converting value from rename to the new key + * + * @throws TypeConflictException if previous value does not fit the expected type + */ + private function migrateAppConfigValue(string $appId, ConfigLexiconEntry $entry): void { + $value = $this->appConfig->getValueMixed($appId, $entry->getRename(), lazy: null); + switch ($entry->getValueType()) { + case ValueType::STRING: + $this->appConfig->setValueString($appId, $entry->getKey(), $value); + return; + + case ValueType::INT: + $this->appConfig->setValueInt($appId, $entry->getKey(), $this->convertToInt($value)); + return; + + case ValueType::FLOAT: + $this->appConfig->setValueFloat($appId, $entry->getKey(), $this->convertToFloat($value)); + return; + + case ValueType::BOOL: + $this->appConfig->setValueBool($appId, $entry->getKey(), $this->convertToBool($value, $entry)); + return; + + case ValueType::ARRAY: + $this->appConfig->setValueArray($appId, $entry->getKey(), $this->convertToArray($value)); + return; + } + } + + /** + * converting value from rename to the new key + * + * @throws TypeConflictException if previous value does not fit the expected type + */ + private function migrateUserConfigValue(string $userId, string $appId, ConfigLexiconEntry $entry): void { + $value = $this->userConfig->getValueMixed($userId, $appId, $entry->getRename(), lazy: null); + switch ($entry->getValueType()) { + case ValueType::STRING: + $this->userConfig->setValueString($userId, $appId, $entry->getKey(), $value); + return; + + case ValueType::INT: + $this->userConfig->setValueInt($userId, $appId, $entry->getKey(), $this->convertToInt($value)); + return; + + case ValueType::FLOAT: + $this->userConfig->setValueFloat($userId, $appId, $entry->getKey(), $this->convertToFloat($value)); + return; + + case ValueType::BOOL: + $this->userConfig->setValueBool($userId, $appId, $entry->getKey(), $this->convertToBool($value, $entry)); + return; + + case ValueType::ARRAY: + $this->userConfig->setValueArray($userId, $appId, $entry->getKey(), $this->convertToArray($value)); + return; + } + } + + public function convertToInt(string $value): int { + if (!is_numeric($value) || (float)$value <> (int)$value) { + throw new TypeConflictException('Value is not an integer'); + } + + return (int)$value; + } + + public function convertToFloat(string $value): float { + if (!is_numeric($value)) { + throw new TypeConflictException('Value is not a float'); + } + + return (float)$value; + } + + public function convertToBool(string $value, ?ConfigLexiconEntry $entry = null): bool { + if (in_array(strtolower($value), ['true', '1', 'on', 'yes'])) { + $valueBool = true; + } elseif (in_array(strtolower($value), ['false', '0', 'off', 'no'])) { + $valueBool = false; + } else { + throw new TypeConflictException('Value cannot be converted to boolean'); + } + if ($entry?->hasOption(ConfigLexiconEntry::RENAME_INVERT_BOOLEAN) === true) { + $valueBool = !$valueBool; + } + + return $valueBool; + } + + public function convertToArray(string $value): array { + try { + $valueArray = json_decode($value, true, flags: JSON_THROW_ON_ERROR); + } catch (JsonException) { + throw new TypeConflictException('Value is not a valid json'); + } + if (!is_array($valueArray)) { + throw new TypeConflictException('Value is not an array'); + } + + return $valueArray; + } +} diff --git a/lib/private/Config/UserConfig.php b/lib/private/Config/UserConfig.php index 1fdcfaa53a7a0..f8c59a13d3da2 100644 --- a/lib/private/Config/UserConfig.php +++ b/lib/private/Config/UserConfig.php @@ -25,6 +25,7 @@ use OCP\IConfig; use OCP\IDBConnection; use OCP\Security\ICrypto; +use OCP\Server; use Psr\Log\LoggerInterface; /** @@ -62,8 +63,9 @@ class UserConfig implements IUserConfig { private array $fastLoaded = []; /** @var array ['user_id' => bool] */ private array $lazyLoaded = []; - /** @var array, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */ + /** @var array, aliases: array, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */ private array $configLexiconDetails = []; + private bool $ignoreLexiconAliases = false; public function __construct( protected IDBConnection $connection, @@ -150,6 +152,7 @@ public function getKeys(string $userId, string $app): array { 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); @@ -178,6 +181,7 @@ public function hasKey(string $userId, string $app, string $key, ?bool $lazy = f 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'); @@ -201,6 +205,7 @@ public function isSensitive(string $userId, string $app, string $key, ?bool $laz 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'); @@ -222,6 +227,8 @@ public function isIndexed(string $userId, string $app, string $key, ?bool $lazy * @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)) { @@ -349,6 +356,7 @@ public function getValuesByUsers( ?array $userIds = null, ): array { $this->assertParams('', $app, $key, allowEmptyUser: true); + $this->matchAndApplyLexiconDefinition('', $app, $key); $qb = $this->connection->getQueryBuilder(); $qb->select('userid', 'configvalue', 'type') @@ -464,6 +472,7 @@ public function searchUsersByValueBool(string $app, string $key, bool $value): G */ 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'); @@ -541,6 +550,7 @@ public function getValueMixed( string $default = '', ?bool $lazy = false, ): string { + $this->matchAndApplyLexiconDefinition($userId, $app, $key); try { $lazy ??= $this->isLazy($userId, $app, $key); } catch (UnknownKeyException) { @@ -710,6 +720,7 @@ private function getTypedValue( ValueType $type, ): string { $this->assertParams($userId, $app, $key); + $origKey = $key; if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, default: $default)) { // returns default if strictness of lexicon is set to WARNING (block and report) return $default; @@ -746,6 +757,15 @@ private function getTypedValue( } $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) { + $configManager = Server::get(ConfigManager::class); + $value = ($configManager->convertToBool($value, $this->getLexiconEntry($app, $key))) ? '1' : '0'; + } + return $value; } @@ -764,6 +784,7 @@ private function getTypedValue( 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 +809,7 @@ public function getValueType(string $userId, string $app, string $key, ?bool $la 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'); @@ -1202,8 +1224,8 @@ private function setTypedValue( public function updateType(string $userId, string $app, string $key, ValueType $type = ValueType::MIXED): bool { $this->assertParams($userId, $app, $key); $this->loadConfigAll($userId); - // confirm key exists - $this->isLazy($userId, $app, $key); + $this->matchAndApplyLexiconDefinition($userId, $app, $key); + $this->isLazy($userId, $app, $key); // confirm key exists $update = $this->connection->getQueryBuilder(); $update->update('preferences') @@ -1232,6 +1254,7 @@ public function updateType(string $userId, string $app, string $key, ValueType $ 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)) { @@ -1287,6 +1310,8 @@ public function updateSensitive(string $userId, string $app, string $key, bool $ */ 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); @@ -1316,6 +1341,7 @@ public function updateGlobalSensitive(string $app, string $key, bool $sensitive) 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)) { @@ -1371,6 +1397,8 @@ public function updateIndexed(string $userId, string $app, string $key, bool $in */ 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); @@ -1397,6 +1425,7 @@ public function updateGlobalIndexed(string $app, string $key, bool $indexed): vo 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)) { @@ -1431,6 +1460,7 @@ public function updateLazy(string $userId, string $app, string $key, bool $lazy) */ 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') @@ -1456,6 +1486,8 @@ public function updateGlobalLazy(string $app, string $key, bool $lazy): void { 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) { @@ -1503,6 +1535,8 @@ public function getDetails(string $userId, string $app, string $key): array { */ 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))) @@ -1525,6 +1559,8 @@ public function deleteUserConfig(string $userId, string $app, string $key): void */ 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))) @@ -1543,6 +1579,7 @@ public function deleteKey(string $app, string $key): void { */ 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))); @@ -1835,7 +1872,8 @@ private function decryptSensitiveValue(string $userId, string $app, string $key, } /** - * 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 @@ -1844,17 +1882,27 @@ private function decryptSensitiveValue(string $userId, string $app, string $key, 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 = '', ): 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'); } + // if lazy is NULL, we ignore all check on the type/lazyness/default from Lexicon + if ($lazy === null) { + return true; + } + /** @var ConfigLexiconEntry $configValue */ $configValue = $configDetails['entries'][$key]; if ($type === ValueType::MIXED) { @@ -1939,24 +1987,42 @@ private function applyLexiconStrictness(?ConfigLexiconStrictness $strictness, st * extract details from registered $appId's config lexicon * * @param string $appId + * @internal * - * @return array{entries: array, strictness: ConfigLexiconStrictness} + * @return array{entries: array, aliases: array, strictness: ConfigLexiconStrictness} */ - 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, + 'aliases' => $aliases, 'strictness' => $configLexicon?->getStrictness() ?? ConfigLexiconStrictness::IGNORE ]; } return $this->configLexiconDetails[$appId]; } + + private function getLexiconEntry(string $appId, string $key): ?ConfigLexiconEntry { + 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; + } } diff --git a/lib/private/Repair/ConfigKeyMigration.php b/lib/private/Repair/ConfigKeyMigration.php new file mode 100644 index 0000000000000..da4aa153dc5a2 --- /dev/null +++ b/lib/private/Repair/ConfigKeyMigration.php @@ -0,0 +1,29 @@ +configManager->migrateConfigLexiconKeys(); + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index 5964a979e215e..c78decd90cb70 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -572,6 +572,7 @@ public function __construct($webRoot, \OC\Config $config) { $this->registerAlias(IAppConfig::class, \OC\AppConfig::class); $this->registerAlias(IUserConfig::class, \OC\Config\UserConfig::class); + $this->registerAlias(IAppManager::class, AppManager::class); $this->registerService(IFactory::class, function (Server $c) { return new \OC\L10N\Factory( @@ -780,21 +781,6 @@ public function __construct($webRoot, \OC\Config $config) { }); $this->registerAlias(ITempManager::class, TempManager::class); - - $this->registerService(AppManager::class, function (ContainerInterface $c) { - // TODO: use auto-wiring - return new \OC\App\AppManager( - $c->get(IUserSession::class), - $c->get(\OCP\IConfig::class), - $c->get(IGroupManager::class), - $c->get(ICacheFactory::class), - $c->get(IEventDispatcher::class), - $c->get(LoggerInterface::class), - $c->get(ServerVersion::class), - ); - }); - $this->registerAlias(IAppManager::class, AppManager::class); - $this->registerAlias(IDateTimeZone::class, DateTimeZone::class); $this->registerService(IDateTimeFormatter::class, function (Server $c) { diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php index 4f0fff8884e0f..24982ab9e8025 100644 --- a/lib/private/legacy/OC_App.php +++ b/lib/private/legacy/OC_App.php @@ -9,6 +9,7 @@ use OC\App\DependencyAnalyzer; use OC\App\Platform; use OC\AppFramework\Bootstrap\Coordinator; +use OC\Config\ConfigManager; use OC\DB\MigrationService; use OC\Installer; use OC\Repair; @@ -211,7 +212,7 @@ public function enable(string $appId, array $groups = []) { // Check if app is already downloaded /** @var Installer $installer */ - $installer = \OCP\Server::get(Installer::class); + $installer = Server::get(Installer::class); $isDownloaded = $installer->isDownloaded($appId); if (!$isDownloaded) { @@ -246,7 +247,7 @@ public static function getInstallPath(): ?string { } } - \OCP\Server::get(LoggerInterface::class)->error('No application directories are marked as writable.', ['app' => 'core']); + Server::get(LoggerInterface::class)->error('No application directories are marked as writable.', ['app' => 'core']); return null; } @@ -310,7 +311,7 @@ public static function findAppInDirectories(string $appId, bool $ignoreCache = f * @param string $appId * @param bool $refreshAppPath should be set to true only during install/upgrade * @return string|false - * @deprecated 11.0.0 use \OCP\Server::get(IAppManager)->getAppPath() + * @deprecated 11.0.0 use Server::get(IAppManager)->getAppPath() */ public static function getAppPath(string $appId, bool $refreshAppPath = false) { $appId = self::cleanAppId($appId); @@ -349,7 +350,7 @@ public static function getAppWebPath(string $appId) { */ public static function getAppVersionByPath(string $path): string { $infoFile = $path . '/appinfo/info.xml'; - $appData = \OCP\Server::get(IAppManager::class)->getAppInfoByPath($infoFile); + $appData = Server::get(IAppManager::class)->getAppInfoByPath($infoFile); return $appData['version'] ?? ''; } @@ -391,7 +392,7 @@ public static function getCurrentApp(): string { * @deprecated 20.0.0 Please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface */ public static function registerLogIn(array $entry) { - \OCP\Server::get(LoggerInterface::class)->debug('OC_App::registerLogIn() is deprecated, please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface'); + Server::get(LoggerInterface::class)->debug('OC_App::registerLogIn() is deprecated, please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface'); self::$altLogin[] = $entry; } @@ -400,11 +401,11 @@ public static function registerLogIn(array $entry) { */ public static function getAlternativeLogIns(): array { /** @var Coordinator $bootstrapCoordinator */ - $bootstrapCoordinator = \OCP\Server::get(Coordinator::class); + $bootstrapCoordinator = Server::get(Coordinator::class); foreach ($bootstrapCoordinator->getRegistrationContext()->getAlternativeLogins() as $registration) { if (!in_array(IAlternativeLogin::class, class_implements($registration->getService()), true)) { - \OCP\Server::get(LoggerInterface::class)->error('Alternative login option {option} does not implement {interface} and is therefore ignored.', [ + Server::get(LoggerInterface::class)->error('Alternative login option {option} does not implement {interface} and is therefore ignored.', [ 'option' => $registration->getService(), 'interface' => IAlternativeLogin::class, 'app' => $registration->getAppId(), @@ -414,9 +415,9 @@ public static function getAlternativeLogIns(): array { try { /** @var IAlternativeLogin $provider */ - $provider = \OCP\Server::get($registration->getService()); + $provider = Server::get($registration->getService()); } catch (ContainerExceptionInterface $e) { - \OCP\Server::get(LoggerInterface::class)->error('Alternative login option {option} can not be initialized.', + Server::get(LoggerInterface::class)->error('Alternative login option {option} can not be initialized.', [ 'exception' => $e, 'option' => $registration->getService(), @@ -433,7 +434,7 @@ public static function getAlternativeLogIns(): array { 'class' => $provider->getClass(), ]; } catch (Throwable $e) { - \OCP\Server::get(LoggerInterface::class)->error('Alternative login option {option} had an error while loading.', + Server::get(LoggerInterface::class)->error('Alternative login option {option} had an error while loading.', [ 'exception' => $e, 'option' => $registration->getService(), @@ -452,7 +453,7 @@ public static function getAlternativeLogIns(): array { * @deprecated 31.0.0 Use IAppManager::getAllAppsInAppsFolders instead */ public static function getAllApps(): array { - return \OCP\Server::get(IAppManager::class)->getAllAppsInAppsFolders(); + return Server::get(IAppManager::class)->getAllAppsInAppsFolders(); } /** @@ -461,7 +462,7 @@ public static function getAllApps(): array { * @deprecated 32.0.0 Use \OCP\Support\Subscription\IRegistry::delegateGetSupportedApps instead */ public function getSupportedApps(): array { - $subscriptionRegistry = \OCP\Server::get(\OCP\Support\Subscription\IRegistry::class); + $subscriptionRegistry = Server::get(\OCP\Support\Subscription\IRegistry::class); $supportedApps = $subscriptionRegistry->delegateGetSupportedApps(); return $supportedApps; } @@ -486,12 +487,12 @@ public function listAllApps(): array { if (!in_array($app, $blacklist)) { $info = $appManager->getAppInfo($app, false, $langCode); if (!is_array($info)) { - \OCP\Server::get(LoggerInterface::class)->error('Could not read app info file for app "' . $app . '"', ['app' => 'core']); + Server::get(LoggerInterface::class)->error('Could not read app info file for app "' . $app . '"', ['app' => 'core']); continue; } if (!isset($info['name'])) { - \OCP\Server::get(LoggerInterface::class)->error('App id "' . $app . '" has no name in appinfo', ['app' => 'core']); + Server::get(LoggerInterface::class)->error('App id "' . $app . '" has no name in appinfo', ['app' => 'core']); continue; } @@ -558,7 +559,7 @@ public function listAllApps(): array { public static function shouldUpgrade(string $app): bool { $versions = self::getAppVersions(); - $currentVersion = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppVersion($app); + $currentVersion = Server::get(\OCP\App\IAppManager::class)->getAppVersion($app); if ($currentVersion && isset($versions[$app])) { $installedVersion = $versions[$app]; if (!version_compare($currentVersion, $installedVersion, '=')) { @@ -647,7 +648,7 @@ public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ * @deprecated 32.0.0 Use IAppManager::getAppInstalledVersions or IAppConfig::getAppInstalledVersions instead */ public static function getAppVersions(): array { - return \OCP\Server::get(IAppConfig::class)->getAppInstalledVersions(); + return Server::get(IAppConfig::class)->getAppInstalledVersions(); } /** @@ -665,13 +666,13 @@ public static function updateApp(string $appId): bool { } if (is_file($appPath . '/appinfo/database.xml')) { - \OCP\Server::get(LoggerInterface::class)->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId); + Server::get(LoggerInterface::class)->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId); return false; } \OC::$server->getAppManager()->clearAppsCache(); $l = \OC::$server->getL10N('core'); - $appData = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppInfo($appId, false, $l->getLanguageCode()); + $appData = Server::get(\OCP\App\IAppManager::class)->getAppInfo($appId, false, $l->getLanguageCode()); $ignoreMaxApps = \OC::$server->getConfig()->getSystemValue('app_install_overwrite', []); $ignoreMax = in_array($appId, $ignoreMaxApps, true); @@ -711,9 +712,13 @@ public static function updateApp(string $appId): bool { self::setAppTypes($appId); - $version = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppVersion($appId); + $version = Server::get(\OCP\App\IAppManager::class)->getAppVersion($appId); \OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version); + // migrate eventual new config keys in the process + /** @psalm-suppress InternalMethod */ + Server::get(ConfigManager::class)->migrateConfigLexiconKeys($appId); + \OC::$server->get(IEventDispatcher::class)->dispatchTyped(new AppUpdateEvent($appId)); \OC::$server->get(IEventDispatcher::class)->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent( ManagerEvent::EVENT_APP_UPDATE, $appId diff --git a/lib/public/App/Events/AppUpdateEvent.php b/lib/public/App/Events/AppUpdateEvent.php index 344e7def080e3..2cf59ff7949b9 100644 --- a/lib/public/App/Events/AppUpdateEvent.php +++ b/lib/public/App/Events/AppUpdateEvent.php @@ -16,15 +16,13 @@ * @since 27.0.0 */ class AppUpdateEvent extends Event { - private string $appId; - /** * @since 27.0.0 */ - public function __construct(string $appId) { + public function __construct( + private readonly string $appId, + ) { parent::__construct(); - - $this->appId = $appId; } /** diff --git a/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php b/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php index d7d781d8e2654..d0d9b4cbd23e1 100644 --- a/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php +++ b/lib/unstable/Config/Lexicon/ConfigLexiconEntry.php @@ -17,6 +17,9 @@ * @experimental 31.0.0 */ class ConfigLexiconEntry { + /** @experimental 32.0.0 */ + public const RENAME_INVERT_BOOLEAN = 1; + private string $definition = ''; private ?string $default = null; @@ -26,6 +29,7 @@ class ConfigLexiconEntry { * @param string $definition optional description of config key available when using occ command * @param bool $lazy set config value as lazy * @param int $flags set flags + * @param string|null $rename previous config key to migrate config value from * @param bool $deprecated set config key as deprecated * * @experimental 31.0.0 @@ -40,6 +44,8 @@ public function __construct( private readonly bool $lazy = false, private readonly int $flags = 0, private readonly bool $deprecated = false, + private readonly ?string $rename = null, + private readonly int $options = 0, ) { /** @psalm-suppress UndefinedClass */ if (\OC::$CLI) { // only store definition if ran from CLI @@ -198,6 +204,25 @@ public function isFlagged(int $flag): bool { return (($flag & $this->getFlags()) === $flag); } + /** + * should be called/used only during migration/upgrade. + * link to an old config key. + * + * @return string|null not NULL if value can be imported from a previous key + * @experimental 32.0.0 + */ + public function getRename(): ?string { + return $this->rename; + } + + /** + * @experimental 32.0.0 + * @return bool TRUE if $option was set during the creation of the entry. + */ + public function hasOption(int $option): bool { + return (($option & $this->options) !== 0); + } + /** * returns if config key is set as deprecated * diff --git a/tests/Core/Command/Config/App/DeleteConfigTest.php b/tests/Core/Command/Config/App/DeleteConfigTest.php index 1e44c2aafe62e..6f7bfbf3a7cae 100644 --- a/tests/Core/Command/Config/App/DeleteConfigTest.php +++ b/tests/Core/Command/Config/App/DeleteConfigTest.php @@ -9,6 +9,7 @@ namespace Tests\Core\Command\Config\App; +use OC\Config\ConfigManager; use OC\Core\Command\Config\App\DeleteConfig; use OCP\IAppConfig; use PHPUnit\Framework\MockObject\MockObject; @@ -19,6 +20,7 @@ class DeleteConfigTest extends TestCase { protected IAppConfig&MockObject $appConfig; + protected ConfigManager&MockObject $configManager; protected InputInterface&MockObject $consoleInput; protected OutputInterface&MockObject $consoleOutput; protected Command $command; @@ -27,10 +29,11 @@ protected function setUp(): void { parent::setUp(); $this->appConfig = $this->createMock(IAppConfig::class); + $this->configManager = $this->createMock(ConfigManager::class); $this->consoleInput = $this->createMock(InputInterface::class); $this->consoleOutput = $this->createMock(OutputInterface::class); - $this->command = new DeleteConfig($this->appConfig); + $this->command = new DeleteConfig($this->appConfig, $this->configManager); } diff --git a/tests/Core/Command/Config/App/GetConfigTest.php b/tests/Core/Command/Config/App/GetConfigTest.php index 89a75c0b5277f..63a02f263d07d 100644 --- a/tests/Core/Command/Config/App/GetConfigTest.php +++ b/tests/Core/Command/Config/App/GetConfigTest.php @@ -9,6 +9,7 @@ namespace Tests\Core\Command\Config\App; +use OC\Config\ConfigManager; use OC\Core\Command\Config\App\GetConfig; use OCP\Exceptions\AppConfigUnknownKeyException; use OCP\IAppConfig; @@ -20,6 +21,7 @@ class GetConfigTest extends TestCase { protected IAppConfig&MockObject $appConfig; + protected ConfigManager&MockObject $configManager; protected InputInterface&MockObject $consoleInput; protected OutputInterface&MockObject $consoleOutput; protected Command $command; @@ -28,10 +30,11 @@ protected function setUp(): void { parent::setUp(); $this->appConfig = $this->createMock(IAppConfig::class); + $this->configManager = $this->createMock(ConfigManager::class); $this->consoleInput = $this->createMock(InputInterface::class); $this->consoleOutput = $this->createMock(OutputInterface::class); - $this->command = new GetConfig($this->appConfig); + $this->command = new GetConfig($this->appConfig, $this->configManager); } diff --git a/tests/Core/Command/Config/App/SetConfigTest.php b/tests/Core/Command/Config/App/SetConfigTest.php index 099471228b476..596439d85a800 100644 --- a/tests/Core/Command/Config/App/SetConfigTest.php +++ b/tests/Core/Command/Config/App/SetConfigTest.php @@ -10,6 +10,7 @@ namespace Tests\Core\Command\Config\App; use OC\AppConfig; +use OC\Config\ConfigManager; use OC\Core\Command\Config\App\SetConfig; use OCP\Exceptions\AppConfigUnknownKeyException; use OCP\IAppConfig; @@ -21,6 +22,7 @@ class SetConfigTest extends TestCase { protected IAppConfig&MockObject $appConfig; + protected ConfigManager&MockObject $configManager; protected InputInterface&MockObject $consoleInput; protected OutputInterface&MockObject $consoleOutput; protected Command $command; @@ -29,10 +31,11 @@ protected function setUp(): void { parent::setUp(); $this->appConfig = $this->createMock(AppConfig::class); + $this->configManager = $this->createMock(ConfigManager::class); $this->consoleInput = $this->createMock(InputInterface::class); $this->consoleOutput = $this->createMock(OutputInterface::class); - $this->command = new SetConfig($this->appConfig); + $this->command = new SetConfig($this->appConfig, $this->configManager); } diff --git a/tests/Core/Command/Config/ListConfigsTest.php b/tests/Core/Command/Config/ListConfigsTest.php index 12a6e6f1cf89e..216a61335799f 100644 --- a/tests/Core/Command/Config/ListConfigsTest.php +++ b/tests/Core/Command/Config/ListConfigsTest.php @@ -7,6 +7,7 @@ namespace Tests\Core\Command\Config; +use OC\Config\ConfigManager; use OC\Core\Command\Config\ListConfigs; use OC\SystemConfig; use OCP\IAppConfig; @@ -20,6 +21,8 @@ class ListConfigsTest extends TestCase { protected $appConfig; /** @var \PHPUnit\Framework\MockObject\MockObject */ protected $systemConfig; + /** @var \PHPUnit\Framework\MockObject\MockObject */ + protected $configManager; /** @var \PHPUnit\Framework\MockObject\MockObject */ protected $consoleInput; @@ -38,12 +41,17 @@ protected function setUp(): void { $appConfig = $this->appConfig = $this->getMockBuilder(IAppConfig::class) ->disableOriginalConstructor() ->getMock(); + $configManager = $this->configManager = $this->getMockBuilder(ConfigManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock(); $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock(); /** @var \OC\SystemConfig $systemConfig */ /** @var \OCP\IAppConfig $appConfig */ - $this->command = new ListConfigs($systemConfig, $appConfig); + /** @var ConfigManager $configManager */ + $this->command = new ListConfigs($systemConfig, $appConfig, $configManager); } public static function listData(): array { diff --git a/tests/lib/App/AppManagerTest.php b/tests/lib/App/AppManagerTest.php index 3dd5073731cba..5cd141c16a9e5 100644 --- a/tests/lib/App/AppManagerTest.php +++ b/tests/lib/App/AppManagerTest.php @@ -12,6 +12,7 @@ use OC\App\AppManager; use OC\AppConfig; +use OC\Config\ConfigManager; use OCP\App\AppPathNotFoundException; use OCP\App\Events\AppDisableEvent; use OCP\App\Events\AppEnableEvent; @@ -36,10 +37,7 @@ * @package Test\App */ class AppManagerTest extends TestCase { - /** - * @return AppConfig|MockObject - */ - protected function getAppConfig() { + protected function getAppConfig(): AppConfig&MockObject { $appConfig = []; $config = $this->createMock(AppConfig::class); @@ -86,33 +84,17 @@ protected function getAppConfig() { return $config; } - /** @var IUserSession|MockObject */ - protected $userSession; - - /** @var IConfig|MockObject */ - private $config; - - /** @var IGroupManager|MockObject */ - protected $groupManager; - - /** @var AppConfig|MockObject */ - protected $appConfig; - - /** @var ICache|MockObject */ - protected $cache; - - /** @var ICacheFactory|MockObject */ - protected $cacheFactory; - - /** @var IEventDispatcher|MockObject */ - protected $eventDispatcher; - - /** @var LoggerInterface|MockObject */ - protected $logger; - + protected IUserSession&MockObject $userSession; + private IConfig&MockObject $config; + protected IGroupManager&MockObject $groupManager; + protected AppConfig&MockObject $appConfig; + protected ICache&MockObject $cache; + protected ICacheFactory&MockObject $cacheFactory; + protected IEventDispatcher&MockObject $eventDispatcher; + protected LoggerInterface&MockObject $logger; protected IURLGenerator&MockObject $urlGenerator; - protected ServerVersion&MockObject $serverVersion; + protected ConfigManager&MockObject $configManager; /** @var IAppManager */ protected $manager; @@ -130,6 +112,7 @@ protected function setUp(): void { $this->logger = $this->createMock(LoggerInterface::class); $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->serverVersion = $this->createMock(ServerVersion::class); + $this->configManager = $this->createMock(ConfigManager::class); $this->overwriteService(AppConfig::class, $this->appConfig); $this->overwriteService(IURLGenerator::class, $this->urlGenerator); @@ -152,6 +135,7 @@ protected function setUp(): void { $this->eventDispatcher, $this->logger, $this->serverVersion, + $this->configManager, ); } @@ -295,6 +279,7 @@ public function testEnableAppForGroups(): void { $this->eventDispatcher, $this->logger, $this->serverVersion, + $this->configManager, ]) ->onlyMethods([ 'getAppPath', @@ -349,6 +334,7 @@ public function testEnableAppForGroupsAllowedTypes(array $appInfo): void { $this->eventDispatcher, $this->logger, $this->serverVersion, + $this->configManager, ]) ->onlyMethods([ 'getAppPath', @@ -411,6 +397,7 @@ public function testEnableAppForGroupsForbiddenTypes($type): void { $this->eventDispatcher, $this->logger, $this->serverVersion, + $this->configManager, ]) ->onlyMethods([ 'getAppPath', @@ -616,6 +603,7 @@ public function testGetAppsNeedingUpgrade(): void { $this->eventDispatcher, $this->logger, $this->serverVersion, + $this->configManager, ]) ->onlyMethods(['getAppInfo']) ->getMock(); @@ -676,6 +664,7 @@ public function testGetIncompatibleApps(): void { $this->eventDispatcher, $this->logger, $this->serverVersion, + $this->configManager, ]) ->onlyMethods(['getAppInfo']) ->getMock(); @@ -817,6 +806,7 @@ public function testGetAppVersion() { $this->eventDispatcher, $this->logger, $this->serverVersion, + $this->configManager, ]) ->onlyMethods([ 'getAppInfo', @@ -848,6 +838,7 @@ public function testGetAppVersionCore() { $this->eventDispatcher, $this->logger, $this->serverVersion, + $this->configManager, ]) ->onlyMethods([ 'getAppInfo', @@ -878,6 +869,7 @@ public function testGetAppVersionUnknown() { $this->eventDispatcher, $this->logger, $this->serverVersion, + $this->configManager, ]) ->onlyMethods([ 'getAppInfo', diff --git a/tests/lib/AppTest.php b/tests/lib/AppTest.php index 4813ea8c839c0..07205c730ce1f 100644 --- a/tests/lib/AppTest.php +++ b/tests/lib/AppTest.php @@ -10,6 +10,7 @@ use OC\App\AppManager; use OC\App\InfoParser; use OC\AppConfig; +use OC\Config\ConfigManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\IAppConfig; use OCP\ICacheFactory; @@ -573,6 +574,7 @@ private function registerAppConfig(AppConfig $appConfig) { Server::get(IEventDispatcher::class), Server::get(LoggerInterface::class), Server::get(ServerVersion::class), + \OCP\Server::get(ConfigManager::class), )); } diff --git a/tests/lib/Config/LexiconTest.php b/tests/lib/Config/LexiconTest.php index e9b763b179908..530767a7416d5 100644 --- a/tests/lib/Config/LexiconTest.php +++ b/tests/lib/Config/LexiconTest.php @@ -10,7 +10,9 @@ use NCU\Config\Exceptions\TypeConflictException; use NCU\Config\Exceptions\UnknownKeyException; use NCU\Config\IUserConfig; +use OC\AppConfig; use OC\AppFramework\Bootstrap\Coordinator; +use OC\Config\ConfigManager; use OCP\Exceptions\AppConfigTypeConflictException; use OCP\Exceptions\AppConfigUnknownKeyException; use OCP\IAppConfig; @@ -25,8 +27,10 @@ * @package Test */ class LexiconTest extends TestCase { + /** @var AppConfig */ private IAppConfig $appConfig; private IUserConfig $userConfig; + private ConfigManager $configManager; protected function setUp(): void { parent::setUp(); @@ -39,6 +43,7 @@ protected function setUp(): void { $this->appConfig = Server::get(IAppConfig::class); $this->userConfig = Server::get(IUserConfig::class); + $this->configManager = Server::get(ConfigManager::class); } protected function tearDown(): void { @@ -141,11 +146,61 @@ public function testUserLexiconWarning() { public function testUserLexiconSetException() { $this->expectException(UnknownKeyException::class); $this->userConfig->setValueString('user1', TestConfigLexicon_E::APPID, 'key_exception', 'new_value'); - $this->assertSame('', $this->userConfig->getValueString('user1', TestConfigLexicon_E::APPID, 'key3', '')); + $this->assertSame('', $this->userConfig->getValueString('user1', TestConfigLexicon_E::APPID, 'key5', '')); } public function testUserLexiconGetException() { $this->expectException(UnknownKeyException::class); $this->userConfig->getValueString('user1', TestConfigLexicon_E::APPID, 'key_exception'); } + + public function testAppConfigLexiconRenameSetNewValue() { + $this->assertSame(12345, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'key3', 123)); + $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'old_key3', 994); + $this->assertSame(994, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'key3', 123)); + } + + public function testAppConfigLexiconRenameSetOldValuePreMigration() { + $this->appConfig->ignoreLexiconAliases(true); + $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'old_key3', 993); + $this->appConfig->ignoreLexiconAliases(false); + $this->assertSame(12345, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'key3', 123)); + } + + public function testAppConfigLexiconRenameSetOldValuePostMigration() { + $this->appConfig->ignoreLexiconAliases(true); + $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'old_key3', 994); + $this->appConfig->ignoreLexiconAliases(false); + $this->configManager->migrateConfigLexiconKeys(TestConfigLexicon_I::APPID); + $this->assertSame(994, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'key3', 123)); + } + + public function testAppConfigLexiconRenameGetNewValue() { + $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'key3', 981); + $this->assertSame(981, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'old_key3', 123)); + } + + public function testAppConfigLexiconRenameGetOldValuePreMigration() { + $this->appConfig->ignoreLexiconAliases(true); + $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'key3', 984); + $this->assertSame(123, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'old_key3', 123)); + $this->appConfig->ignoreLexiconAliases(false); + } + + public function testAppConfigLexiconRenameGetOldValuePostMigration() { + $this->appConfig->ignoreLexiconAliases(true); + $this->appConfig->setValueInt(TestConfigLexicon_I::APPID, 'key3', 987); + $this->appConfig->ignoreLexiconAliases(false); + $this->configManager->migrateConfigLexiconKeys(TestConfigLexicon_I::APPID); + $this->assertSame(987, $this->appConfig->getValueInt(TestConfigLexicon_I::APPID, 'old_key3', 123)); + } + + public function testAppConfigLexiconRenameInvertBoolean() { + $this->appConfig->ignoreLexiconAliases(true); + $this->appConfig->setValueBool(TestConfigLexicon_I::APPID, 'old_key4', true); + $this->appConfig->ignoreLexiconAliases(false); + $this->assertSame(true, $this->appConfig->getValueBool(TestConfigLexicon_I::APPID, 'key4')); + $this->configManager->migrateConfigLexiconKeys(TestConfigLexicon_I::APPID); + $this->assertSame(false, $this->appConfig->getValueBool(TestConfigLexicon_I::APPID, 'key4')); + } } diff --git a/tests/lib/Config/TestConfigLexicon_I.php b/tests/lib/Config/TestConfigLexicon_I.php index 497c62acecb7b..6fb7921b6e68d 100644 --- a/tests/lib/Config/TestConfigLexicon_I.php +++ b/tests/lib/Config/TestConfigLexicon_I.php @@ -25,8 +25,9 @@ public function getStrictness(): ConfigLexiconStrictness { public function getAppConfigs(): array { return [ new ConfigLexiconEntry('key1', ValueType::STRING, 'abcde', 'test key', true, IAppConfig::FLAG_SENSITIVE), - new ConfigLexiconEntry('key2', ValueType::INT, 12345, 'test key', false) - + new ConfigLexiconEntry('key2', ValueType::INT, 12345, 'test key', false), + new ConfigLexiconEntry('key3', ValueType::INT, 12345, 'test key', true, rename: 'old_key3'), + new ConfigLexiconEntry('key4', ValueType::BOOL, 12345, 'test key', true, rename: 'old_key4', options: ConfigLexiconEntry::RENAME_INVERT_BOOLEAN), ]; } diff --git a/tests/lib/DB/AdapterTest.php b/tests/lib/DB/AdapterTest.php index 2fc7c5089a348..bf95ad26a971b 100644 --- a/tests/lib/DB/AdapterTest.php +++ b/tests/lib/DB/AdapterTest.php @@ -16,7 +16,7 @@ class AdapterTest extends TestCase { public function setUp(): void { $this->connection = Server::get(IDBConnection::class); - $this->appId = uniqid('test_db_adapter', true); + $this->appId = substr(uniqid('test_db_adapter', true), 0, 32); } public function tearDown(): void {