From 0249e3a2f5a97769d4fd9b3ecced5c8b2feadb77 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Tue, 16 Sep 2025 10:46:45 -0100 Subject: [PATCH] fix(userconfig): set 'mail' as indexed Signed-off-by: Maxence Lange --- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + apps/settings/lib/AppInfo/Application.php | 3 + apps/settings/lib/ConfigLexicon.php | 38 ++++++++++ lib/private/App/AppManager.php | 1 + lib/private/Config/ConfigManager.php | 15 ++++ lib/private/Config/UserConfig.php | 75 ++++++++++++------- lib/private/Repair/ConfigKeyMigration.php | 1 + 8 files changed, 108 insertions(+), 27 deletions(-) create mode 100644 apps/settings/lib/ConfigLexicon.php diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php index bb63026da77..b12f345e05b 100644 --- a/apps/settings/composer/composer/autoload_classmap.php +++ b/apps/settings/composer/composer/autoload_classmap.php @@ -19,6 +19,7 @@ return array( 'OCA\\Settings\\Command\\AdminDelegation\\Add' => $baseDir . '/../lib/Command/AdminDelegation/Add.php', 'OCA\\Settings\\Command\\AdminDelegation\\Remove' => $baseDir . '/../lib/Command/AdminDelegation/Remove.php', 'OCA\\Settings\\Command\\AdminDelegation\\Show' => $baseDir . '/../lib/Command/AdminDelegation/Show.php', + 'OCA\\Settings\\ConfigLexicon' => $baseDir . '/../lib/ConfigLexicon.php', 'OCA\\Settings\\Controller\\AISettingsController' => $baseDir . '/../lib/Controller/AISettingsController.php', 'OCA\\Settings\\Controller\\AdminSettingsController' => $baseDir . '/../lib/Controller/AdminSettingsController.php', 'OCA\\Settings\\Controller\\AppSettingsController' => $baseDir . '/../lib/Controller/AppSettingsController.php', diff --git a/apps/settings/composer/composer/autoload_static.php b/apps/settings/composer/composer/autoload_static.php index cca48b409ad..e6e2ed9f45c 100644 --- a/apps/settings/composer/composer/autoload_static.php +++ b/apps/settings/composer/composer/autoload_static.php @@ -34,6 +34,7 @@ class ComposerStaticInitSettings 'OCA\\Settings\\Command\\AdminDelegation\\Add' => __DIR__ . '/..' . '/../lib/Command/AdminDelegation/Add.php', 'OCA\\Settings\\Command\\AdminDelegation\\Remove' => __DIR__ . '/..' . '/../lib/Command/AdminDelegation/Remove.php', 'OCA\\Settings\\Command\\AdminDelegation\\Show' => __DIR__ . '/..' . '/../lib/Command/AdminDelegation/Show.php', + 'OCA\\Settings\\ConfigLexicon' => __DIR__ . '/..' . '/../lib/ConfigLexicon.php', 'OCA\\Settings\\Controller\\AISettingsController' => __DIR__ . '/..' . '/../lib/Controller/AISettingsController.php', 'OCA\\Settings\\Controller\\AdminSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AdminSettingsController.php', 'OCA\\Settings\\Controller\\AppSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AppSettingsController.php', diff --git a/apps/settings/lib/AppInfo/Application.php b/apps/settings/lib/AppInfo/Application.php index 6e59e56fe82..de007a6978f 100644 --- a/apps/settings/lib/AppInfo/Application.php +++ b/apps/settings/lib/AppInfo/Application.php @@ -12,6 +12,7 @@ use OC\AppFramework\Utility\TimeFactory; use OC\Authentication\Events\AppPasswordCreatedEvent; use OC\Authentication\Token\IProvider; use OC\Server; +use OCA\Settings\ConfigLexicon; use OCA\Settings\Hooks; use OCA\Settings\Listener\AppPasswordCreatedActivityListener; use OCA\Settings\Listener\GroupRemovedListener; @@ -112,6 +113,8 @@ class Application extends App implements IBootstrap { $context->registerSearchProvider(AppSearch::class); $context->registerSearchProvider(UserSearch::class); + $context->registerConfigLexicon(ConfigLexicon::class); + // Register listeners $context->registerEventListener(AppPasswordCreatedEvent::class, AppPasswordCreatedActivityListener::class); $context->registerEventListener(UserAddedEvent::class, UserAddedToGroupActivityListener::class); diff --git a/apps/settings/lib/ConfigLexicon.php b/apps/settings/lib/ConfigLexicon.php new file mode 100644 index 00000000000..8d2035dbc73 --- /dev/null +++ b/apps/settings/lib/ConfigLexicon.php @@ -0,0 +1,38 @@ +configManager->migrateConfigLexiconKeys($appId); + $this->configManager->updateLexiconEntries($appId); $this->dispatcher->dispatchTyped(new AppUpdateEvent($appId)); $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent( diff --git a/lib/private/Config/ConfigManager.php b/lib/private/Config/ConfigManager.php index 28397402249..7c763e2ae37 100644 --- a/lib/private/Config/ConfigManager.php +++ b/lib/private/Config/ConfigManager.php @@ -82,6 +82,21 @@ class ConfigManager { $this->userConfig->ignoreLexiconAliases(false); } + /** + * Upgrade stored data in case of changes in the lexicon. + * Heavy process to be executed on core and app upgrade. + * + * - upgrade UserConfig entries if set as indexed + */ + public function updateLexiconEntries(string $appId): void { + $this->loadConfigServices(); + $lexicon = $this->userConfig->getConfigDetailsFromLexicon($appId); + foreach ($lexicon['entries'] as $entry) { + // upgrade based on index flag + $this->userConfig->updateGlobalIndexed($appId, $entry->getKey(), $entry->isFlagged(UserConfig::FLAG_INDEXED)); + } + } + /** * config services cannot be load at __construct() or install will fail */ diff --git a/lib/private/Config/UserConfig.php b/lib/private/Config/UserConfig.php index d0d19561e2d..bf86cfa493d 100644 --- a/lib/private/Config/UserConfig.php +++ b/lib/private/Config/UserConfig.php @@ -477,40 +477,55 @@ class UserConfig implements IUserConfig { $this->assertParams('', $app, $key, allowEmptyUser: true); $this->matchAndApplyLexiconDefinition('', $app, $key); + $lexiconEntry = $this->getLexiconEntry($app, $key); + if ($lexiconEntry?->isFlagged(self::FLAG_INDEXED) === false) { + $this->logger->notice('UserConfig+Lexicon: using searchUsersByTypedValue on a config key not set as indexed'); + } + $qb = $this->connection->getQueryBuilder(); $qb->from('preferences'); $qb->select('userid'); $qb->where($qb->expr()->eq('appid', $qb->createNamedParameter($app))); $qb->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key))); - // search within 'indexed' OR 'configvalue' only if 'flags' is set as not indexed - // TODO: when implementing config lexicon remove the searches on 'configvalue' if value is set as indexed $configValueColumn = ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) ? $qb->expr()->castColumn('configvalue', IQueryBuilder::PARAM_STR) : 'configvalue'; if (is_array($value)) { - $where = $qb->expr()->orX( - $qb->expr()->in('indexed', $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR_ARRAY)), - $qb->expr()->andX( - $qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)), - $qb->expr()->in($configValueColumn, $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR_ARRAY)) - ) - ); - } else { - if ($caseInsensitive) { + $where = $qb->expr()->in('indexed', $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR_ARRAY)); + // in case lexicon does not exist for this key - or is not set as indexed - we keep searching for non-index entries if 'flags' is set as not indexed + if ($lexiconEntry?->isFlagged(self::FLAG_INDEXED) !== true) { $where = $qb->expr()->orX( - $qb->expr()->eq($qb->func()->lower('indexed'), $qb->createNamedParameter(strtolower($value))), + $where, $qb->expr()->andX( $qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)), - $qb->expr()->eq($qb->func()->lower($configValueColumn), $qb->createNamedParameter(strtolower($value))) + $qb->expr()->in($configValueColumn, $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR_ARRAY)) ) ); + } + } else { + if ($caseInsensitive) { + $where = $qb->expr()->eq($qb->func()->lower('indexed'), $qb->createNamedParameter(strtolower($value))); + // in case lexicon does not exist for this key - or is not set as indexed - we keep searching for non-index entries if 'flags' is set as not indexed + if ($lexiconEntry?->isFlagged(self::FLAG_INDEXED) !== true) { + $where = $qb->expr()->orX( + $where, + $qb->expr()->andX( + $qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)), + $qb->expr()->eq($qb->func()->lower($configValueColumn), $qb->createNamedParameter(strtolower($value))) + ) + ); + } } else { - $where = $qb->expr()->orX( - $qb->expr()->eq('indexed', $qb->createNamedParameter($value)), - $qb->expr()->andX( - $qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)), - $qb->expr()->eq($configValueColumn, $qb->createNamedParameter($value)) - ) - ); + $where = $qb->expr()->eq('indexed', $qb->createNamedParameter($value)); + // in case lexicon does not exist for this key - or is not set as indexed - we keep searching for non-index entries if 'flags' is set as not indexed + if ($lexiconEntry?->isFlagged(self::FLAG_INDEXED) !== true) { + $where = $qb->expr()->orX( + $where, + $qb->expr()->andX( + $qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)), + $qb->expr()->eq($configValueColumn, $qb->createNamedParameter($value)) + ) + ); + } } } @@ -1408,13 +1423,19 @@ class UserConfig implements IUserConfig { $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); - } catch (UnknownKeyException) { - // should not happen and can be ignored - } - } + $bitPosition = log(self::FLAG_INDEXED) / log(2); // emulate base-2 logarithm (log2) + $bitOperation = ($indexed) ? '`flags` | (1 << ' . $bitPosition . ')' : '`flags` & ~(1 << ' . $bitPosition . ')'; + + $update = $this->connection->getQueryBuilder(); + $update->update('preferences') + ->set('flags', $update->createFunction($bitOperation)) + ->set('indexed', ($indexed) ? 'configvalue' : $update->createNamedParameter('')) + ->where( + $update->expr()->eq('appid', $update->createNamedParameter($app)), + $update->expr()->eq('configkey', $update->createNamedParameter($key)) + ); + + $update->executeStatement(); // we clear all cache $this->clearCacheAll(); diff --git a/lib/private/Repair/ConfigKeyMigration.php b/lib/private/Repair/ConfigKeyMigration.php index da4aa153dc5..dcca43d65df 100644 --- a/lib/private/Repair/ConfigKeyMigration.php +++ b/lib/private/Repair/ConfigKeyMigration.php @@ -25,5 +25,6 @@ class ConfigKeyMigration implements IRepairStep { public function run(IOutput $output) { $this->configManager->migrateConfigLexiconKeys(); + $this->configManager->updateLexiconEntries('core'); } }