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; } }