Skip to content

Commit 5bfc3e4

Browse files
Merge pull request #55159 from nextcloud/backport/55139/stable32
[stable32] fix(userconfig): set 'mail' as indexed
2 parents 85ef043 + 545f930 commit 5bfc3e4

File tree

11 files changed

+235
-26
lines changed

11 files changed

+235
-26
lines changed

apps/settings/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
'OCA\\Settings\\Command\\AdminDelegation\\Add' => $baseDir . '/../lib/Command/AdminDelegation/Add.php',
2020
'OCA\\Settings\\Command\\AdminDelegation\\Remove' => $baseDir . '/../lib/Command/AdminDelegation/Remove.php',
2121
'OCA\\Settings\\Command\\AdminDelegation\\Show' => $baseDir . '/../lib/Command/AdminDelegation/Show.php',
22+
'OCA\\Settings\\ConfigLexicon' => $baseDir . '/../lib/ConfigLexicon.php',
2223
'OCA\\Settings\\Controller\\AISettingsController' => $baseDir . '/../lib/Controller/AISettingsController.php',
2324
'OCA\\Settings\\Controller\\AdminSettingsController' => $baseDir . '/../lib/Controller/AdminSettingsController.php',
2425
'OCA\\Settings\\Controller\\AppSettingsController' => $baseDir . '/../lib/Controller/AppSettingsController.php',

apps/settings/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class ComposerStaticInitSettings
3434
'OCA\\Settings\\Command\\AdminDelegation\\Add' => __DIR__ . '/..' . '/../lib/Command/AdminDelegation/Add.php',
3535
'OCA\\Settings\\Command\\AdminDelegation\\Remove' => __DIR__ . '/..' . '/../lib/Command/AdminDelegation/Remove.php',
3636
'OCA\\Settings\\Command\\AdminDelegation\\Show' => __DIR__ . '/..' . '/../lib/Command/AdminDelegation/Show.php',
37+
'OCA\\Settings\\ConfigLexicon' => __DIR__ . '/..' . '/../lib/ConfigLexicon.php',
3738
'OCA\\Settings\\Controller\\AISettingsController' => __DIR__ . '/..' . '/../lib/Controller/AISettingsController.php',
3839
'OCA\\Settings\\Controller\\AdminSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AdminSettingsController.php',
3940
'OCA\\Settings\\Controller\\AppSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AppSettingsController.php',

apps/settings/lib/AppInfo/Application.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OC\Authentication\Events\AppPasswordCreatedEvent;
1313
use OC\Authentication\Token\IProvider;
1414
use OC\Server;
15+
use OCA\Settings\ConfigLexicon;
1516
use OCA\Settings\Hooks;
1617
use OCA\Settings\Listener\AppPasswordCreatedActivityListener;
1718
use OCA\Settings\Listener\GroupRemovedListener;
@@ -112,6 +113,8 @@ public function register(IRegistrationContext $context): void {
112113
$context->registerSearchProvider(AppSearch::class);
113114
$context->registerSearchProvider(UserSearch::class);
114115

116+
$context->registerConfigLexicon(ConfigLexicon::class);
117+
115118
// Register listeners
116119
$context->registerEventListener(AppPasswordCreatedEvent::class, AppPasswordCreatedActivityListener::class);
117120
$context->registerEventListener(UserAddedEvent::class, UserAddedToGroupActivityListener::class);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OCA\Settings;
10+
11+
use OCP\Config\IUserConfig;
12+
use OCP\Config\Lexicon\Entry;
13+
use OCP\Config\Lexicon\ILexicon;
14+
use OCP\Config\Lexicon\Strictness;
15+
use OCP\Config\ValueType;
16+
17+
/**
18+
* Config Lexicon for settings.
19+
*
20+
* Please Add & Manage your Config Keys in that file and keep the Lexicon up to date!
21+
*/
22+
class ConfigLexicon implements ILexicon {
23+
public const USER_SETTINGS_EMAIL = 'email';
24+
25+
public function getStrictness(): Strictness {
26+
return Strictness::IGNORE;
27+
}
28+
29+
public function getAppConfigs(): array {
30+
return [];
31+
}
32+
33+
public function getUserConfigs(): array {
34+
return [
35+
new Entry(key: self::USER_SETTINGS_EMAIL, type: ValueType::STRING, defaultRaw: '', definition: 'account mail address', flags: IUserConfig::FLAG_INDEXED),
36+
];
37+
}
38+
}

lib/private/App/AppManager.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,7 @@ public function upgradeApp(string $appId): bool {
10891089
// migrate eventual new config keys in the process
10901090
/** @psalm-suppress InternalMethod */
10911091
$this->configManager->migrateConfigLexiconKeys($appId);
1092+
$this->configManager->updateLexiconEntries($appId);
10921093

10931094
$this->dispatcher->dispatchTyped(new AppUpdateEvent($appId));
10941095
$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(

lib/private/Config/ConfigManager.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@ public function migrateConfigLexiconKeys(?string $appId = null): void {
8282
$this->userConfig->ignoreLexiconAliases(false);
8383
}
8484

85+
/**
86+
* Upgrade stored data in case of changes in the lexicon.
87+
* Heavy process to be executed on core and app upgrade.
88+
*
89+
* - upgrade UserConfig entries if set as indexed
90+
*/
91+
public function updateLexiconEntries(string $appId): void {
92+
$this->loadConfigServices();
93+
$lexicon = $this->userConfig->getConfigDetailsFromLexicon($appId);
94+
foreach ($lexicon['entries'] as $entry) {
95+
// upgrade based on index flag
96+
$this->userConfig->updateGlobalIndexed($appId, $entry->getKey(), $entry->isFlagged(IUserConfig::FLAG_INDEXED));
97+
}
98+
}
99+
85100
/**
86101
* config services cannot be load at __construct() or install will fail
87102
*/

lib/private/Config/UserConfig.php

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -477,40 +477,55 @@ private function searchUsersByTypedValue(string $app, string $key, string|array
477477
$this->assertParams('', $app, $key, allowEmptyUser: true);
478478
$this->matchAndApplyLexiconDefinition('', $app, $key);
479479

480+
$lexiconEntry = $this->getLexiconEntry($app, $key);
481+
if ($lexiconEntry?->isFlagged(self::FLAG_INDEXED) === false) {
482+
$this->logger->notice('UserConfig+Lexicon: using searchUsersByTypedValue on config key ' . $app . '/' . $key . ' which is not set as indexed');
483+
}
484+
480485
$qb = $this->connection->getQueryBuilder();
481486
$qb->from('preferences');
482487
$qb->select('userid');
483488
$qb->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
484489
$qb->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
485490

486-
// search within 'indexed' OR 'configvalue' only if 'flags' is set as not indexed
487-
// TODO: when implementing config lexicon remove the searches on 'configvalue' if value is set as indexed
488491
$configValueColumn = ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) ? $qb->expr()->castColumn('configvalue', IQueryBuilder::PARAM_STR) : 'configvalue';
489492
if (is_array($value)) {
490-
$where = $qb->expr()->orX(
491-
$qb->expr()->in('indexed', $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR_ARRAY)),
492-
$qb->expr()->andX(
493-
$qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)),
494-
$qb->expr()->in($configValueColumn, $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR_ARRAY))
495-
)
496-
);
497-
} else {
498-
if ($caseInsensitive) {
493+
$where = $qb->expr()->in('indexed', $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR_ARRAY));
494+
// 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
495+
if ($lexiconEntry?->isFlagged(self::FLAG_INDEXED) !== true) {
499496
$where = $qb->expr()->orX(
500-
$qb->expr()->eq($qb->func()->lower('indexed'), $qb->createNamedParameter(strtolower($value))),
497+
$where,
501498
$qb->expr()->andX(
502499
$qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)),
503-
$qb->expr()->eq($qb->func()->lower($configValueColumn), $qb->createNamedParameter(strtolower($value)))
500+
$qb->expr()->in($configValueColumn, $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR_ARRAY))
504501
)
505502
);
503+
}
504+
} else {
505+
if ($caseInsensitive) {
506+
$where = $qb->expr()->eq($qb->func()->lower('indexed'), $qb->createNamedParameter(strtolower($value)));
507+
// 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
508+
if ($lexiconEntry?->isFlagged(self::FLAG_INDEXED) !== true) {
509+
$where = $qb->expr()->orX(
510+
$where,
511+
$qb->expr()->andX(
512+
$qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)),
513+
$qb->expr()->eq($qb->func()->lower($configValueColumn), $qb->createNamedParameter(strtolower($value)))
514+
)
515+
);
516+
}
506517
} else {
507-
$where = $qb->expr()->orX(
508-
$qb->expr()->eq('indexed', $qb->createNamedParameter($value)),
509-
$qb->expr()->andX(
510-
$qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)),
511-
$qb->expr()->eq($configValueColumn, $qb->createNamedParameter($value))
512-
)
513-
);
518+
$where = $qb->expr()->eq('indexed', $qb->createNamedParameter($value));
519+
// 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
520+
if ($lexiconEntry?->isFlagged(self::FLAG_INDEXED) !== true) {
521+
$where = $qb->expr()->orX(
522+
$where,
523+
$qb->expr()->andX(
524+
$qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)),
525+
$qb->expr()->eq($configValueColumn, $qb->createNamedParameter($value))
526+
)
527+
);
528+
}
514529
}
515530
}
516531

@@ -1408,14 +1423,33 @@ public function updateGlobalIndexed(string $app, string $key, bool $indexed): vo
14081423
$this->assertParams('', $app, $key, allowEmptyUser: true);
14091424
$this->matchAndApplyLexiconDefinition('', $app, $key);
14101425

1411-
foreach (array_keys($this->getValuesByUsers($app, $key)) as $userId) {
1412-
try {
1413-
$this->updateIndexed($userId, $app, $key, $indexed);
1414-
} catch (UnknownKeyException) {
1415-
// should not happen and can be ignored
1416-
}
1426+
$update = $this->connection->getQueryBuilder();
1427+
$update->update('preferences')
1428+
->where(
1429+
$update->expr()->eq('appid', $update->createNamedParameter($app)),
1430+
$update->expr()->eq('configkey', $update->createNamedParameter($key))
1431+
);
1432+
1433+
// switching flags 'indexed' on and off is about adding/removing the bit value on the correct entries
1434+
if ($indexed) {
1435+
$update->set('indexed', $update->func()->substring('configvalue', $update->createNamedParameter(1, IQueryBuilder::PARAM_INT), $update->createNamedParameter(64, IQueryBuilder::PARAM_INT)));
1436+
$update->set('flags', $update->func()->add('flags', $update->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)));
1437+
$update->andWhere(
1438+
$update->expr()->neq($update->expr()->castColumn(
1439+
$update->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), IQueryBuilder::PARAM_INT), $update->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)
1440+
));
1441+
} else {
1442+
// emptying field 'indexed' if key is not set as indexed anymore
1443+
$update->set('indexed', $update->createNamedParameter(''));
1444+
$update->set('flags', $update->func()->subtract('flags', $update->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)));
1445+
$update->andWhere(
1446+
$update->expr()->eq($update->expr()->castColumn(
1447+
$update->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), IQueryBuilder::PARAM_INT), $update->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)
1448+
));
14171449
}
14181450

1451+
$update->executeStatement();
1452+
14191453
// we clear all cache
14201454
$this->clearCacheAll();
14211455
}

lib/private/Repair/ConfigKeyMigration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ public function getName(): string {
2525

2626
public function run(IOutput $output) {
2727
$this->configManager->migrateConfigLexiconKeys();
28+
$this->configManager->updateLexiconEntries('core');
2829
}
2930
}

tests/lib/Config/LexiconTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,15 @@ protected function tearDown(): void {
6060
$this->appConfig->deleteApp(TestLexicon_N::APPID);
6161
$this->appConfig->deleteApp(TestLexicon_W::APPID);
6262
$this->appConfig->deleteApp(TestLexicon_E::APPID);
63+
$this->appConfig->deleteApp(TestLexicon_UserIndexed::APPID);
64+
$this->appConfig->deleteApp(TestLexicon_UserIndexedRemove::APPID);
6365

6466
$this->userConfig->deleteApp(TestConfigLexicon_I::APPID);
6567
$this->userConfig->deleteApp(TestLexicon_N::APPID);
6668
$this->userConfig->deleteApp(TestLexicon_W::APPID);
6769
$this->userConfig->deleteApp(TestLexicon_E::APPID);
70+
$this->userConfig->deleteApp(TestLexicon_UserIndexed::APPID);
71+
$this->userConfig->deleteApp(TestLexicon_UserIndexedRemove::APPID);
6872
}
6973

7074
public function testAppLexiconSetCorrect() {
@@ -234,4 +238,50 @@ public function testUserConfigLexiconPresets() {
234238
$this->presetManager->setLexiconPreset(Preset::FAMILY);
235239
$this->assertSame('family', $this->userConfig->getValueString('user1', TestLexicon_E::APPID, 'key3'));
236240
}
241+
242+
public function testLexiconIndexedUpdate() {
243+
$this->userConfig->setValueString('user1', TestLexicon_UserIndexed::APPID, 'key1', 'abcd');
244+
$this->userConfig->setValueString('user2', TestLexicon_UserIndexed::APPID, 'key1', '1234', flags: 64);
245+
$this->userConfig->setValueString('user3', TestLexicon_UserIndexed::APPID, 'key1', 'qwer', flags: IUserConfig::FLAG_INDEXED);
246+
$this->userConfig->setValueString('user4', TestLexicon_UserIndexed::APPID, 'key1', 'uiop', flags: 64 | IUserConfig::FLAG_INDEXED);
247+
248+
$bootstrapCoordinator = Server::get(Coordinator::class);
249+
$bootstrapCoordinator->getRegistrationContext()?->registerConfigLexicon(TestLexicon_UserIndexed::APPID, TestLexicon_UserIndexed::class);
250+
$this->userConfig->clearCacheAll();
251+
252+
$this->configManager->updateLexiconEntries(TestLexicon_UserIndexed::APPID);
253+
254+
$this->assertTrue($this->userConfig->isIndexed('user1', TestLexicon_UserIndexed::APPID, 'key1'));
255+
$this->assertTrue($this->userConfig->isIndexed('user2', TestLexicon_UserIndexed::APPID, 'key1'));
256+
$this->assertTrue($this->userConfig->isIndexed('user3', TestLexicon_UserIndexed::APPID, 'key1'));
257+
$this->assertTrue($this->userConfig->isIndexed('user4', TestLexicon_UserIndexed::APPID, 'key1'));
258+
259+
$this->assertSame(2, $this->userConfig->getValueFlags('user1', TestLexicon_UserIndexed::APPID, 'key1'));
260+
$this->assertSame(66, $this->userConfig->getValueFlags('user2', TestLexicon_UserIndexed::APPID, 'key1'));
261+
$this->assertSame(2, $this->userConfig->getValueFlags('user3', TestLexicon_UserIndexed::APPID, 'key1'));
262+
$this->assertSame(66, $this->userConfig->getValueFlags('user4', TestLexicon_UserIndexed::APPID, 'key1'));
263+
}
264+
265+
public function testLexiconIndexedUpdateRemove() {
266+
$this->userConfig->setValueString('user1', TestLexicon_UserIndexedRemove::APPID, 'key1', 'abcd');
267+
$this->userConfig->setValueString('user2', TestLexicon_UserIndexedRemove::APPID, 'key1', '1234', flags: 64);
268+
$this->userConfig->setValueString('user3', TestLexicon_UserIndexedRemove::APPID, 'key1', 'qwer', flags: IUserConfig::FLAG_INDEXED);
269+
$this->userConfig->setValueString('user4', TestLexicon_UserIndexedRemove::APPID, 'key1', 'uiop', flags: 64 | IUserConfig::FLAG_INDEXED);
270+
271+
$bootstrapCoordinator = Server::get(Coordinator::class);
272+
$bootstrapCoordinator->getRegistrationContext()?->registerConfigLexicon(TestLexicon_UserIndexedRemove::APPID, TestLexicon_UserIndexedRemove::class);
273+
$this->userConfig->clearCacheAll();
274+
275+
$this->configManager->updateLexiconEntries(TestLexicon_UserIndexedRemove::APPID);
276+
277+
$this->assertFalse($this->userConfig->isIndexed('user1', TestLexicon_UserIndexedRemove::APPID, 'key1'));
278+
$this->assertFalse($this->userConfig->isIndexed('user2', TestLexicon_UserIndexedRemove::APPID, 'key1'));
279+
$this->assertFalse($this->userConfig->isIndexed('user3', TestLexicon_UserIndexedRemove::APPID, 'key1'));
280+
$this->assertFalse($this->userConfig->isIndexed('user4', TestLexicon_UserIndexedRemove::APPID, 'key1'));
281+
282+
$this->assertSame(0, $this->userConfig->getValueFlags('user1', TestLexicon_UserIndexedRemove::APPID, 'key1'));
283+
$this->assertSame(64, $this->userConfig->getValueFlags('user2', TestLexicon_UserIndexedRemove::APPID, 'key1'));
284+
$this->assertSame(0, $this->userConfig->getValueFlags('user3', TestLexicon_UserIndexedRemove::APPID, 'key1'));
285+
$this->assertSame(64, $this->userConfig->getValueFlags('user4', TestLexicon_UserIndexedRemove::APPID, 'key1'));
286+
}
237287
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-only
7+
*/
8+
9+
namespace Tests\lib\Config;
10+
11+
use OCP\Config\IUserConfig;
12+
use OCP\Config\Lexicon\Entry;
13+
use OCP\Config\Lexicon\ILexicon;
14+
use OCP\Config\Lexicon\Strictness;
15+
use OCP\Config\ValueType;
16+
17+
class TestLexicon_UserIndexed implements ILexicon {
18+
public const APPID = 'lexicon_user_indexed';
19+
public function getStrictness(): Strictness {
20+
return Strictness::EXCEPTION;
21+
}
22+
23+
public function getAppConfigs(): array {
24+
return [
25+
];
26+
}
27+
28+
public function getUserConfigs(): array {
29+
return [
30+
new Entry(key: 'key1', type: ValueType::STRING, defaultRaw: '', definition: 'test key', flags: IUserConfig::FLAG_INDEXED),
31+
];
32+
}
33+
}

0 commit comments

Comments
 (0)