Merge pull request #55139 from nextcloud/fix/noid/index-settings-mail-on-upgrade

fix(userconfig): set 'mail' as indexed
pull/54963/merge
Maxence Lange 3 weeks ago committed by GitHub
commit 22c0e76e23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      apps/settings/composer/composer/autoload_classmap.php
  2. 1
      apps/settings/composer/composer/autoload_static.php
  3. 3
      apps/settings/lib/AppInfo/Application.php
  4. 38
      apps/settings/lib/ConfigLexicon.php
  5. 1
      lib/private/App/AppManager.php
  6. 15
      lib/private/Config/ConfigManager.php
  7. 86
      lib/private/Config/UserConfig.php
  8. 1
      lib/private/Repair/ConfigKeyMigration.php
  9. 50
      tests/lib/Config/LexiconTest.php
  10. 33
      tests/lib/Config/TestLexicon_UserIndexed.php
  11. 32
      tests/lib/Config/TestLexicon_UserIndexedRemove.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',

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

@ -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);

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Settings;
use OCP\Config\IUserConfig;
use OCP\Config\Lexicon\Entry;
use OCP\Config\Lexicon\ILexicon;
use OCP\Config\Lexicon\Strictness;
use OCP\Config\ValueType;
/**
* Config Lexicon for settings.
*
* Please Add & Manage your Config Keys in that file and keep the Lexicon up to date!
*/
class ConfigLexicon implements ILexicon {
public const USER_SETTINGS_EMAIL = 'email';
public function getStrictness(): Strictness {
return Strictness::IGNORE;
}
public function getAppConfigs(): array {
return [];
}
public function getUserConfigs(): array {
return [
new Entry(key: self::USER_SETTINGS_EMAIL, type: ValueType::STRING, defaultRaw: '', definition: 'account mail address', flags: IUserConfig::FLAG_INDEXED),
];
}
}

@ -1089,6 +1089,7 @@ class AppManager implements IAppManager {
// migrate eventual new config keys in the process
/** @psalm-suppress InternalMethod */
$this->configManager->migrateConfigLexiconKeys($appId);
$this->configManager->updateLexiconEntries($appId);
$this->dispatcher->dispatchTyped(new AppUpdateEvent($appId));
$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(

@ -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(IUserConfig::FLAG_INDEXED));
}
}
/**
* config services cannot be load at __construct() or install will fail
*/

@ -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 config key ' . $app . '/' . $key . ' which is 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,14 +1423,33 @@ 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
}
$update = $this->connection->getQueryBuilder();
$update->update('preferences')
->where(
$update->expr()->eq('appid', $update->createNamedParameter($app)),
$update->expr()->eq('configkey', $update->createNamedParameter($key))
);
// switching flags 'indexed' on and off is about adding/removing the bit value on the correct entries
if ($indexed) {
$update->set('indexed', $update->func()->substring('configvalue', $update->createNamedParameter(1, IQueryBuilder::PARAM_INT), $update->createNamedParameter(64, IQueryBuilder::PARAM_INT)));
$update->set('flags', $update->func()->add('flags', $update->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)));
$update->andWhere(
$update->expr()->neq($update->expr()->castColumn(
$update->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), IQueryBuilder::PARAM_INT), $update->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)
));
} else {
// emptying field 'indexed' if key is not set as indexed anymore
$update->set('indexed', $update->createNamedParameter(''));
$update->set('flags', $update->func()->subtract('flags', $update->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)));
$update->andWhere(
$update->expr()->eq($update->expr()->castColumn(
$update->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), IQueryBuilder::PARAM_INT), $update->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)
));
}
$update->executeStatement();
// we clear all cache
$this->clearCacheAll();
}

@ -25,5 +25,6 @@ class ConfigKeyMigration implements IRepairStep {
public function run(IOutput $output) {
$this->configManager->migrateConfigLexiconKeys();
$this->configManager->updateLexiconEntries('core');
}
}

@ -60,11 +60,15 @@ class LexiconTest extends TestCase {
$this->appConfig->deleteApp(TestLexicon_N::APPID);
$this->appConfig->deleteApp(TestLexicon_W::APPID);
$this->appConfig->deleteApp(TestLexicon_E::APPID);
$this->appConfig->deleteApp(TestLexicon_UserIndexed::APPID);
$this->appConfig->deleteApp(TestLexicon_UserIndexedRemove::APPID);
$this->userConfig->deleteApp(TestConfigLexicon_I::APPID);
$this->userConfig->deleteApp(TestLexicon_N::APPID);
$this->userConfig->deleteApp(TestLexicon_W::APPID);
$this->userConfig->deleteApp(TestLexicon_E::APPID);
$this->userConfig->deleteApp(TestLexicon_UserIndexed::APPID);
$this->userConfig->deleteApp(TestLexicon_UserIndexedRemove::APPID);
}
public function testAppLexiconSetCorrect() {
@ -234,4 +238,50 @@ class LexiconTest extends TestCase {
$this->presetManager->setLexiconPreset(Preset::FAMILY);
$this->assertSame('family', $this->userConfig->getValueString('user1', TestLexicon_E::APPID, 'key3'));
}
public function testLexiconIndexedUpdate() {
$this->userConfig->setValueString('user1', TestLexicon_UserIndexed::APPID, 'key1', 'abcd');
$this->userConfig->setValueString('user2', TestLexicon_UserIndexed::APPID, 'key1', '1234', flags: 64);
$this->userConfig->setValueString('user3', TestLexicon_UserIndexed::APPID, 'key1', 'qwer', flags: IUserConfig::FLAG_INDEXED);
$this->userConfig->setValueString('user4', TestLexicon_UserIndexed::APPID, 'key1', 'uiop', flags: 64 | IUserConfig::FLAG_INDEXED);
$bootstrapCoordinator = Server::get(Coordinator::class);
$bootstrapCoordinator->getRegistrationContext()?->registerConfigLexicon(TestLexicon_UserIndexed::APPID, TestLexicon_UserIndexed::class);
$this->userConfig->clearCacheAll();
$this->configManager->updateLexiconEntries(TestLexicon_UserIndexed::APPID);
$this->assertTrue($this->userConfig->isIndexed('user1', TestLexicon_UserIndexed::APPID, 'key1'));
$this->assertTrue($this->userConfig->isIndexed('user2', TestLexicon_UserIndexed::APPID, 'key1'));
$this->assertTrue($this->userConfig->isIndexed('user3', TestLexicon_UserIndexed::APPID, 'key1'));
$this->assertTrue($this->userConfig->isIndexed('user4', TestLexicon_UserIndexed::APPID, 'key1'));
$this->assertSame(2, $this->userConfig->getValueFlags('user1', TestLexicon_UserIndexed::APPID, 'key1'));
$this->assertSame(66, $this->userConfig->getValueFlags('user2', TestLexicon_UserIndexed::APPID, 'key1'));
$this->assertSame(2, $this->userConfig->getValueFlags('user3', TestLexicon_UserIndexed::APPID, 'key1'));
$this->assertSame(66, $this->userConfig->getValueFlags('user4', TestLexicon_UserIndexed::APPID, 'key1'));
}
public function testLexiconIndexedUpdateRemove() {
$this->userConfig->setValueString('user1', TestLexicon_UserIndexedRemove::APPID, 'key1', 'abcd');
$this->userConfig->setValueString('user2', TestLexicon_UserIndexedRemove::APPID, 'key1', '1234', flags: 64);
$this->userConfig->setValueString('user3', TestLexicon_UserIndexedRemove::APPID, 'key1', 'qwer', flags: IUserConfig::FLAG_INDEXED);
$this->userConfig->setValueString('user4', TestLexicon_UserIndexedRemove::APPID, 'key1', 'uiop', flags: 64 | IUserConfig::FLAG_INDEXED);
$bootstrapCoordinator = Server::get(Coordinator::class);
$bootstrapCoordinator->getRegistrationContext()?->registerConfigLexicon(TestLexicon_UserIndexedRemove::APPID, TestLexicon_UserIndexedRemove::class);
$this->userConfig->clearCacheAll();
$this->configManager->updateLexiconEntries(TestLexicon_UserIndexedRemove::APPID);
$this->assertFalse($this->userConfig->isIndexed('user1', TestLexicon_UserIndexedRemove::APPID, 'key1'));
$this->assertFalse($this->userConfig->isIndexed('user2', TestLexicon_UserIndexedRemove::APPID, 'key1'));
$this->assertFalse($this->userConfig->isIndexed('user3', TestLexicon_UserIndexedRemove::APPID, 'key1'));
$this->assertFalse($this->userConfig->isIndexed('user4', TestLexicon_UserIndexedRemove::APPID, 'key1'));
$this->assertSame(0, $this->userConfig->getValueFlags('user1', TestLexicon_UserIndexedRemove::APPID, 'key1'));
$this->assertSame(64, $this->userConfig->getValueFlags('user2', TestLexicon_UserIndexedRemove::APPID, 'key1'));
$this->assertSame(0, $this->userConfig->getValueFlags('user3', TestLexicon_UserIndexedRemove::APPID, 'key1'));
$this->assertSame(64, $this->userConfig->getValueFlags('user4', TestLexicon_UserIndexedRemove::APPID, 'key1'));
}
}

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace Tests\lib\Config;
use OCP\Config\IUserConfig;
use OCP\Config\Lexicon\Entry;
use OCP\Config\Lexicon\ILexicon;
use OCP\Config\Lexicon\Strictness;
use OCP\Config\ValueType;
class TestLexicon_UserIndexed implements ILexicon {
public const APPID = 'lexicon_user_indexed';
public function getStrictness(): Strictness {
return Strictness::EXCEPTION;
}
public function getAppConfigs(): array {
return [
];
}
public function getUserConfigs(): array {
return [
new Entry(key: 'key1', type: ValueType::STRING, defaultRaw: '', definition: 'test key', flags: IUserConfig::FLAG_INDEXED),
];
}
}

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace Tests\lib\Config;
use OCP\Config\Lexicon\Entry;
use OCP\Config\Lexicon\ILexicon;
use OCP\Config\Lexicon\Strictness;
use OCP\Config\ValueType;
class TestLexicon_UserIndexedRemove implements ILexicon {
public const APPID = 'lexicon_user_not_indexed';
public function getStrictness(): Strictness {
return Strictness::EXCEPTION;
}
public function getAppConfigs(): array {
return [
];
}
public function getUserConfigs(): array {
return [
new Entry(key: 'key1', type: ValueType::STRING, defaultRaw: '', definition: 'test key'),
];
}
}
Loading…
Cancel
Save