You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
267 lines
8.1 KiB
267 lines
8.1 KiB
<?php
|
|
|
|
declare(strict_types=1);
|
|
/**
|
|
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OC\Config;
|
|
|
|
use JsonException;
|
|
use NCU\Config\Exceptions\TypeConflictException;
|
|
use NCU\Config\IUserConfig;
|
|
use NCU\Config\Lexicon\ConfigLexiconEntry;
|
|
use NCU\Config\Lexicon\Preset;
|
|
use NCU\Config\ValueType;
|
|
use OC\AppConfig;
|
|
use OCP\App\IAppManager;
|
|
use OCP\IAppConfig;
|
|
use OCP\IConfig;
|
|
use OCP\Server;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
/**
|
|
* tools to maintains configurations
|
|
*
|
|
* @since 32.0.0
|
|
*/
|
|
class ConfigManager {
|
|
/** @since 32.0.0 */
|
|
public const PRESET_CONFIGKEY = 'config_preset';
|
|
|
|
/** @var AppConfig|null $appConfig */
|
|
private ?IAppConfig $appConfig = null;
|
|
/** @var UserConfig|null $userConfig */
|
|
private ?IUserConfig $userConfig = null;
|
|
|
|
public function __construct(
|
|
private readonly IConfig $config,
|
|
private readonly LoggerInterface $logger,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @see ConfigLexiconEntry
|
|
* @internal
|
|
* @since 32.0.0
|
|
* @param string|null $appId when set to NULL the method will be executed for all enabled apps of the instance
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* store in config.php the new preset
|
|
* refresh cached preset
|
|
*/
|
|
public function setLexiconPreset(Preset $preset): void {
|
|
$this->config->setSystemValue(self::PRESET_CONFIGKEY, $preset->value);
|
|
$this->loadConfigServices();
|
|
$this->appConfig->clearCache();
|
|
$this->userConfig->clearCacheAll();
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|