Merge pull request #52832 from nextcloud/feat/noid/lexicon-migrate-keys

feat(lexicon): migrate config key/value
This commit is contained in:
Maxence Lange 2025-06-24 12:54:13 -01:00 committed by GitHub
commit d161e07cf7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 605 additions and 126 deletions

View file

@ -7,12 +7,14 @@ declare(strict_types=1);
*/
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();
}

View file

@ -9,7 +9,6 @@ declare(strict_types=1);
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 @@ class SetConfig extends Base {
}
$value = (string)$input->getOption('value');
switch ($type) {
case IAppConfig::VALUE_MIXED:
$updated = $this->appConfig->setValueMixed($appName, $configName, $value, $lazy, $sensitive);
@ -172,34 +170,19 @@ class SetConfig extends Base {
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;
}
}

View file

@ -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 @@ class ListConfigs extends Base {
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 @@ class ListConfigs extends Base {
$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('<error>Invalid app value given</error>');
return 1;

View file

@ -1190,6 +1190,7 @@ return array(
'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 @@ return array(
'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',

View file

@ -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',

View file

@ -8,6 +8,7 @@ namespace OC\App;
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\INavigationManager;
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 @@ class AppManager implements IAppManager {
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 @@ class AppManager implements IAppManager {
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 @@ class AppManager implements IAppManager {
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 @@ class AppManager implements IAppManager {
]);
}
$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 @@ class AppManager implements IAppManager {
ManagerEvent::EVENT_APP_ENABLE, $appId
));
$this->clearAppsCache();
$this->configManager->migrateConfigLexiconKeys($appId);
}
/**
@ -626,6 +631,8 @@ class AppManager implements IAppManager {
ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
));
$this->clearAppsCache();
$this->configManager->migrateConfigLexiconKeys($appId);
}
/**

View file

@ -15,6 +15,7 @@ use NCU\Config\Lexicon\ConfigLexiconEntry;
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\IAppConfig;
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<array-key, array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
/** @var array<string, array{entries: array<string, ConfigLexiconEntry>, aliases: array<string, string>, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
private array $configLexiconDetails = [];
private bool $ignoreLexiconAliases = false;
/** @var ?array<string, string> */
private ?array $appVersionsCache = null;
@ -117,6 +120,7 @@ class AppConfig implements IAppConfig {
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 @@ class AppConfig implements IAppConfig {
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 @@ class AppConfig implements IAppConfig {
* @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 @@ class AppConfig implements IAppConfig {
): string {
try {
$lazy = ($lazy === null) ? $this->isLazy($app, $key) : $lazy;
} catch (AppConfigUnknownKeyException $e) {
} catch (AppConfigUnknownKeyException) {
return $default;
}
@ -429,6 +437,7 @@ class AppConfig implements IAppConfig {
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 @@ class AppConfig implements IAppConfig {
$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 @@ class AppConfig implements IAppConfig {
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 @@ class AppConfig implements IAppConfig {
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 @@ class AppConfig implements IAppConfig {
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 @@ class AppConfig implements IAppConfig {
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 @@ class AppConfig implements IAppConfig {
*/
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 @@ class AppConfig implements IAppConfig {
*/
public function getValue($app, $key, $default = null) {
$this->loadConfig($app);
$this->matchAndApplyLexiconDefinition($app, $key);
return $this->fastCache[$app][$key] ?? $default;
}
@ -1372,7 +1396,7 @@ class AppConfig implements IAppConfig {
foreach ($values as $key => $value) {
try {
$type = $this->getValueType($app, $key, $lazy);
} catch (AppConfigUnknownKeyException $e) {
} catch (AppConfigUnknownKeyException) {
continue;
}
@ -1556,7 +1580,8 @@ class AppConfig implements IAppConfig {
}
/**
* 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 @@ class AppConfig implements IAppConfig {
*/
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 @@ class AppConfig implements IAppConfig {
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 @@ class AppConfig implements IAppConfig {
* extract details from registered $appId's config lexicon
*
* @param string $appId
* @internal
*
* @return array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}
* @return array{entries: array<string, ConfigLexiconEntry>, aliases: array<string, string>, 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 @@ class AppConfig implements IAppConfig {
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
*

View file

@ -0,0 +1,250 @@
<?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\ValueType;
use OC\AppConfig;
use OCP\App\IAppManager;
use OCP\IAppConfig;
use OCP\Server;
use Psr\Log\LoggerInterface;
/**
* tools to maintains configurations
*
* @since 32.0.0
*/
class ConfigManager {
/** @var AppConfig|null $appConfig */
private ?IAppConfig $appConfig = null;
/** @var UserConfig|null $userConfig */
private ?IUserConfig $userConfig = null;
public function __construct(
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);
}
/**
* 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;
}
}

View file

@ -25,6 +25,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder;
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<string, boolean> ['user_id' => bool] */
private array $lazyLoaded = [];
/** @var array<array-key, array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
/** @var array<string, array{entries: array<string, ConfigLexiconEntry>, aliases: array<string, string>, 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 @@ class UserConfig implements IUserConfig {
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 @@ class UserConfig implements IUserConfig {
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 @@ class UserConfig implements IUserConfig {
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 @@ class UserConfig implements IUserConfig {
* @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 @@ class UserConfig implements IUserConfig {
?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 @@ class UserConfig implements IUserConfig {
*/
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 @@ class UserConfig implements IUserConfig {
string $default = '',
?bool $lazy = false,
): string {
$this->matchAndApplyLexiconDefinition($userId, $app, $key);
try {
$lazy ??= $this->isLazy($userId, $app, $key);
} catch (UnknownKeyException) {
@ -710,6 +720,7 @@ class UserConfig implements IUserConfig {
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 @@ class UserConfig implements IUserConfig {
}
$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 @@ class UserConfig implements IUserConfig {
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 @@ class UserConfig implements IUserConfig {
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 @@ class UserConfig implements IUserConfig {
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 @@ class UserConfig implements IUserConfig {
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 @@ class UserConfig implements IUserConfig {
*/
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 @@ class UserConfig implements IUserConfig {
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 @@ class UserConfig implements IUserConfig {
*/
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 @@ class UserConfig implements IUserConfig {
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 @@ class UserConfig implements IUserConfig {
*/
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 @@ class UserConfig implements IUserConfig {
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 @@ class UserConfig implements IUserConfig {
*/
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 @@ class UserConfig implements IUserConfig {
*/
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 @@ class UserConfig implements IUserConfig {
*/
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 @@ class UserConfig implements IUserConfig {
}
/**
* 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 @@ class UserConfig implements IUserConfig {
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 @@ class UserConfig implements IUserConfig {
* extract details from registered $appId's config lexicon
*
* @param string $appId
* @internal
*
* @return array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}
* @return array{entries: array<string, ConfigLexiconEntry>, aliases: array<string, string>, 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;
}
}

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Repair;
use OC\Config\ConfigManager;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
class ConfigKeyMigration implements IRepairStep {
public function __construct(
private ConfigManager $configManager,
) {
}
public function getName(): string {
return 'Migrate config keys';
}
public function run(IOutput $output) {
$this->configManager->migrateConfigLexiconKeys();
}
}

View file

@ -572,6 +572,7 @@ class Server extends ServerContainer implements IServerContainer {
$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 @@ class Server extends ServerContainer implements IServerContainer {
});
$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) {

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
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 @@ class OC_App {
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 @@ class OC_App {
}
}
\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 @@ class OC_App {
* @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 @@ class OC_App {
*/
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 @@ class OC_App {
* @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 @@ class OC_App {
*/
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 @@ class OC_App {
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 @@ class OC_App {
'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 @@ class OC_App {
* @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 @@ class OC_App {
* @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 @@ class OC_App {
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 @@ class OC_App {
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 @@ class OC_App {
* @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 @@ class OC_App {
}
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 @@ class OC_App {
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

View file

@ -16,15 +16,13 @@ use OCP\EventDispatcher\Event;
* @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;
}
/**

View file

@ -17,6 +17,9 @@ use NCU\Config\ValueType;
* @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 @@ class ConfigLexiconEntry {
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 @@ class ConfigLexiconEntry {
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
*

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
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 @@ use Test\TestCase;
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 @@ class DeleteConfigTest extends TestCase {
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);
}

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
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 @@ use Test\TestCase;
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 @@ class GetConfigTest extends TestCase {
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);
}

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
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 @@ use Test\TestCase;
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 @@ class SetConfigTest extends TestCase {
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);
}

View file

@ -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 @@ class ListConfigsTest extends TestCase {
$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 {

View file

@ -12,6 +12,7 @@ namespace Test\App;
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 @@ use Test\TestCase;
* @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 @@ class AppManagerTest extends TestCase {
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 @@ class AppManagerTest extends TestCase {
$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 @@ class AppManagerTest extends TestCase {
$this->eventDispatcher,
$this->logger,
$this->serverVersion,
$this->configManager,
);
}
@ -295,6 +279,7 @@ class AppManagerTest extends TestCase {
$this->eventDispatcher,
$this->logger,
$this->serverVersion,
$this->configManager,
])
->onlyMethods([
'getAppPath',
@ -349,6 +334,7 @@ class AppManagerTest extends TestCase {
$this->eventDispatcher,
$this->logger,
$this->serverVersion,
$this->configManager,
])
->onlyMethods([
'getAppPath',
@ -411,6 +397,7 @@ class AppManagerTest extends TestCase {
$this->eventDispatcher,
$this->logger,
$this->serverVersion,
$this->configManager,
])
->onlyMethods([
'getAppPath',
@ -616,6 +603,7 @@ class AppManagerTest extends TestCase {
$this->eventDispatcher,
$this->logger,
$this->serverVersion,
$this->configManager,
])
->onlyMethods(['getAppInfo'])
->getMock();
@ -676,6 +664,7 @@ class AppManagerTest extends TestCase {
$this->eventDispatcher,
$this->logger,
$this->serverVersion,
$this->configManager,
])
->onlyMethods(['getAppInfo'])
->getMock();
@ -817,6 +806,7 @@ class AppManagerTest extends TestCase {
$this->eventDispatcher,
$this->logger,
$this->serverVersion,
$this->configManager,
])
->onlyMethods([
'getAppInfo',
@ -848,6 +838,7 @@ class AppManagerTest extends TestCase {
$this->eventDispatcher,
$this->logger,
$this->serverVersion,
$this->configManager,
])
->onlyMethods([
'getAppInfo',
@ -878,6 +869,7 @@ class AppManagerTest extends TestCase {
$this->eventDispatcher,
$this->logger,
$this->serverVersion,
$this->configManager,
])
->onlyMethods([
'getAppInfo',

View file

@ -10,6 +10,7 @@ namespace Test;
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 @@ class AppTest extends \Test\TestCase {
Server::get(IEventDispatcher::class),
Server::get(LoggerInterface::class),
Server::get(ServerVersion::class),
\OCP\Server::get(ConfigManager::class),
));
}

View file

@ -10,7 +10,9 @@ namespace Tests\lib\Config;
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 @@ use Test\TestCase;
* @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 @@ class LexiconTest extends TestCase {
$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 @@ class LexiconTest extends TestCase {
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'));
}
}

View file

@ -25,8 +25,9 @@ class TestConfigLexicon_I implements IConfigLexicon {
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),
];
}

View file

@ -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 {