loadConfigServices(); $this->appConfig->clearCache(); $this->userConfig->clearCacheAll(); } /** * Use the rename values from the list of ConfigLexiconEntry defined in each app ConfigLexicon * to migrate config value to a new config key. * Migration will only occur if new config key has no value in database. * The previous value from the key set in rename will be deleted from the database when migration * is over. * * This method should be mainly called during a new upgrade or when a new app is enabled. * * @param string|null $appId when set to NULL the method will be executed for all enabled apps of the instance * * @internal * @since 32.0.0 * @see Entry */ public function migrateConfigLexiconKeys(?string $appId = null): void { if ($appId === null) { $this->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); } /** * Upgrade stored data in case of changes in the lexicon. * Heavy process to be executed on core and app upgrade. */ public function updateLexiconEntries(string $appId): void { $this->loadConfigServices(); $this->updateLexiconAppConfigEntries($appId); $this->updateLexiconUserConfigEntries($appId); } /** * Apply modification on the lexicon to the stored app config values: * * - Upgrade AppConfig entries if set as lazy/not-lazy */ private function updateLexiconAppConfigEntries(string $appId): void { $lexicon = $this->appConfig->getConfigDetailsFromLexicon($appId); foreach ($lexicon['entries'] as $entry) { // update laziness $this->appConfig->updateLazy($appId, $entry->getKey(), $entry->isLazy()); } } /** * Apply modification on the lexicon to the stored user preferences values: * * - Upgrade UserConfig entries if set as indexed/not-indexed * - Upgrade UserConfig entries if set as lazy/not-lazy */ private function updateLexiconUserConfigEntries(string $appId): void { $lexicon = $this->userConfig->getConfigDetailsFromLexicon($appId); foreach ($lexicon['entries'] as $entry) { // upgrade based on index flag $this->userConfig->updateGlobalIndexed($appId, $entry->getKey(), $entry->isFlagged(IUserConfig::FLAG_INDEXED)); // update laziness $this->userConfig->updateGlobalLazy($appId, $entry->getKey(), $entry->isLazy()); } } /** * 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, Entry $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, Entry $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, ?Entry $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(Entry::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; } }