Completed
Push — master ( 74804b...a3183f )
by
unknown
17:36 queued 10s
created
lib/private/Config/UserConfig.php 2 patches
Indentation   +2003 added lines, -2003 removed lines patch added patch discarded remove patch
@@ -46,2007 +46,2007 @@
 block discarded – undo
46 46
  * @since 31.0.0
47 47
  */
48 48
 class UserConfig implements IUserConfig {
49
-	private const USER_MAX_LENGTH = 64;
50
-	private const APP_MAX_LENGTH = 32;
51
-	private const KEY_MAX_LENGTH = 64;
52
-	private const INDEX_MAX_LENGTH = 64;
53
-	private const ENCRYPTION_PREFIX = '$UserConfigEncryption$';
54
-	private const ENCRYPTION_PREFIX_LENGTH = 22; // strlen(self::ENCRYPTION_PREFIX)
55
-
56
-	/** @var array<string, array<string, array<string, mixed>>> [ass'user_id' => ['app_id' => ['key' => 'value']]] */
57
-	private array $fastCache = [];   // cache for normal config keys
58
-	/** @var array<string, array<string, array<string, mixed>>> ['user_id' => ['app_id' => ['key' => 'value']]] */
59
-	private array $lazyCache = [];   // cache for lazy config keys
60
-	/** @var array<string, array<string, array<string, array<string, mixed>>>> ['user_id' => ['app_id' => ['key' => ['type' => ValueType, 'flags' => bitflag]]]] */
61
-	private array $valueDetails = [];  // type for all config values
62
-	/** @var array<string, boolean> ['user_id' => bool] */
63
-	private array $fastLoaded = [];
64
-	/** @var array<string, boolean> ['user_id' => bool] */
65
-	private array $lazyLoaded = [];
66
-	/** @var array<string, array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
67
-	private array $configLexiconDetails = [];
68
-	private bool $ignoreLexiconAliases = false;
69
-	private array $strictnessApplied = [];
70
-
71
-	public function __construct(
72
-		protected IDBConnection $connection,
73
-		protected IConfig $config,
74
-		private readonly ConfigManager $configManager,
75
-		private readonly PresetManager $presetManager,
76
-		protected LoggerInterface $logger,
77
-		protected ICrypto $crypto,
78
-	) {
79
-	}
80
-
81
-	/**
82
-	 * @inheritDoc
83
-	 *
84
-	 * @param string $appId optional id of app
85
-	 *
86
-	 * @return list<string> list of userIds
87
-	 * @since 31.0.0
88
-	 */
89
-	public function getUserIds(string $appId = ''): array {
90
-		$this->assertParams(app: $appId, allowEmptyUser: true, allowEmptyApp: true);
91
-
92
-		$qb = $this->connection->getQueryBuilder();
93
-		$qb->from('preferences');
94
-		$qb->select('userid');
95
-		$qb->groupBy('userid');
96
-		if ($appId !== '') {
97
-			$qb->where($qb->expr()->eq('appid', $qb->createNamedParameter($appId)));
98
-		}
99
-
100
-		$result = $qb->executeQuery();
101
-		$rows = $result->fetchAll();
102
-		$userIds = [];
103
-		foreach ($rows as $row) {
104
-			$userIds[] = $row['userid'];
105
-		}
106
-
107
-		return $userIds;
108
-	}
109
-
110
-	/**
111
-	 * @inheritDoc
112
-	 *
113
-	 * @return list<string> list of app ids
114
-	 * @since 31.0.0
115
-	 */
116
-	public function getApps(string $userId): array {
117
-		$this->assertParams($userId, allowEmptyApp: true);
118
-		$this->loadConfigAll($userId);
119
-		$apps = array_merge(array_keys($this->fastCache[$userId] ?? []), array_keys($this->lazyCache[$userId] ?? []));
120
-		sort($apps);
121
-
122
-		return array_values(array_unique($apps));
123
-	}
124
-
125
-	/**
126
-	 * @inheritDoc
127
-	 *
128
-	 * @param string $userId id of the user
129
-	 * @param string $app id of the app
130
-	 *
131
-	 * @return list<string> list of stored config keys
132
-	 * @since 31.0.0
133
-	 */
134
-	public function getKeys(string $userId, string $app): array {
135
-		$this->assertParams($userId, $app);
136
-		$this->loadConfigAll($userId);
137
-		// array_merge() will remove numeric keys (here config keys), so addition arrays instead
138
-		$keys = array_map('strval', array_keys(($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? [])));
139
-		sort($keys);
140
-
141
-		return array_values(array_unique($keys));
142
-	}
143
-
144
-	/**
145
-	 * @inheritDoc
146
-	 *
147
-	 * @param string $userId id of the user
148
-	 * @param string $app id of the app
149
-	 * @param string $key config key
150
-	 * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
151
-	 *
152
-	 * @return bool TRUE if key exists
153
-	 * @since 31.0.0
154
-	 */
155
-	public function hasKey(string $userId, string $app, string $key, ?bool $lazy = false): bool {
156
-		$this->assertParams($userId, $app, $key);
157
-		$this->loadConfig($userId, $lazy);
158
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
159
-
160
-		if ($lazy === null) {
161
-			$appCache = $this->getValues($userId, $app);
162
-			return isset($appCache[$key]);
163
-		}
164
-
165
-		if ($lazy) {
166
-			return isset($this->lazyCache[$userId][$app][$key]);
167
-		}
168
-
169
-		return isset($this->fastCache[$userId][$app][$key]);
170
-	}
171
-
172
-	/**
173
-	 * @inheritDoc
174
-	 *
175
-	 * @param string $userId id of the user
176
-	 * @param string $app id of the app
177
-	 * @param string $key config key
178
-	 * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
179
-	 *
180
-	 * @return bool
181
-	 * @throws UnknownKeyException if config key is not known
182
-	 * @since 31.0.0
183
-	 */
184
-	public function isSensitive(string $userId, string $app, string $key, ?bool $lazy = false): bool {
185
-		$this->assertParams($userId, $app, $key);
186
-		$this->loadConfig($userId, $lazy);
187
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
188
-
189
-		if (!isset($this->valueDetails[$userId][$app][$key])) {
190
-			throw new UnknownKeyException('unknown config key');
191
-		}
192
-
193
-		return $this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags']);
194
-	}
195
-
196
-	/**
197
-	 * @inheritDoc
198
-	 *
199
-	 * @param string $userId id of the user
200
-	 * @param string $app id of the app
201
-	 * @param string $key config key
202
-	 * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
203
-	 *
204
-	 * @return bool
205
-	 * @throws UnknownKeyException if config key is not known
206
-	 * @since 31.0.0
207
-	 */
208
-	public function isIndexed(string $userId, string $app, string $key, ?bool $lazy = false): bool {
209
-		$this->assertParams($userId, $app, $key);
210
-		$this->loadConfig($userId, $lazy);
211
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
212
-
213
-		if (!isset($this->valueDetails[$userId][$app][$key])) {
214
-			throw new UnknownKeyException('unknown config key');
215
-		}
216
-
217
-		return $this->isFlagged(self::FLAG_INDEXED, $this->valueDetails[$userId][$app][$key]['flags']);
218
-	}
219
-
220
-	/**
221
-	 * @inheritDoc
222
-	 *
223
-	 * @param string $userId id of the user
224
-	 * @param string $app if of the app
225
-	 * @param string $key config key
226
-	 *
227
-	 * @return bool TRUE if config is lazy loaded
228
-	 * @throws UnknownKeyException if config key is not known
229
-	 * @see IUserConfig for details about lazy loading
230
-	 * @since 31.0.0
231
-	 */
232
-	public function isLazy(string $userId, string $app, string $key): bool {
233
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
234
-
235
-		// there is a huge probability the non-lazy config are already loaded
236
-		// meaning that we can start by only checking if a current non-lazy key exists
237
-		if ($this->hasKey($userId, $app, $key, false)) {
238
-			// meaning key is not lazy.
239
-			return false;
240
-		}
241
-
242
-		// as key is not found as non-lazy, we load and search in the lazy config
243
-		if ($this->hasKey($userId, $app, $key, true)) {
244
-			return true;
245
-		}
246
-
247
-		throw new UnknownKeyException('unknown config key');
248
-	}
249
-
250
-	/**
251
-	 * @inheritDoc
252
-	 *
253
-	 * @param string $userId id of the user
254
-	 * @param string $app id of the app
255
-	 * @param string $prefix config keys prefix to search
256
-	 * @param bool $filtered TRUE to hide sensitive config values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
257
-	 *
258
-	 * @return array<string, string|int|float|bool|array> [key => value]
259
-	 * @since 31.0.0
260
-	 */
261
-	public function getValues(
262
-		string $userId,
263
-		string $app,
264
-		string $prefix = '',
265
-		bool $filtered = false,
266
-	): array {
267
-		$this->assertParams($userId, $app, $prefix);
268
-		// if we want to filter values, we need to get sensitivity
269
-		$this->loadConfigAll($userId);
270
-		// array_merge() will remove numeric keys (here config keys), so addition arrays instead
271
-		$values = array_filter(
272
-			$this->formatAppValues($userId, $app, ($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? []), $filtered),
273
-			function (string $key) use ($prefix): bool {
274
-				// filter values based on $prefix
275
-				return str_starts_with($key, $prefix);
276
-			}, ARRAY_FILTER_USE_KEY
277
-		);
278
-
279
-		return $values;
280
-	}
281
-
282
-	/**
283
-	 * @inheritDoc
284
-	 *
285
-	 * @param string $userId id of the user
286
-	 * @param bool $filtered TRUE to hide sensitive config values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
287
-	 *
288
-	 * @return array<string, array<string, string|int|float|bool|array>> [appId => [key => value]]
289
-	 * @since 31.0.0
290
-	 */
291
-	public function getAllValues(string $userId, bool $filtered = false): array {
292
-		$this->assertParams($userId, allowEmptyApp: true);
293
-		$this->loadConfigAll($userId);
294
-
295
-		$result = [];
296
-		foreach ($this->getApps($userId) as $app) {
297
-			// array_merge() will remove numeric keys (here config keys), so addition arrays instead
298
-			$cached = ($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? []);
299
-			$result[$app] = $this->formatAppValues($userId, $app, $cached, $filtered);
300
-		}
301
-
302
-		return $result;
303
-	}
304
-
305
-	/**
306
-	 * @inheritDoc
307
-	 *
308
-	 * @param string $userId id of the user
309
-	 * @param string $key config key
310
-	 * @param bool $lazy search within lazy loaded config
311
-	 * @param ValueType|null $typedAs enforce type for the returned values
312
-	 *
313
-	 * @return array<string, string|int|float|bool|array> [appId => value]
314
-	 * @since 31.0.0
315
-	 */
316
-	public function getValuesByApps(string $userId, string $key, bool $lazy = false, ?ValueType $typedAs = null): array {
317
-		$this->assertParams($userId, '', $key, allowEmptyApp: true);
318
-		$this->loadConfig($userId, $lazy);
319
-
320
-		/** @var array<array-key, array<array-key, mixed>> $cache */
321
-		if ($lazy) {
322
-			$cache = $this->lazyCache[$userId];
323
-		} else {
324
-			$cache = $this->fastCache[$userId];
325
-		}
326
-
327
-		$values = [];
328
-		foreach (array_keys($cache) as $app) {
329
-			if (isset($cache[$app][$key])) {
330
-				$value = $cache[$app][$key];
331
-				try {
332
-					$this->decryptSensitiveValue($userId, $app, $key, $value);
333
-					$value = $this->convertTypedValue($value, $typedAs ?? $this->getValueType($userId, $app, $key, $lazy));
334
-				} catch (IncorrectTypeException|UnknownKeyException) {
335
-				}
336
-				$values[$app] = $value;
337
-			}
338
-		}
339
-
340
-		return $values;
341
-	}
342
-
343
-
344
-	/**
345
-	 * @inheritDoc
346
-	 *
347
-	 * @param string $app id of the app
348
-	 * @param string $key config key
349
-	 * @param ValueType|null $typedAs enforce type for the returned values
350
-	 * @param array|null $userIds limit to a list of user ids
351
-	 *
352
-	 * @return array<string, string|int|float|bool|array> [userId => value]
353
-	 * @since 31.0.0
354
-	 */
355
-	public function getValuesByUsers(
356
-		string $app,
357
-		string $key,
358
-		?ValueType $typedAs = null,
359
-		?array $userIds = null,
360
-	): array {
361
-		$this->assertParams('', $app, $key, allowEmptyUser: true);
362
-		$this->matchAndApplyLexiconDefinition('', $app, $key);
363
-
364
-		$qb = $this->connection->getQueryBuilder();
365
-		$qb->select('userid', 'configvalue', 'type')
366
-			->from('preferences')
367
-			->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
368
-			->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
369
-
370
-		$values = [];
371
-		// this nested function will execute current Query and store result within $values.
372
-		$executeAndStoreValue = function (IQueryBuilder $qb) use (&$values, $typedAs): IResult {
373
-			$result = $qb->executeQuery();
374
-			while ($row = $result->fetch()) {
375
-				$value = $row['configvalue'];
376
-				try {
377
-					$value = $this->convertTypedValue($value, $typedAs ?? ValueType::from((int)$row['type']));
378
-				} catch (IncorrectTypeException) {
379
-				}
380
-				$values[$row['userid']] = $value;
381
-			}
382
-			return $result;
383
-		};
384
-
385
-		// if no userIds to filter, we execute query as it is and returns all values ...
386
-		if ($userIds === null) {
387
-			$result = $executeAndStoreValue($qb);
388
-			$result->closeCursor();
389
-			return $values;
390
-		}
391
-
392
-		// if userIds to filter, we chunk the list and execute the same query multiple times until we get all values
393
-		$result = null;
394
-		$qb->andWhere($qb->expr()->in('userid', $qb->createParameter('userIds')));
395
-		foreach (array_chunk($userIds, 50, true) as $chunk) {
396
-			$qb->setParameter('userIds', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
397
-			$result = $executeAndStoreValue($qb);
398
-		}
399
-		$result?->closeCursor();
400
-
401
-		return $values;
402
-	}
403
-
404
-	/**
405
-	 * @inheritDoc
406
-	 *
407
-	 * @param string $app id of the app
408
-	 * @param string $key config key
409
-	 * @param string $value config value
410
-	 * @param bool $caseInsensitive non-case-sensitive search, only works if $value is a string
411
-	 *
412
-	 * @return Generator<string>
413
-	 * @since 31.0.0
414
-	 */
415
-	public function searchUsersByValueString(string $app, string $key, string $value, bool $caseInsensitive = false): Generator {
416
-		return $this->searchUsersByTypedValue($app, $key, $value, $caseInsensitive);
417
-	}
418
-
419
-	/**
420
-	 * @inheritDoc
421
-	 *
422
-	 * @param string $app id of the app
423
-	 * @param string $key config key
424
-	 * @param int $value config value
425
-	 *
426
-	 * @return Generator<string>
427
-	 * @since 31.0.0
428
-	 */
429
-	public function searchUsersByValueInt(string $app, string $key, int $value): Generator {
430
-		return $this->searchUsersByValueString($app, $key, (string)$value);
431
-	}
432
-
433
-	/**
434
-	 * @inheritDoc
435
-	 *
436
-	 * @param string $app id of the app
437
-	 * @param string $key config key
438
-	 * @param array $values list of config values
439
-	 *
440
-	 * @return Generator<string>
441
-	 * @since 31.0.0
442
-	 */
443
-	public function searchUsersByValues(string $app, string $key, array $values): Generator {
444
-		return $this->searchUsersByTypedValue($app, $key, $values);
445
-	}
446
-
447
-	/**
448
-	 * @inheritDoc
449
-	 *
450
-	 * @param string $app id of the app
451
-	 * @param string $key config key
452
-	 * @param bool $value config value
453
-	 *
454
-	 * @return Generator<string>
455
-	 * @since 31.0.0
456
-	 */
457
-	public function searchUsersByValueBool(string $app, string $key, bool $value): Generator {
458
-		$values = ['0', 'off', 'false', 'no'];
459
-		if ($value) {
460
-			$values = ['1', 'on', 'true', 'yes'];
461
-		}
462
-		return $this->searchUsersByValues($app, $key, $values);
463
-	}
464
-
465
-	/**
466
-	 * returns a list of users with config key set to a specific value, or within the list of
467
-	 * possible values
468
-	 *
469
-	 * @param string $app
470
-	 * @param string $key
471
-	 * @param string|array $value
472
-	 * @param bool $caseInsensitive
473
-	 *
474
-	 * @return Generator<string>
475
-	 */
476
-	private function searchUsersByTypedValue(string $app, string $key, string|array $value, bool $caseInsensitive = false): Generator {
477
-		$this->assertParams('', $app, $key, allowEmptyUser: true);
478
-		$this->matchAndApplyLexiconDefinition('', $app, $key);
479
-
480
-		$qb = $this->connection->getQueryBuilder();
481
-		$qb->from('preferences');
482
-		$qb->select('userid');
483
-		$qb->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
484
-		$qb->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
485
-
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
488
-		$configValueColumn = ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) ? $qb->expr()->castColumn('configvalue', IQueryBuilder::PARAM_STR) : 'configvalue';
489
-		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) {
499
-				$where = $qb->expr()->orX(
500
-					$qb->expr()->eq($qb->func()->lower('indexed'), $qb->createNamedParameter(strtolower($value))),
501
-					$qb->expr()->andX(
502
-						$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)))
504
-					)
505
-				);
506
-			} 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
-				);
514
-			}
515
-		}
516
-
517
-		$qb->andWhere($where);
518
-		$result = $qb->executeQuery();
519
-		while ($row = $result->fetch()) {
520
-			yield $row['userid'];
521
-		}
522
-	}
523
-
524
-	/**
525
-	 * Get the config value as string.
526
-	 * If the value does not exist the given default will be returned.
527
-	 *
528
-	 * Set lazy to `null` to ignore it and get the value from either source.
529
-	 *
530
-	 * **WARNING:** Method is internal and **SHOULD** not be used, as it is better to get the value with a type.
531
-	 *
532
-	 * @param string $userId id of the user
533
-	 * @param string $app id of the app
534
-	 * @param string $key config key
535
-	 * @param string $default config value
536
-	 * @param null|bool $lazy get config as lazy loaded or not. can be NULL
537
-	 *
538
-	 * @return string the value or $default
539
-	 * @throws TypeConflictException
540
-	 * @internal
541
-	 * @since 31.0.0
542
-	 * @see IUserConfig for explanation about lazy loading
543
-	 * @see getValueString()
544
-	 * @see getValueInt()
545
-	 * @see getValueFloat()
546
-	 * @see getValueBool()
547
-	 * @see getValueArray()
548
-	 */
549
-	public function getValueMixed(
550
-		string $userId,
551
-		string $app,
552
-		string $key,
553
-		string $default = '',
554
-		?bool $lazy = false,
555
-	): string {
556
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
557
-		try {
558
-			$lazy ??= $this->isLazy($userId, $app, $key);
559
-		} catch (UnknownKeyException) {
560
-			return $default;
561
-		}
562
-
563
-		return $this->getTypedValue(
564
-			$userId,
565
-			$app,
566
-			$key,
567
-			$default,
568
-			$lazy,
569
-			ValueType::MIXED
570
-		);
571
-	}
572
-
573
-	/**
574
-	 * @inheritDoc
575
-	 *
576
-	 * @param string $userId id of the user
577
-	 * @param string $app id of the app
578
-	 * @param string $key config key
579
-	 * @param string $default default value
580
-	 * @param bool $lazy search within lazy loaded config
581
-	 *
582
-	 * @return string stored config value or $default if not set in database
583
-	 * @throws InvalidArgumentException if one of the argument format is invalid
584
-	 * @throws TypeConflictException in case of conflict with the value type set in database
585
-	 * @since 31.0.0
586
-	 * @see IUserConfig for explanation about lazy loading
587
-	 */
588
-	public function getValueString(
589
-		string $userId,
590
-		string $app,
591
-		string $key,
592
-		string $default = '',
593
-		bool $lazy = false,
594
-	): string {
595
-		return $this->getTypedValue($userId, $app, $key, $default, $lazy, ValueType::STRING);
596
-	}
597
-
598
-	/**
599
-	 * @inheritDoc
600
-	 *
601
-	 * @param string $userId id of the user
602
-	 * @param string $app id of the app
603
-	 * @param string $key config key
604
-	 * @param int $default default value
605
-	 * @param bool $lazy search within lazy loaded config
606
-	 *
607
-	 * @return int stored config value or $default if not set in database
608
-	 * @throws InvalidArgumentException if one of the argument format is invalid
609
-	 * @throws TypeConflictException in case of conflict with the value type set in database
610
-	 * @since 31.0.0
611
-	 * @see IUserConfig for explanation about lazy loading
612
-	 */
613
-	public function getValueInt(
614
-		string $userId,
615
-		string $app,
616
-		string $key,
617
-		int $default = 0,
618
-		bool $lazy = false,
619
-	): int {
620
-		return (int)$this->getTypedValue($userId, $app, $key, (string)$default, $lazy, ValueType::INT);
621
-	}
622
-
623
-	/**
624
-	 * @inheritDoc
625
-	 *
626
-	 * @param string $userId id of the user
627
-	 * @param string $app id of the app
628
-	 * @param string $key config key
629
-	 * @param float $default default value
630
-	 * @param bool $lazy search within lazy loaded config
631
-	 *
632
-	 * @return float stored config value or $default if not set in database
633
-	 * @throws InvalidArgumentException if one of the argument format is invalid
634
-	 * @throws TypeConflictException in case of conflict with the value type set in database
635
-	 * @since 31.0.0
636
-	 * @see IUserConfig for explanation about lazy loading
637
-	 */
638
-	public function getValueFloat(
639
-		string $userId,
640
-		string $app,
641
-		string $key,
642
-		float $default = 0,
643
-		bool $lazy = false,
644
-	): float {
645
-		return (float)$this->getTypedValue($userId, $app, $key, (string)$default, $lazy, ValueType::FLOAT);
646
-	}
647
-
648
-	/**
649
-	 * @inheritDoc
650
-	 *
651
-	 * @param string $userId id of the user
652
-	 * @param string $app id of the app
653
-	 * @param string $key config key
654
-	 * @param bool $default default value
655
-	 * @param bool $lazy search within lazy loaded config
656
-	 *
657
-	 * @return bool stored config value or $default if not set in database
658
-	 * @throws InvalidArgumentException if one of the argument format is invalid
659
-	 * @throws TypeConflictException in case of conflict with the value type set in database
660
-	 * @since 31.0.0
661
-	 * @see IUserConfig for explanation about lazy loading
662
-	 */
663
-	public function getValueBool(
664
-		string $userId,
665
-		string $app,
666
-		string $key,
667
-		bool $default = false,
668
-		bool $lazy = false,
669
-	): bool {
670
-		$b = strtolower($this->getTypedValue($userId, $app, $key, $default ? 'true' : 'false', $lazy, ValueType::BOOL));
671
-		return in_array($b, ['1', 'true', 'yes', 'on']);
672
-	}
673
-
674
-	/**
675
-	 * @inheritDoc
676
-	 *
677
-	 * @param string $userId id of the user
678
-	 * @param string $app id of the app
679
-	 * @param string $key config key
680
-	 * @param array $default default value
681
-	 * @param bool $lazy search within lazy loaded config
682
-	 *
683
-	 * @return array stored config value or $default if not set in database
684
-	 * @throws InvalidArgumentException if one of the argument format is invalid
685
-	 * @throws TypeConflictException in case of conflict with the value type set in database
686
-	 * @since 31.0.0
687
-	 * @see IUserConfig for explanation about lazy loading
688
-	 */
689
-	public function getValueArray(
690
-		string $userId,
691
-		string $app,
692
-		string $key,
693
-		array $default = [],
694
-		bool $lazy = false,
695
-	): array {
696
-		try {
697
-			$defaultJson = json_encode($default, JSON_THROW_ON_ERROR);
698
-			$value = json_decode($this->getTypedValue($userId, $app, $key, $defaultJson, $lazy, ValueType::ARRAY), true, flags: JSON_THROW_ON_ERROR);
699
-
700
-			return is_array($value) ? $value : [];
701
-		} catch (JsonException) {
702
-			return [];
703
-		}
704
-	}
705
-
706
-	/**
707
-	 * @param string $userId
708
-	 * @param string $app id of the app
709
-	 * @param string $key config key
710
-	 * @param string $default default value
711
-	 * @param bool $lazy search within lazy loaded config
712
-	 * @param ValueType $type value type
713
-	 *
714
-	 * @return string
715
-	 * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
716
-	 */
717
-	private function getTypedValue(
718
-		string $userId,
719
-		string $app,
720
-		string $key,
721
-		string $default,
722
-		bool $lazy,
723
-		ValueType $type,
724
-	): string {
725
-		$this->assertParams($userId, $app, $key);
726
-		$origKey = $key;
727
-		$matched = $this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, default: $default);
728
-		if ($default === null) {
729
-			// there is no logical reason for it to be null
730
-			throw new \Exception('default cannot be null');
731
-		}
732
-
733
-		// returns default if strictness of lexicon is set to WARNING (block and report)
734
-		if (!$matched) {
735
-			return $default;
736
-		}
737
-
738
-		$this->loadConfig($userId, $lazy);
739
-
740
-		/**
741
-		 * We ignore check if mixed type is requested.
742
-		 * If type of stored value is set as mixed, we don't filter.
743
-		 * If type of stored value is defined, we compare with the one requested.
744
-		 */
745
-		$knownType = $this->valueDetails[$userId][$app][$key]['type'] ?? null;
746
-		if ($type !== ValueType::MIXED
747
-			&& $knownType !== null
748
-			&& $knownType !== ValueType::MIXED
749
-			&& $type !== $knownType) {
750
-			$this->logger->warning('conflict with value type from database', ['app' => $app, 'key' => $key, 'type' => $type, 'knownType' => $knownType]);
751
-			throw new TypeConflictException('conflict with value type from database');
752
-		}
753
-
754
-		/**
755
-		 * - the pair $app/$key cannot exist in both array,
756
-		 * - we should still return an existing non-lazy value even if current method
757
-		 *   is called with $lazy is true
758
-		 *
759
-		 * This way, lazyCache will be empty until the load for lazy config value is requested.
760
-		 */
761
-		if (isset($this->lazyCache[$userId][$app][$key])) {
762
-			$value = $this->lazyCache[$userId][$app][$key];
763
-		} elseif (isset($this->fastCache[$userId][$app][$key])) {
764
-			$value = $this->fastCache[$userId][$app][$key];
765
-		} else {
766
-			return $default;
767
-		}
768
-
769
-		$this->decryptSensitiveValue($userId, $app, $key, $value);
770
-
771
-		// in case the key was modified while running matchAndApplyLexiconDefinition() we are
772
-		// interested to check options in case a modification of the value is needed
773
-		// ie inverting value from previous key when using lexicon option RENAME_INVERT_BOOLEAN
774
-		if ($origKey !== $key && $type === ValueType::BOOL) {
775
-			$value = ($this->configManager->convertToBool($value, $this->getLexiconEntry($app, $key))) ? '1' : '0';
776
-		}
777
-
778
-		return $value;
779
-	}
780
-
781
-	/**
782
-	 * @inheritDoc
783
-	 *
784
-	 * @param string $userId id of the user
785
-	 * @param string $app id of the app
786
-	 * @param string $key config key
787
-	 *
788
-	 * @return ValueType type of the value
789
-	 * @throws UnknownKeyException if config key is not known
790
-	 * @throws IncorrectTypeException if config value type is not known
791
-	 * @since 31.0.0
792
-	 */
793
-	public function getValueType(string $userId, string $app, string $key, ?bool $lazy = null): ValueType {
794
-		$this->assertParams($userId, $app, $key);
795
-		$this->loadConfig($userId, $lazy);
796
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
797
-
798
-		if (!isset($this->valueDetails[$userId][$app][$key]['type'])) {
799
-			throw new UnknownKeyException('unknown config key');
800
-		}
801
-
802
-		return $this->valueDetails[$userId][$app][$key]['type'];
803
-	}
804
-
805
-	/**
806
-	 * @inheritDoc
807
-	 *
808
-	 * @param string $userId id of the user
809
-	 * @param string $app id of the app
810
-	 * @param string $key config key
811
-	 * @param bool $lazy lazy loading
812
-	 *
813
-	 * @return int flags applied to value
814
-	 * @throws UnknownKeyException if config key is not known
815
-	 * @throws IncorrectTypeException if config value type is not known
816
-	 * @since 31.0.0
817
-	 */
818
-	public function getValueFlags(string $userId, string $app, string $key, bool $lazy = false): int {
819
-		$this->assertParams($userId, $app, $key);
820
-		$this->loadConfig($userId, $lazy);
821
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
822
-
823
-		if (!isset($this->valueDetails[$userId][$app][$key])) {
824
-			throw new UnknownKeyException('unknown config key');
825
-		}
826
-
827
-		return $this->valueDetails[$userId][$app][$key]['flags'];
828
-	}
829
-
830
-	/**
831
-	 * Store a config key and its value in database as VALUE_MIXED
832
-	 *
833
-	 * **WARNING:** Method is internal and **MUST** not be used as it is best to set a real value type
834
-	 *
835
-	 * @param string $userId id of the user
836
-	 * @param string $app id of the app
837
-	 * @param string $key config key
838
-	 * @param string $value config value
839
-	 * @param bool $lazy set config as lazy loaded
840
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
841
-	 *
842
-	 * @return bool TRUE if value was different, therefor updated in database
843
-	 * @throws TypeConflictException if type from database is not VALUE_MIXED
844
-	 * @internal
845
-	 * @since 31.0.0
846
-	 * @see IUserConfig for explanation about lazy loading
847
-	 * @see setValueString()
848
-	 * @see setValueInt()
849
-	 * @see setValueFloat()
850
-	 * @see setValueBool()
851
-	 * @see setValueArray()
852
-	 */
853
-	public function setValueMixed(
854
-		string $userId,
855
-		string $app,
856
-		string $key,
857
-		string $value,
858
-		bool $lazy = false,
859
-		int $flags = 0,
860
-	): bool {
861
-		return $this->setTypedValue(
862
-			$userId,
863
-			$app,
864
-			$key,
865
-			$value,
866
-			$lazy,
867
-			$flags,
868
-			ValueType::MIXED
869
-		);
870
-	}
871
-
872
-
873
-	/**
874
-	 * @inheritDoc
875
-	 *
876
-	 * @param string $userId id of the user
877
-	 * @param string $app id of the app
878
-	 * @param string $key config key
879
-	 * @param string $value config value
880
-	 * @param bool $lazy set config as lazy loaded
881
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
882
-	 *
883
-	 * @return bool TRUE if value was different, therefor updated in database
884
-	 * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
885
-	 * @since 31.0.0
886
-	 * @see IUserConfig for explanation about lazy loading
887
-	 */
888
-	public function setValueString(
889
-		string $userId,
890
-		string $app,
891
-		string $key,
892
-		string $value,
893
-		bool $lazy = false,
894
-		int $flags = 0,
895
-	): bool {
896
-		return $this->setTypedValue(
897
-			$userId,
898
-			$app,
899
-			$key,
900
-			$value,
901
-			$lazy,
902
-			$flags,
903
-			ValueType::STRING
904
-		);
905
-	}
906
-
907
-	/**
908
-	 * @inheritDoc
909
-	 *
910
-	 * @param string $userId id of the user
911
-	 * @param string $app id of the app
912
-	 * @param string $key config key
913
-	 * @param int $value config value
914
-	 * @param bool $lazy set config as lazy loaded
915
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
916
-	 *
917
-	 * @return bool TRUE if value was different, therefor updated in database
918
-	 * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
919
-	 * @since 31.0.0
920
-	 * @see IUserConfig for explanation about lazy loading
921
-	 */
922
-	public function setValueInt(
923
-		string $userId,
924
-		string $app,
925
-		string $key,
926
-		int $value,
927
-		bool $lazy = false,
928
-		int $flags = 0,
929
-	): bool {
930
-		if ($value > 2000000000) {
931
-			$this->logger->debug('You are trying to store an integer value around/above 2,147,483,647. This is a reminder that reaching this theoretical limit on 32 bits system will throw an exception.');
932
-		}
933
-
934
-		return $this->setTypedValue(
935
-			$userId,
936
-			$app,
937
-			$key,
938
-			(string)$value,
939
-			$lazy,
940
-			$flags,
941
-			ValueType::INT
942
-		);
943
-	}
944
-
945
-	/**
946
-	 * @inheritDoc
947
-	 *
948
-	 * @param string $userId id of the user
949
-	 * @param string $app id of the app
950
-	 * @param string $key config key
951
-	 * @param float $value config value
952
-	 * @param bool $lazy set config as lazy loaded
953
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
954
-	 *
955
-	 * @return bool TRUE if value was different, therefor updated in database
956
-	 * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
957
-	 * @since 31.0.0
958
-	 * @see IUserConfig for explanation about lazy loading
959
-	 */
960
-	public function setValueFloat(
961
-		string $userId,
962
-		string $app,
963
-		string $key,
964
-		float $value,
965
-		bool $lazy = false,
966
-		int $flags = 0,
967
-	): bool {
968
-		return $this->setTypedValue(
969
-			$userId,
970
-			$app,
971
-			$key,
972
-			(string)$value,
973
-			$lazy,
974
-			$flags,
975
-			ValueType::FLOAT
976
-		);
977
-	}
978
-
979
-	/**
980
-	 * @inheritDoc
981
-	 *
982
-	 * @param string $userId id of the user
983
-	 * @param string $app id of the app
984
-	 * @param string $key config key
985
-	 * @param bool $value config value
986
-	 * @param bool $lazy set config as lazy loaded
987
-	 *
988
-	 * @return bool TRUE if value was different, therefor updated in database
989
-	 * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
990
-	 * @since 31.0.0
991
-	 * @see IUserConfig for explanation about lazy loading
992
-	 */
993
-	public function setValueBool(
994
-		string $userId,
995
-		string $app,
996
-		string $key,
997
-		bool $value,
998
-		bool $lazy = false,
999
-		int $flags = 0,
1000
-	): bool {
1001
-		return $this->setTypedValue(
1002
-			$userId,
1003
-			$app,
1004
-			$key,
1005
-			($value) ? '1' : '0',
1006
-			$lazy,
1007
-			$flags,
1008
-			ValueType::BOOL
1009
-		);
1010
-	}
1011
-
1012
-	/**
1013
-	 * @inheritDoc
1014
-	 *
1015
-	 * @param string $userId id of the user
1016
-	 * @param string $app id of the app
1017
-	 * @param string $key config key
1018
-	 * @param array $value config value
1019
-	 * @param bool $lazy set config as lazy loaded
1020
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
1021
-	 *
1022
-	 * @return bool TRUE if value was different, therefor updated in database
1023
-	 * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
1024
-	 * @throws JsonException
1025
-	 * @since 31.0.0
1026
-	 * @see IUserConfig for explanation about lazy loading
1027
-	 */
1028
-	public function setValueArray(
1029
-		string $userId,
1030
-		string $app,
1031
-		string $key,
1032
-		array $value,
1033
-		bool $lazy = false,
1034
-		int $flags = 0,
1035
-	): bool {
1036
-		try {
1037
-			return $this->setTypedValue(
1038
-				$userId,
1039
-				$app,
1040
-				$key,
1041
-				json_encode($value, JSON_THROW_ON_ERROR),
1042
-				$lazy,
1043
-				$flags,
1044
-				ValueType::ARRAY
1045
-			);
1046
-		} catch (JsonException $e) {
1047
-			$this->logger->warning('could not setValueArray', ['app' => $app, 'key' => $key, 'exception' => $e]);
1048
-			throw $e;
1049
-		}
1050
-	}
1051
-
1052
-	/**
1053
-	 * Store a config key and its value in database
1054
-	 *
1055
-	 * If config key is already known with the exact same config value and same sensitive/lazy status, the
1056
-	 * database is not updated. If config value was previously stored as sensitive, status will not be
1057
-	 * altered.
1058
-	 *
1059
-	 * @param string $userId id of the user
1060
-	 * @param string $app id of the app
1061
-	 * @param string $key config key
1062
-	 * @param string $value config value
1063
-	 * @param bool $lazy config set as lazy loaded
1064
-	 * @param ValueType $type value type
1065
-	 *
1066
-	 * @return bool TRUE if value was updated in database
1067
-	 * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
1068
-	 * @see IUserConfig for explanation about lazy loading
1069
-	 */
1070
-	private function setTypedValue(
1071
-		string $userId,
1072
-		string $app,
1073
-		string $key,
1074
-		string $value,
1075
-		bool $lazy,
1076
-		int $flags,
1077
-		ValueType $type,
1078
-	): bool {
1079
-		// Primary email addresses are always(!) expected to be lowercase
1080
-		if ($app === 'settings' && $key === 'email') {
1081
-			$value = strtolower($value);
1082
-		}
1083
-
1084
-		$this->assertParams($userId, $app, $key);
1085
-		if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags)) {
1086
-			// returns false as database is not updated
1087
-			return false;
1088
-		}
1089
-		$this->loadConfig($userId, $lazy);
1090
-
1091
-		$inserted = $refreshCache = false;
1092
-		$origValue = $value;
1093
-		$sensitive = $this->isFlagged(self::FLAG_SENSITIVE, $flags);
1094
-		if ($sensitive || ($this->hasKey($userId, $app, $key, $lazy) && $this->isSensitive($userId, $app, $key, $lazy))) {
1095
-			$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
1096
-			$flags |= self::FLAG_SENSITIVE;
1097
-		}
1098
-
1099
-		// if requested, we fill the 'indexed' field with current value
1100
-		$indexed = '';
1101
-		if ($type !== ValueType::ARRAY && $this->isFlagged(self::FLAG_INDEXED, $flags)) {
1102
-			if ($this->isFlagged(self::FLAG_SENSITIVE, $flags)) {
1103
-				$this->logger->warning('sensitive value are not to be indexed');
1104
-			} elseif (strlen($value) > self::USER_MAX_LENGTH) {
1105
-				$this->logger->warning('value is too lengthy to be indexed');
1106
-			} else {
1107
-				$indexed = $value;
1108
-			}
1109
-		}
1110
-
1111
-		if ($this->hasKey($userId, $app, $key, $lazy)) {
1112
-			/**
1113
-			 * no update if key is already known with set lazy status and value is
1114
-			 * not different, unless sensitivity is switched from false to true.
1115
-			 */
1116
-			if ($origValue === $this->getTypedValue($userId, $app, $key, $value, $lazy, $type)
1117
-				&& (!$sensitive || $this->isSensitive($userId, $app, $key, $lazy))) {
1118
-				return false;
1119
-			}
1120
-		} else {
1121
-			/**
1122
-			 * if key is not known yet, we try to insert.
1123
-			 * It might fail if the key exists with a different lazy flag.
1124
-			 */
1125
-			try {
1126
-				$insert = $this->connection->getQueryBuilder();
1127
-				$insert->insert('preferences')
1128
-					->setValue('userid', $insert->createNamedParameter($userId))
1129
-					->setValue('appid', $insert->createNamedParameter($app))
1130
-					->setValue('lazy', $insert->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
1131
-					->setValue('type', $insert->createNamedParameter($type->value, IQueryBuilder::PARAM_INT))
1132
-					->setValue('flags', $insert->createNamedParameter($flags, IQueryBuilder::PARAM_INT))
1133
-					->setValue('indexed', $insert->createNamedParameter($indexed))
1134
-					->setValue('configkey', $insert->createNamedParameter($key))
1135
-					->setValue('configvalue', $insert->createNamedParameter($value));
1136
-				$insert->executeStatement();
1137
-				$inserted = true;
1138
-			} catch (DBException $e) {
1139
-				if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
1140
-					// TODO: throw exception or just log and returns false !?
1141
-					throw $e;
1142
-				}
1143
-			}
1144
-		}
1145
-
1146
-		/**
1147
-		 * We cannot insert a new row, meaning we need to update an already existing one
1148
-		 */
1149
-		if (!$inserted) {
1150
-			$currType = $this->valueDetails[$userId][$app][$key]['type'] ?? null;
1151
-			if ($currType === null) { // this might happen when switching lazy loading status
1152
-				$this->loadConfigAll($userId);
1153
-				$currType = $this->valueDetails[$userId][$app][$key]['type'];
1154
-			}
1155
-
1156
-			/**
1157
-			 * We only log a warning and set it to VALUE_MIXED.
1158
-			 */
1159
-			if ($currType === null) {
1160
-				$this->logger->warning('Value type is set to zero (0) in database. This is not supposed to happens', ['app' => $app, 'key' => $key]);
1161
-				$currType = ValueType::MIXED;
1162
-			}
1163
-
1164
-			/**
1165
-			 * we only accept a different type from the one stored in database
1166
-			 * if the one stored in database is not-defined (VALUE_MIXED)
1167
-			 */
1168
-			if ($currType !== ValueType::MIXED
1169
-				&& $currType !== $type) {
1170
-				try {
1171
-					$currTypeDef = $currType->getDefinition();
1172
-					$typeDef = $type->getDefinition();
1173
-				} catch (IncorrectTypeException) {
1174
-					$currTypeDef = $currType->value;
1175
-					$typeDef = $type->value;
1176
-				}
1177
-				throw new TypeConflictException('conflict between new type (' . $typeDef . ') and old type (' . $currTypeDef . ')');
1178
-			}
1179
-
1180
-			if ($lazy !== $this->isLazy($userId, $app, $key)) {
1181
-				$refreshCache = true;
1182
-			}
1183
-
1184
-			$update = $this->connection->getQueryBuilder();
1185
-			$update->update('preferences')
1186
-				->set('configvalue', $update->createNamedParameter($value))
1187
-				->set('lazy', $update->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
1188
-				->set('type', $update->createNamedParameter($type->value, IQueryBuilder::PARAM_INT))
1189
-				->set('flags', $update->createNamedParameter($flags, IQueryBuilder::PARAM_INT))
1190
-				->set('indexed', $update->createNamedParameter($indexed))
1191
-				->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
1192
-				->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
1193
-				->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1194
-
1195
-			$update->executeStatement();
1196
-		}
1197
-
1198
-		if ($refreshCache) {
1199
-			$this->clearCache($userId);
1200
-			return true;
1201
-		}
1202
-
1203
-		// update local cache
1204
-		if ($lazy) {
1205
-			$this->lazyCache[$userId][$app][$key] = $value;
1206
-		} else {
1207
-			$this->fastCache[$userId][$app][$key] = $value;
1208
-		}
1209
-		$this->valueDetails[$userId][$app][$key] = [
1210
-			'type' => $type,
1211
-			'flags' => $flags
1212
-		];
1213
-
1214
-		return true;
1215
-	}
1216
-
1217
-	/**
1218
-	 * Change the type of config value.
1219
-	 *
1220
-	 * **WARNING:** Method is internal and **MUST** not be used as it may break things.
1221
-	 *
1222
-	 * @param string $userId id of the user
1223
-	 * @param string $app id of the app
1224
-	 * @param string $key config key
1225
-	 * @param ValueType $type value type
1226
-	 *
1227
-	 * @return bool TRUE if database update were necessary
1228
-	 * @throws UnknownKeyException if $key is now known in database
1229
-	 * @throws IncorrectTypeException if $type is not valid
1230
-	 * @internal
1231
-	 * @since 31.0.0
1232
-	 */
1233
-	public function updateType(string $userId, string $app, string $key, ValueType $type = ValueType::MIXED): bool {
1234
-		$this->assertParams($userId, $app, $key);
1235
-		$this->loadConfigAll($userId);
1236
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
1237
-		$this->isLazy($userId, $app, $key); // confirm key exists
1238
-
1239
-		$update = $this->connection->getQueryBuilder();
1240
-		$update->update('preferences')
1241
-			->set('type', $update->createNamedParameter($type->value, IQueryBuilder::PARAM_INT))
1242
-			->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
1243
-			->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
1244
-			->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1245
-		$update->executeStatement();
1246
-
1247
-		$this->valueDetails[$userId][$app][$key]['type'] = $type;
1248
-
1249
-		return true;
1250
-	}
1251
-
1252
-	/**
1253
-	 * @inheritDoc
1254
-	 *
1255
-	 * @param string $userId id of the user
1256
-	 * @param string $app id of the app
1257
-	 * @param string $key config key
1258
-	 * @param bool $sensitive TRUE to set as sensitive, FALSE to unset
1259
-	 *
1260
-	 * @return bool TRUE if entry was found in database and an update was necessary
1261
-	 * @since 31.0.0
1262
-	 */
1263
-	public function updateSensitive(string $userId, string $app, string $key, bool $sensitive): bool {
1264
-		$this->assertParams($userId, $app, $key);
1265
-		$this->loadConfigAll($userId);
1266
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
1267
-
1268
-		try {
1269
-			if ($sensitive === $this->isSensitive($userId, $app, $key, null)) {
1270
-				return false;
1271
-			}
1272
-		} catch (UnknownKeyException) {
1273
-			return false;
1274
-		}
1275
-
1276
-		$lazy = $this->isLazy($userId, $app, $key);
1277
-		if ($lazy) {
1278
-			$cache = $this->lazyCache;
1279
-		} else {
1280
-			$cache = $this->fastCache;
1281
-		}
1282
-
1283
-		if (!isset($cache[$userId][$app][$key])) {
1284
-			throw new UnknownKeyException('unknown config key');
1285
-		}
1286
-
1287
-		$value = $cache[$userId][$app][$key];
1288
-		$flags = $this->getValueFlags($userId, $app, $key);
1289
-		if ($sensitive) {
1290
-			$flags |= self::FLAG_SENSITIVE;
1291
-			$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
1292
-		} else {
1293
-			$flags &= ~self::FLAG_SENSITIVE;
1294
-			$this->decryptSensitiveValue($userId, $app, $key, $value);
1295
-		}
1296
-
1297
-		$update = $this->connection->getQueryBuilder();
1298
-		$update->update('preferences')
1299
-			->set('flags', $update->createNamedParameter($flags, IQueryBuilder::PARAM_INT))
1300
-			->set('configvalue', $update->createNamedParameter($value))
1301
-			->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
1302
-			->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
1303
-			->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1304
-		$update->executeStatement();
1305
-
1306
-		$this->valueDetails[$userId][$app][$key]['flags'] = $flags;
1307
-
1308
-		return true;
1309
-	}
1310
-
1311
-	/**
1312
-	 * @inheritDoc
1313
-	 *
1314
-	 * @param string $app
1315
-	 * @param string $key
1316
-	 * @param bool $sensitive
1317
-	 *
1318
-	 * @since 31.0.0
1319
-	 */
1320
-	public function updateGlobalSensitive(string $app, string $key, bool $sensitive): void {
1321
-		$this->assertParams('', $app, $key, allowEmptyUser: true);
1322
-		$this->matchAndApplyLexiconDefinition('', $app, $key);
1323
-
1324
-		foreach (array_keys($this->getValuesByUsers($app, $key)) as $userId) {
1325
-			try {
1326
-				$this->updateSensitive($userId, $app, $key, $sensitive);
1327
-			} catch (UnknownKeyException) {
1328
-				// should not happen and can be ignored
1329
-			}
1330
-		}
1331
-
1332
-		// we clear all cache
1333
-		$this->clearCacheAll();
1334
-	}
1335
-
1336
-	/**
1337
-	 * @inheritDoc
1338
-	 *
1339
-	 * @param string $userId
1340
-	 * @param string $app
1341
-	 * @param string $key
1342
-	 * @param bool $indexed
1343
-	 *
1344
-	 * @return bool
1345
-	 * @throws DBException
1346
-	 * @throws IncorrectTypeException
1347
-	 * @throws UnknownKeyException
1348
-	 * @since 31.0.0
1349
-	 */
1350
-	public function updateIndexed(string $userId, string $app, string $key, bool $indexed): bool {
1351
-		$this->assertParams($userId, $app, $key);
1352
-		$this->loadConfigAll($userId);
1353
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
1354
-
1355
-		try {
1356
-			if ($indexed === $this->isIndexed($userId, $app, $key, null)) {
1357
-				return false;
1358
-			}
1359
-		} catch (UnknownKeyException) {
1360
-			return false;
1361
-		}
1362
-
1363
-		$lazy = $this->isLazy($userId, $app, $key);
1364
-		if ($lazy) {
1365
-			$cache = $this->lazyCache;
1366
-		} else {
1367
-			$cache = $this->fastCache;
1368
-		}
1369
-
1370
-		if (!isset($cache[$userId][$app][$key])) {
1371
-			throw new UnknownKeyException('unknown config key');
1372
-		}
1373
-
1374
-		$value = $cache[$userId][$app][$key];
1375
-		$flags = $this->getValueFlags($userId, $app, $key);
1376
-		if ($indexed) {
1377
-			$indexed = $value;
1378
-		} else {
1379
-			$flags &= ~self::FLAG_INDEXED;
1380
-			$indexed = '';
1381
-		}
1382
-
1383
-		$update = $this->connection->getQueryBuilder();
1384
-		$update->update('preferences')
1385
-			->set('flags', $update->createNamedParameter($flags, IQueryBuilder::PARAM_INT))
1386
-			->set('indexed', $update->createNamedParameter($indexed))
1387
-			->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
1388
-			->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
1389
-			->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1390
-		$update->executeStatement();
1391
-
1392
-		$this->valueDetails[$userId][$app][$key]['flags'] = $flags;
1393
-
1394
-		return true;
1395
-	}
1396
-
1397
-
1398
-	/**
1399
-	 * @inheritDoc
1400
-	 *
1401
-	 * @param string $app
1402
-	 * @param string $key
1403
-	 * @param bool $indexed
1404
-	 *
1405
-	 * @since 31.0.0
1406
-	 */
1407
-	public function updateGlobalIndexed(string $app, string $key, bool $indexed): void {
1408
-		$this->assertParams('', $app, $key, allowEmptyUser: true);
1409
-		$this->matchAndApplyLexiconDefinition('', $app, $key);
1410
-
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
-			}
1417
-		}
1418
-
1419
-		// we clear all cache
1420
-		$this->clearCacheAll();
1421
-	}
1422
-
1423
-	/**
1424
-	 * @inheritDoc
1425
-	 *
1426
-	 * @param string $userId id of the user
1427
-	 * @param string $app id of the app
1428
-	 * @param string $key config key
1429
-	 * @param bool $lazy TRUE to set as lazy loaded, FALSE to unset
1430
-	 *
1431
-	 * @return bool TRUE if entry was found in database and an update was necessary
1432
-	 * @since 31.0.0
1433
-	 */
1434
-	public function updateLazy(string $userId, string $app, string $key, bool $lazy): bool {
1435
-		$this->assertParams($userId, $app, $key);
1436
-		$this->loadConfigAll($userId);
1437
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
1438
-
1439
-		try {
1440
-			if ($lazy === $this->isLazy($userId, $app, $key)) {
1441
-				return false;
1442
-			}
1443
-		} catch (UnknownKeyException) {
1444
-			return false;
1445
-		}
1446
-
1447
-		$update = $this->connection->getQueryBuilder();
1448
-		$update->update('preferences')
1449
-			->set('lazy', $update->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))
1450
-			->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
1451
-			->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
1452
-			->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1453
-		$update->executeStatement();
1454
-
1455
-		// At this point, it is a lot safer to clean cache
1456
-		$this->clearCache($userId);
1457
-
1458
-		return true;
1459
-	}
1460
-
1461
-	/**
1462
-	 * @inheritDoc
1463
-	 *
1464
-	 * @param string $app id of the app
1465
-	 * @param string $key config key
1466
-	 * @param bool $lazy TRUE to set as lazy loaded, FALSE to unset
1467
-	 *
1468
-	 * @since 31.0.0
1469
-	 */
1470
-	public function updateGlobalLazy(string $app, string $key, bool $lazy): void {
1471
-		$this->assertParams('', $app, $key, allowEmptyUser: true);
1472
-		$this->matchAndApplyLexiconDefinition('', $app, $key);
1473
-
1474
-		$update = $this->connection->getQueryBuilder();
1475
-		$update->update('preferences')
1476
-			->set('lazy', $update->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))
1477
-			->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
1478
-			->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1479
-		$update->executeStatement();
1480
-
1481
-		$this->clearCacheAll();
1482
-	}
1483
-
1484
-	/**
1485
-	 * @inheritDoc
1486
-	 *
1487
-	 * @param string $userId id of the user
1488
-	 * @param string $app id of the app
1489
-	 * @param string $key config key
1490
-	 *
1491
-	 * @return array
1492
-	 * @throws UnknownKeyException if config key is not known in database
1493
-	 * @since 31.0.0
1494
-	 */
1495
-	public function getDetails(string $userId, string $app, string $key): array {
1496
-		$this->assertParams($userId, $app, $key);
1497
-		$this->loadConfigAll($userId);
1498
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
1499
-
1500
-		$lazy = $this->isLazy($userId, $app, $key);
1501
-
1502
-		if ($lazy) {
1503
-			$cache = $this->lazyCache[$userId];
1504
-		} else {
1505
-			$cache = $this->fastCache[$userId];
1506
-		}
1507
-
1508
-		$type = $this->getValueType($userId, $app, $key);
1509
-		try {
1510
-			$typeString = $type->getDefinition();
1511
-		} catch (IncorrectTypeException $e) {
1512
-			$this->logger->warning('type stored in database is not correct', ['exception' => $e, 'type' => $type]);
1513
-			$typeString = (string)$type->value;
1514
-		}
1515
-
1516
-		if (!isset($cache[$app][$key])) {
1517
-			throw new UnknownKeyException('unknown config key');
1518
-		}
1519
-
1520
-		$value = $cache[$app][$key];
1521
-		$sensitive = $this->isSensitive($userId, $app, $key, null);
1522
-		$this->decryptSensitiveValue($userId, $app, $key, $value);
1523
-
1524
-		return [
1525
-			'userId' => $userId,
1526
-			'app' => $app,
1527
-			'key' => $key,
1528
-			'value' => $value,
1529
-			'type' => $type->value,
1530
-			'lazy' => $lazy,
1531
-			'typeString' => $typeString,
1532
-			'sensitive' => $sensitive
1533
-		];
1534
-	}
1535
-
1536
-	/**
1537
-	 * @inheritDoc
1538
-	 *
1539
-	 * @param string $userId id of the user
1540
-	 * @param string $app id of the app
1541
-	 * @param string $key config key
1542
-	 *
1543
-	 * @since 31.0.0
1544
-	 */
1545
-	public function deleteUserConfig(string $userId, string $app, string $key): void {
1546
-		$this->assertParams($userId, $app, $key);
1547
-		$this->matchAndApplyLexiconDefinition($userId, $app, $key);
1548
-
1549
-		$qb = $this->connection->getQueryBuilder();
1550
-		$qb->delete('preferences')
1551
-			->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId)))
1552
-			->andWhere($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
1553
-			->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
1554
-		$qb->executeStatement();
1555
-
1556
-		unset($this->lazyCache[$userId][$app][$key]);
1557
-		unset($this->fastCache[$userId][$app][$key]);
1558
-		unset($this->valueDetails[$userId][$app][$key]);
1559
-	}
1560
-
1561
-	/**
1562
-	 * @inheritDoc
1563
-	 *
1564
-	 * @param string $app id of the app
1565
-	 * @param string $key config key
1566
-	 *
1567
-	 * @since 31.0.0
1568
-	 */
1569
-	public function deleteKey(string $app, string $key): void {
1570
-		$this->assertParams('', $app, $key, allowEmptyUser: true);
1571
-		$this->matchAndApplyLexiconDefinition('', $app, $key);
1572
-
1573
-		$qb = $this->connection->getQueryBuilder();
1574
-		$qb->delete('preferences')
1575
-			->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
1576
-			->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
1577
-		$qb->executeStatement();
1578
-
1579
-		$this->clearCacheAll();
1580
-	}
1581
-
1582
-	/**
1583
-	 * @inheritDoc
1584
-	 *
1585
-	 * @param string $app id of the app
1586
-	 *
1587
-	 * @since 31.0.0
1588
-	 */
1589
-	public function deleteApp(string $app): void {
1590
-		$this->assertParams('', $app, allowEmptyUser: true);
1591
-
1592
-		$qb = $this->connection->getQueryBuilder();
1593
-		$qb->delete('preferences')
1594
-			->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
1595
-		$qb->executeStatement();
1596
-
1597
-		$this->clearCacheAll();
1598
-	}
1599
-
1600
-	public function deleteAllUserConfig(string $userId): void {
1601
-		$this->assertParams($userId, '', allowEmptyApp: true);
1602
-		$qb = $this->connection->getQueryBuilder();
1603
-		$qb->delete('preferences')
1604
-			->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId)));
1605
-		$qb->executeStatement();
1606
-
1607
-		$this->clearCache($userId);
1608
-	}
1609
-
1610
-	/**
1611
-	 * @inheritDoc
1612
-	 *
1613
-	 * @param string $userId id of the user
1614
-	 * @param bool $reload set to TRUE to refill cache instantly after clearing it.
1615
-	 *
1616
-	 * @since 31.0.0
1617
-	 */
1618
-	public function clearCache(string $userId, bool $reload = false): void {
1619
-		$this->assertParams($userId, allowEmptyApp: true);
1620
-		$this->lazyLoaded[$userId] = $this->fastLoaded[$userId] = false;
1621
-		$this->lazyCache[$userId] = $this->fastCache[$userId] = $this->valueDetails[$userId] = [];
1622
-
1623
-		if (!$reload) {
1624
-			return;
1625
-		}
1626
-
1627
-		$this->loadConfigAll($userId);
1628
-	}
1629
-
1630
-	/**
1631
-	 * @inheritDoc
1632
-	 *
1633
-	 * @since 31.0.0
1634
-	 */
1635
-	public function clearCacheAll(): void {
1636
-		$this->lazyLoaded = $this->fastLoaded = [];
1637
-		$this->lazyCache = $this->fastCache = $this->valueDetails = $this->configLexiconDetails = [];
1638
-	}
1639
-
1640
-	/**
1641
-	 * For debug purpose.
1642
-	 * Returns the cached data.
1643
-	 *
1644
-	 * @return array
1645
-	 * @since 31.0.0
1646
-	 * @internal
1647
-	 */
1648
-	public function statusCache(): array {
1649
-		return [
1650
-			'fastLoaded' => $this->fastLoaded,
1651
-			'fastCache' => $this->fastCache,
1652
-			'lazyLoaded' => $this->lazyLoaded,
1653
-			'lazyCache' => $this->lazyCache,
1654
-			'valueDetails' => $this->valueDetails,
1655
-		];
1656
-	}
1657
-
1658
-	/**
1659
-	 * @param int $needle bitflag to search
1660
-	 * @param int $flags all flags
1661
-	 *
1662
-	 * @return bool TRUE if bitflag $needle is set in $flags
1663
-	 */
1664
-	private function isFlagged(int $needle, int $flags): bool {
1665
-		return (($needle & $flags) !== 0);
1666
-	}
1667
-
1668
-	/**
1669
-	 * Confirm the string set for app and key fit the database description
1670
-	 *
1671
-	 * @param string $userId
1672
-	 * @param string $app assert $app fit in database
1673
-	 * @param string $prefKey assert config key fit in database
1674
-	 * @param bool $allowEmptyUser
1675
-	 * @param bool $allowEmptyApp $app can be empty string
1676
-	 * @param ValueType|null $valueType assert value type is only one type
1677
-	 */
1678
-	private function assertParams(
1679
-		string $userId = '',
1680
-		string $app = '',
1681
-		string $prefKey = '',
1682
-		bool $allowEmptyUser = false,
1683
-		bool $allowEmptyApp = false,
1684
-	): void {
1685
-		if (!$allowEmptyUser && $userId === '') {
1686
-			throw new InvalidArgumentException('userId cannot be an empty string');
1687
-		}
1688
-		if (!$allowEmptyApp && $app === '') {
1689
-			throw new InvalidArgumentException('app cannot be an empty string');
1690
-		}
1691
-		if (strlen($userId) > self::USER_MAX_LENGTH) {
1692
-			throw new InvalidArgumentException('Value (' . $userId . ') for userId is too long (' . self::USER_MAX_LENGTH . ')');
1693
-		}
1694
-		if (strlen($app) > self::APP_MAX_LENGTH) {
1695
-			throw new InvalidArgumentException('Value (' . $app . ') for app is too long (' . self::APP_MAX_LENGTH . ')');
1696
-		}
1697
-		if (strlen($prefKey) > self::KEY_MAX_LENGTH) {
1698
-			throw new InvalidArgumentException('Value (' . $prefKey . ') for key is too long (' . self::KEY_MAX_LENGTH . ')');
1699
-		}
1700
-	}
1701
-
1702
-	private function loadConfigAll(string $userId): void {
1703
-		$this->loadConfig($userId, null);
1704
-	}
1705
-
1706
-	/**
1707
-	 * Load normal config or config set as lazy loaded
1708
-	 *
1709
-	 * @param bool|null $lazy set to TRUE to load config set as lazy loaded, set to NULL to load all config
1710
-	 */
1711
-	private function loadConfig(string $userId, ?bool $lazy = false): void {
1712
-		if ($this->isLoaded($userId, $lazy)) {
1713
-			return;
1714
-		}
1715
-
1716
-		if (($lazy ?? true) !== false) { // if lazy is null or true, we debug log
1717
-			$this->logger->debug('The loading of lazy UserConfig values have been requested', ['exception' => new \RuntimeException('ignorable exception')]);
1718
-		}
1719
-
1720
-		$qb = $this->connection->getQueryBuilder();
1721
-		$qb->from('preferences');
1722
-		$qb->select('appid', 'configkey', 'configvalue', 'type', 'flags');
1723
-		$qb->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId)));
1724
-
1725
-		// we only need value from lazy when loadConfig does not specify it
1726
-		if ($lazy !== null) {
1727
-			$qb->andWhere($qb->expr()->eq('lazy', $qb->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT)));
1728
-		} else {
1729
-			$qb->addSelect('lazy');
1730
-		}
1731
-
1732
-		$result = $qb->executeQuery();
1733
-		$rows = $result->fetchAll();
1734
-		foreach ($rows as $row) {
1735
-			if (($row['lazy'] ?? ($lazy ?? 0) ? 1 : 0) === 1) {
1736
-				$this->lazyCache[$userId][$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1737
-			} else {
1738
-				$this->fastCache[$userId][$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1739
-			}
1740
-			$this->valueDetails[$userId][$row['appid']][$row['configkey']] = ['type' => ValueType::from((int)($row['type'] ?? 0)), 'flags' => (int)$row['flags']];
1741
-		}
1742
-		$result->closeCursor();
1743
-		$this->setAsLoaded($userId, $lazy);
1744
-	}
1745
-
1746
-	/**
1747
-	 * if $lazy is:
1748
-	 *  - false: will returns true if fast config are loaded
1749
-	 *  - true : will returns true if lazy config are loaded
1750
-	 *  - null : will returns true if both config are loaded
1751
-	 *
1752
-	 * @param string $userId
1753
-	 * @param bool $lazy
1754
-	 *
1755
-	 * @return bool
1756
-	 */
1757
-	private function isLoaded(string $userId, ?bool $lazy): bool {
1758
-		if ($lazy === null) {
1759
-			return ($this->lazyLoaded[$userId] ?? false) && ($this->fastLoaded[$userId] ?? false);
1760
-		}
1761
-
1762
-		return $lazy ? $this->lazyLoaded[$userId] ?? false : $this->fastLoaded[$userId] ?? false;
1763
-	}
1764
-
1765
-	/**
1766
-	 * if $lazy is:
1767
-	 * - false: set fast config as loaded
1768
-	 * - true : set lazy config as loaded
1769
-	 * - null : set both config as loaded
1770
-	 *
1771
-	 * @param string $userId
1772
-	 * @param bool $lazy
1773
-	 */
1774
-	private function setAsLoaded(string $userId, ?bool $lazy): void {
1775
-		if ($lazy === null) {
1776
-			$this->fastLoaded[$userId] = $this->lazyLoaded[$userId] = true;
1777
-			return;
1778
-		}
1779
-
1780
-		// We also create empty entry to keep both fastLoaded/lazyLoaded synced
1781
-		if ($lazy) {
1782
-			$this->lazyLoaded[$userId] = true;
1783
-			$this->fastLoaded[$userId] = $this->fastLoaded[$userId] ?? false;
1784
-			$this->fastCache[$userId] = $this->fastCache[$userId] ?? [];
1785
-		} else {
1786
-			$this->fastLoaded[$userId] = true;
1787
-			$this->lazyLoaded[$userId] = $this->lazyLoaded[$userId] ?? false;
1788
-			$this->lazyCache[$userId] = $this->lazyCache[$userId] ?? [];
1789
-		}
1790
-	}
1791
-
1792
-	/**
1793
-	 * **Warning:** this will load all lazy values from the database
1794
-	 *
1795
-	 * @param string $userId id of the user
1796
-	 * @param string $app id of the app
1797
-	 * @param bool $filtered TRUE to hide sensitive config values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
1798
-	 *
1799
-	 * @return array<string, string|int|float|bool|array>
1800
-	 */
1801
-	private function formatAppValues(string $userId, string $app, array $values, bool $filtered = false): array {
1802
-		foreach ($values as $key => $value) {
1803
-			//$key = (string)$key;
1804
-			try {
1805
-				$type = $this->getValueType($userId, $app, (string)$key);
1806
-			} catch (UnknownKeyException) {
1807
-				continue;
1808
-			}
1809
-
1810
-			if ($this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags'] ?? 0)) {
1811
-				if ($filtered) {
1812
-					$value = IConfig::SENSITIVE_VALUE;
1813
-					$type = ValueType::STRING;
1814
-				} else {
1815
-					$this->decryptSensitiveValue($userId, $app, (string)$key, $value);
1816
-				}
1817
-			}
1818
-
1819
-			$values[$key] = $this->convertTypedValue($value, $type);
1820
-		}
1821
-
1822
-		return $values;
1823
-	}
1824
-
1825
-	/**
1826
-	 * convert string value to the expected type
1827
-	 *
1828
-	 * @param string $value
1829
-	 * @param ValueType $type
1830
-	 *
1831
-	 * @return string|int|float|bool|array
1832
-	 */
1833
-	private function convertTypedValue(string $value, ValueType $type): string|int|float|bool|array {
1834
-		switch ($type) {
1835
-			case ValueType::INT:
1836
-				return (int)$value;
1837
-			case ValueType::FLOAT:
1838
-				return (float)$value;
1839
-			case ValueType::BOOL:
1840
-				return in_array(strtolower($value), ['1', 'true', 'yes', 'on']);
1841
-			case ValueType::ARRAY:
1842
-				try {
1843
-					return json_decode($value, true, flags: JSON_THROW_ON_ERROR);
1844
-				} catch (JsonException) {
1845
-					// ignoreable
1846
-				}
1847
-				break;
1848
-		}
1849
-		return $value;
1850
-	}
1851
-
1852
-
1853
-	/**
1854
-	 * will change referenced $value with the decrypted value in case of encrypted (sensitive value)
1855
-	 *
1856
-	 * @param string $userId
1857
-	 * @param string $app
1858
-	 * @param string $key
1859
-	 * @param string $value
1860
-	 */
1861
-	private function decryptSensitiveValue(string $userId, string $app, string $key, string &$value): void {
1862
-		if (!$this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags'] ?? 0)) {
1863
-			return;
1864
-		}
1865
-
1866
-		if (!str_starts_with($value, self::ENCRYPTION_PREFIX)) {
1867
-			return;
1868
-		}
1869
-
1870
-		try {
1871
-			$value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
1872
-		} catch (\Exception $e) {
1873
-			$this->logger->warning('could not decrypt sensitive value', [
1874
-				'userId' => $userId,
1875
-				'app' => $app,
1876
-				'key' => $key,
1877
-				'value' => $value,
1878
-				'exception' => $e
1879
-			]);
1880
-		}
1881
-	}
1882
-
1883
-	/**
1884
-	 * Match and apply current use of config values with defined lexicon.
1885
-	 * Set $lazy to NULL only if only interested into checking that $key is alias.
1886
-	 *
1887
-	 * @throws UnknownKeyException
1888
-	 * @throws TypeConflictException
1889
-	 * @return bool FALSE if conflict with defined lexicon were observed in the process
1890
-	 */
1891
-	private function matchAndApplyLexiconDefinition(
1892
-		string $userId,
1893
-		string $app,
1894
-		string &$key,
1895
-		?bool &$lazy = null,
1896
-		ValueType &$type = ValueType::MIXED,
1897
-		int &$flags = 0,
1898
-		?string &$default = null,
1899
-	): bool {
1900
-		$configDetails = $this->getConfigDetailsFromLexicon($app);
1901
-		if (array_key_exists($key, $configDetails['aliases']) && !$this->ignoreLexiconAliases) {
1902
-			// in case '$rename' is set in ConfigLexiconEntry, we use the new config key
1903
-			$key = $configDetails['aliases'][$key];
1904
-		}
1905
-
1906
-		if (!array_key_exists($key, $configDetails['entries'])) {
1907
-			return $this->applyLexiconStrictness($configDetails['strictness'], $app . '/' . $key);
1908
-		}
1909
-
1910
-		// if lazy is NULL, we ignore all check on the type/lazyness/default from Lexicon
1911
-		if ($lazy === null) {
1912
-			return true;
1913
-		}
1914
-
1915
-		/** @var Entry $configValue */
1916
-		$configValue = $configDetails['entries'][$key];
1917
-		if ($type === ValueType::MIXED) {
1918
-			// we overwrite if value was requested as mixed
1919
-			$type = $configValue->getValueType();
1920
-		} elseif ($configValue->getValueType() !== $type) {
1921
-			throw new TypeConflictException('The user config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
1922
-		}
1923
-
1924
-		$lazy = $configValue->isLazy();
1925
-		$flags = $configValue->getFlags();
1926
-		if ($configValue->isDeprecated()) {
1927
-			$this->logger->notice('User config key ' . $app . '/' . $key . ' is set as deprecated.');
1928
-		}
1929
-
1930
-		$enforcedValue = $this->config->getSystemValue('lexicon.default.userconfig.enforced', [])[$app][$key] ?? false;
1931
-		if (!$enforcedValue && $this->hasKey($userId, $app, $key, $lazy)) {
1932
-			// if key exists there should be no need to extract default
1933
-			return true;
1934
-		}
1935
-
1936
-		// only look for default if needed, default from Lexicon got priority if not overwritten by admin
1937
-		if ($default !== null) {
1938
-			$default = $this->getSystemDefault($app, $configValue) ?? $configValue->getDefault($this->presetManager->getLexiconPreset()) ?? $default;
1939
-		}
1940
-
1941
-		// returning false will make get() returning $default and set() not changing value in database
1942
-		return !$enforcedValue;
1943
-	}
1944
-
1945
-	/**
1946
-	 * get default value set in config/config.php if stored in key:
1947
-	 *
1948
-	 * 'lexicon.default.userconfig' => [
1949
-	 *        <appId> => [
1950
-	 *           <configKey> => 'my value',
1951
-	 *        ]
1952
-	 *     ],
1953
-	 *
1954
-	 * The entry is converted to string to fit the expected type when managing default value
1955
-	 */
1956
-	private function getSystemDefault(string $appId, Entry $configValue): ?string {
1957
-		$default = $this->config->getSystemValue('lexicon.default.userconfig', [])[$appId][$configValue->getKey()] ?? null;
1958
-		if ($default === null) {
1959
-			// no system default, using default default.
1960
-			return null;
1961
-		}
1962
-
1963
-		return $configValue->convertToString($default);
1964
-	}
1965
-
1966
-	/**
1967
-	 * manage ConfigLexicon behavior based on strictness set in IConfigLexicon
1968
-	 *
1969
-	 * @param Strictness|null $strictness
1970
-	 * @param string $line
1971
-	 *
1972
-	 * @return bool TRUE if conflict can be fully ignored
1973
-	 * @throws UnknownKeyException
1974
-	 * @see ILexicon::getStrictness()
1975
-	 */
1976
-	private function applyLexiconStrictness(?Strictness $strictness, string $configAppKey): bool {
1977
-		if ($strictness === null) {
1978
-			return true;
1979
-		}
1980
-
1981
-		$line = 'The user config key ' . $configAppKey . ' is not defined in the config lexicon';
1982
-		switch ($strictness) {
1983
-			case Strictness::IGNORE:
1984
-				return true;
1985
-			case Strictness::NOTICE:
1986
-				if (!in_array($configAppKey, $this->strictnessApplied, true)) {
1987
-					$this->strictnessApplied[] = $configAppKey;
1988
-					$this->logger->notice($line);
1989
-				}
1990
-				return true;
1991
-			case Strictness::WARNING:
1992
-				if (!in_array($configAppKey, $this->strictnessApplied, true)) {
1993
-					$this->strictnessApplied[] = $configAppKey;
1994
-					$this->logger->warning($line);
1995
-				}
1996
-				return false;
1997
-			case Strictness::EXCEPTION:
1998
-				throw new UnknownKeyException($line);
1999
-		}
2000
-
2001
-		throw new UnknownKeyException($line);
2002
-	}
2003
-
2004
-	/**
2005
-	 * extract details from registered $appId's config lexicon
2006
-	 *
2007
-	 * @param string $appId
2008
-	 *
2009
-	 * @return array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}
2010
-	 * @internal
2011
-	 */
2012
-	public function getConfigDetailsFromLexicon(string $appId): array {
2013
-		if (!array_key_exists($appId, $this->configLexiconDetails)) {
2014
-			$entries = $aliases = [];
2015
-			$bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
2016
-			$configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
2017
-			foreach ($configLexicon?->getUserConfigs() ?? [] as $configEntry) {
2018
-				$entries[$configEntry->getKey()] = $configEntry;
2019
-				if ($configEntry->getRename() !== null) {
2020
-					$aliases[$configEntry->getRename()] = $configEntry->getKey();
2021
-				}
2022
-			}
2023
-
2024
-			$this->configLexiconDetails[$appId] = [
2025
-				'entries' => $entries,
2026
-				'aliases' => $aliases,
2027
-				'strictness' => $configLexicon?->getStrictness() ?? Strictness::IGNORE
2028
-			];
2029
-		}
2030
-
2031
-		return $this->configLexiconDetails[$appId];
2032
-	}
2033
-
2034
-	/**
2035
-	 * get Lexicon Entry using appId and config key entry
2036
-	 *
2037
-	 * @return Entry|null NULL if entry does not exist in user's Lexicon
2038
-	 * @internal
2039
-	 */
2040
-	public function getLexiconEntry(string $appId, string $key): ?Entry {
2041
-		return $this->getConfigDetailsFromLexicon($appId)['entries'][$key] ?? null;
2042
-	}
2043
-
2044
-	/**
2045
-	 * if set to TRUE, ignore aliases defined in Config Lexicon during the use of the methods of this class
2046
-	 *
2047
-	 * @internal
2048
-	 */
2049
-	public function ignoreLexiconAliases(bool $ignore): void {
2050
-		$this->ignoreLexiconAliases = $ignore;
2051
-	}
49
+    private const USER_MAX_LENGTH = 64;
50
+    private const APP_MAX_LENGTH = 32;
51
+    private const KEY_MAX_LENGTH = 64;
52
+    private const INDEX_MAX_LENGTH = 64;
53
+    private const ENCRYPTION_PREFIX = '$UserConfigEncryption$';
54
+    private const ENCRYPTION_PREFIX_LENGTH = 22; // strlen(self::ENCRYPTION_PREFIX)
55
+
56
+    /** @var array<string, array<string, array<string, mixed>>> [ass'user_id' => ['app_id' => ['key' => 'value']]] */
57
+    private array $fastCache = [];   // cache for normal config keys
58
+    /** @var array<string, array<string, array<string, mixed>>> ['user_id' => ['app_id' => ['key' => 'value']]] */
59
+    private array $lazyCache = [];   // cache for lazy config keys
60
+    /** @var array<string, array<string, array<string, array<string, mixed>>>> ['user_id' => ['app_id' => ['key' => ['type' => ValueType, 'flags' => bitflag]]]] */
61
+    private array $valueDetails = [];  // type for all config values
62
+    /** @var array<string, boolean> ['user_id' => bool] */
63
+    private array $fastLoaded = [];
64
+    /** @var array<string, boolean> ['user_id' => bool] */
65
+    private array $lazyLoaded = [];
66
+    /** @var array<string, array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
67
+    private array $configLexiconDetails = [];
68
+    private bool $ignoreLexiconAliases = false;
69
+    private array $strictnessApplied = [];
70
+
71
+    public function __construct(
72
+        protected IDBConnection $connection,
73
+        protected IConfig $config,
74
+        private readonly ConfigManager $configManager,
75
+        private readonly PresetManager $presetManager,
76
+        protected LoggerInterface $logger,
77
+        protected ICrypto $crypto,
78
+    ) {
79
+    }
80
+
81
+    /**
82
+     * @inheritDoc
83
+     *
84
+     * @param string $appId optional id of app
85
+     *
86
+     * @return list<string> list of userIds
87
+     * @since 31.0.0
88
+     */
89
+    public function getUserIds(string $appId = ''): array {
90
+        $this->assertParams(app: $appId, allowEmptyUser: true, allowEmptyApp: true);
91
+
92
+        $qb = $this->connection->getQueryBuilder();
93
+        $qb->from('preferences');
94
+        $qb->select('userid');
95
+        $qb->groupBy('userid');
96
+        if ($appId !== '') {
97
+            $qb->where($qb->expr()->eq('appid', $qb->createNamedParameter($appId)));
98
+        }
99
+
100
+        $result = $qb->executeQuery();
101
+        $rows = $result->fetchAll();
102
+        $userIds = [];
103
+        foreach ($rows as $row) {
104
+            $userIds[] = $row['userid'];
105
+        }
106
+
107
+        return $userIds;
108
+    }
109
+
110
+    /**
111
+     * @inheritDoc
112
+     *
113
+     * @return list<string> list of app ids
114
+     * @since 31.0.0
115
+     */
116
+    public function getApps(string $userId): array {
117
+        $this->assertParams($userId, allowEmptyApp: true);
118
+        $this->loadConfigAll($userId);
119
+        $apps = array_merge(array_keys($this->fastCache[$userId] ?? []), array_keys($this->lazyCache[$userId] ?? []));
120
+        sort($apps);
121
+
122
+        return array_values(array_unique($apps));
123
+    }
124
+
125
+    /**
126
+     * @inheritDoc
127
+     *
128
+     * @param string $userId id of the user
129
+     * @param string $app id of the app
130
+     *
131
+     * @return list<string> list of stored config keys
132
+     * @since 31.0.0
133
+     */
134
+    public function getKeys(string $userId, string $app): array {
135
+        $this->assertParams($userId, $app);
136
+        $this->loadConfigAll($userId);
137
+        // array_merge() will remove numeric keys (here config keys), so addition arrays instead
138
+        $keys = array_map('strval', array_keys(($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? [])));
139
+        sort($keys);
140
+
141
+        return array_values(array_unique($keys));
142
+    }
143
+
144
+    /**
145
+     * @inheritDoc
146
+     *
147
+     * @param string $userId id of the user
148
+     * @param string $app id of the app
149
+     * @param string $key config key
150
+     * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
151
+     *
152
+     * @return bool TRUE if key exists
153
+     * @since 31.0.0
154
+     */
155
+    public function hasKey(string $userId, string $app, string $key, ?bool $lazy = false): bool {
156
+        $this->assertParams($userId, $app, $key);
157
+        $this->loadConfig($userId, $lazy);
158
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
159
+
160
+        if ($lazy === null) {
161
+            $appCache = $this->getValues($userId, $app);
162
+            return isset($appCache[$key]);
163
+        }
164
+
165
+        if ($lazy) {
166
+            return isset($this->lazyCache[$userId][$app][$key]);
167
+        }
168
+
169
+        return isset($this->fastCache[$userId][$app][$key]);
170
+    }
171
+
172
+    /**
173
+     * @inheritDoc
174
+     *
175
+     * @param string $userId id of the user
176
+     * @param string $app id of the app
177
+     * @param string $key config key
178
+     * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
179
+     *
180
+     * @return bool
181
+     * @throws UnknownKeyException if config key is not known
182
+     * @since 31.0.0
183
+     */
184
+    public function isSensitive(string $userId, string $app, string $key, ?bool $lazy = false): bool {
185
+        $this->assertParams($userId, $app, $key);
186
+        $this->loadConfig($userId, $lazy);
187
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
188
+
189
+        if (!isset($this->valueDetails[$userId][$app][$key])) {
190
+            throw new UnknownKeyException('unknown config key');
191
+        }
192
+
193
+        return $this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags']);
194
+    }
195
+
196
+    /**
197
+     * @inheritDoc
198
+     *
199
+     * @param string $userId id of the user
200
+     * @param string $app id of the app
201
+     * @param string $key config key
202
+     * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
203
+     *
204
+     * @return bool
205
+     * @throws UnknownKeyException if config key is not known
206
+     * @since 31.0.0
207
+     */
208
+    public function isIndexed(string $userId, string $app, string $key, ?bool $lazy = false): bool {
209
+        $this->assertParams($userId, $app, $key);
210
+        $this->loadConfig($userId, $lazy);
211
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
212
+
213
+        if (!isset($this->valueDetails[$userId][$app][$key])) {
214
+            throw new UnknownKeyException('unknown config key');
215
+        }
216
+
217
+        return $this->isFlagged(self::FLAG_INDEXED, $this->valueDetails[$userId][$app][$key]['flags']);
218
+    }
219
+
220
+    /**
221
+     * @inheritDoc
222
+     *
223
+     * @param string $userId id of the user
224
+     * @param string $app if of the app
225
+     * @param string $key config key
226
+     *
227
+     * @return bool TRUE if config is lazy loaded
228
+     * @throws UnknownKeyException if config key is not known
229
+     * @see IUserConfig for details about lazy loading
230
+     * @since 31.0.0
231
+     */
232
+    public function isLazy(string $userId, string $app, string $key): bool {
233
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
234
+
235
+        // there is a huge probability the non-lazy config are already loaded
236
+        // meaning that we can start by only checking if a current non-lazy key exists
237
+        if ($this->hasKey($userId, $app, $key, false)) {
238
+            // meaning key is not lazy.
239
+            return false;
240
+        }
241
+
242
+        // as key is not found as non-lazy, we load and search in the lazy config
243
+        if ($this->hasKey($userId, $app, $key, true)) {
244
+            return true;
245
+        }
246
+
247
+        throw new UnknownKeyException('unknown config key');
248
+    }
249
+
250
+    /**
251
+     * @inheritDoc
252
+     *
253
+     * @param string $userId id of the user
254
+     * @param string $app id of the app
255
+     * @param string $prefix config keys prefix to search
256
+     * @param bool $filtered TRUE to hide sensitive config values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
257
+     *
258
+     * @return array<string, string|int|float|bool|array> [key => value]
259
+     * @since 31.0.0
260
+     */
261
+    public function getValues(
262
+        string $userId,
263
+        string $app,
264
+        string $prefix = '',
265
+        bool $filtered = false,
266
+    ): array {
267
+        $this->assertParams($userId, $app, $prefix);
268
+        // if we want to filter values, we need to get sensitivity
269
+        $this->loadConfigAll($userId);
270
+        // array_merge() will remove numeric keys (here config keys), so addition arrays instead
271
+        $values = array_filter(
272
+            $this->formatAppValues($userId, $app, ($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? []), $filtered),
273
+            function (string $key) use ($prefix): bool {
274
+                // filter values based on $prefix
275
+                return str_starts_with($key, $prefix);
276
+            }, ARRAY_FILTER_USE_KEY
277
+        );
278
+
279
+        return $values;
280
+    }
281
+
282
+    /**
283
+     * @inheritDoc
284
+     *
285
+     * @param string $userId id of the user
286
+     * @param bool $filtered TRUE to hide sensitive config values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
287
+     *
288
+     * @return array<string, array<string, string|int|float|bool|array>> [appId => [key => value]]
289
+     * @since 31.0.0
290
+     */
291
+    public function getAllValues(string $userId, bool $filtered = false): array {
292
+        $this->assertParams($userId, allowEmptyApp: true);
293
+        $this->loadConfigAll($userId);
294
+
295
+        $result = [];
296
+        foreach ($this->getApps($userId) as $app) {
297
+            // array_merge() will remove numeric keys (here config keys), so addition arrays instead
298
+            $cached = ($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? []);
299
+            $result[$app] = $this->formatAppValues($userId, $app, $cached, $filtered);
300
+        }
301
+
302
+        return $result;
303
+    }
304
+
305
+    /**
306
+     * @inheritDoc
307
+     *
308
+     * @param string $userId id of the user
309
+     * @param string $key config key
310
+     * @param bool $lazy search within lazy loaded config
311
+     * @param ValueType|null $typedAs enforce type for the returned values
312
+     *
313
+     * @return array<string, string|int|float|bool|array> [appId => value]
314
+     * @since 31.0.0
315
+     */
316
+    public function getValuesByApps(string $userId, string $key, bool $lazy = false, ?ValueType $typedAs = null): array {
317
+        $this->assertParams($userId, '', $key, allowEmptyApp: true);
318
+        $this->loadConfig($userId, $lazy);
319
+
320
+        /** @var array<array-key, array<array-key, mixed>> $cache */
321
+        if ($lazy) {
322
+            $cache = $this->lazyCache[$userId];
323
+        } else {
324
+            $cache = $this->fastCache[$userId];
325
+        }
326
+
327
+        $values = [];
328
+        foreach (array_keys($cache) as $app) {
329
+            if (isset($cache[$app][$key])) {
330
+                $value = $cache[$app][$key];
331
+                try {
332
+                    $this->decryptSensitiveValue($userId, $app, $key, $value);
333
+                    $value = $this->convertTypedValue($value, $typedAs ?? $this->getValueType($userId, $app, $key, $lazy));
334
+                } catch (IncorrectTypeException|UnknownKeyException) {
335
+                }
336
+                $values[$app] = $value;
337
+            }
338
+        }
339
+
340
+        return $values;
341
+    }
342
+
343
+
344
+    /**
345
+     * @inheritDoc
346
+     *
347
+     * @param string $app id of the app
348
+     * @param string $key config key
349
+     * @param ValueType|null $typedAs enforce type for the returned values
350
+     * @param array|null $userIds limit to a list of user ids
351
+     *
352
+     * @return array<string, string|int|float|bool|array> [userId => value]
353
+     * @since 31.0.0
354
+     */
355
+    public function getValuesByUsers(
356
+        string $app,
357
+        string $key,
358
+        ?ValueType $typedAs = null,
359
+        ?array $userIds = null,
360
+    ): array {
361
+        $this->assertParams('', $app, $key, allowEmptyUser: true);
362
+        $this->matchAndApplyLexiconDefinition('', $app, $key);
363
+
364
+        $qb = $this->connection->getQueryBuilder();
365
+        $qb->select('userid', 'configvalue', 'type')
366
+            ->from('preferences')
367
+            ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
368
+            ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
369
+
370
+        $values = [];
371
+        // this nested function will execute current Query and store result within $values.
372
+        $executeAndStoreValue = function (IQueryBuilder $qb) use (&$values, $typedAs): IResult {
373
+            $result = $qb->executeQuery();
374
+            while ($row = $result->fetch()) {
375
+                $value = $row['configvalue'];
376
+                try {
377
+                    $value = $this->convertTypedValue($value, $typedAs ?? ValueType::from((int)$row['type']));
378
+                } catch (IncorrectTypeException) {
379
+                }
380
+                $values[$row['userid']] = $value;
381
+            }
382
+            return $result;
383
+        };
384
+
385
+        // if no userIds to filter, we execute query as it is and returns all values ...
386
+        if ($userIds === null) {
387
+            $result = $executeAndStoreValue($qb);
388
+            $result->closeCursor();
389
+            return $values;
390
+        }
391
+
392
+        // if userIds to filter, we chunk the list and execute the same query multiple times until we get all values
393
+        $result = null;
394
+        $qb->andWhere($qb->expr()->in('userid', $qb->createParameter('userIds')));
395
+        foreach (array_chunk($userIds, 50, true) as $chunk) {
396
+            $qb->setParameter('userIds', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
397
+            $result = $executeAndStoreValue($qb);
398
+        }
399
+        $result?->closeCursor();
400
+
401
+        return $values;
402
+    }
403
+
404
+    /**
405
+     * @inheritDoc
406
+     *
407
+     * @param string $app id of the app
408
+     * @param string $key config key
409
+     * @param string $value config value
410
+     * @param bool $caseInsensitive non-case-sensitive search, only works if $value is a string
411
+     *
412
+     * @return Generator<string>
413
+     * @since 31.0.0
414
+     */
415
+    public function searchUsersByValueString(string $app, string $key, string $value, bool $caseInsensitive = false): Generator {
416
+        return $this->searchUsersByTypedValue($app, $key, $value, $caseInsensitive);
417
+    }
418
+
419
+    /**
420
+     * @inheritDoc
421
+     *
422
+     * @param string $app id of the app
423
+     * @param string $key config key
424
+     * @param int $value config value
425
+     *
426
+     * @return Generator<string>
427
+     * @since 31.0.0
428
+     */
429
+    public function searchUsersByValueInt(string $app, string $key, int $value): Generator {
430
+        return $this->searchUsersByValueString($app, $key, (string)$value);
431
+    }
432
+
433
+    /**
434
+     * @inheritDoc
435
+     *
436
+     * @param string $app id of the app
437
+     * @param string $key config key
438
+     * @param array $values list of config values
439
+     *
440
+     * @return Generator<string>
441
+     * @since 31.0.0
442
+     */
443
+    public function searchUsersByValues(string $app, string $key, array $values): Generator {
444
+        return $this->searchUsersByTypedValue($app, $key, $values);
445
+    }
446
+
447
+    /**
448
+     * @inheritDoc
449
+     *
450
+     * @param string $app id of the app
451
+     * @param string $key config key
452
+     * @param bool $value config value
453
+     *
454
+     * @return Generator<string>
455
+     * @since 31.0.0
456
+     */
457
+    public function searchUsersByValueBool(string $app, string $key, bool $value): Generator {
458
+        $values = ['0', 'off', 'false', 'no'];
459
+        if ($value) {
460
+            $values = ['1', 'on', 'true', 'yes'];
461
+        }
462
+        return $this->searchUsersByValues($app, $key, $values);
463
+    }
464
+
465
+    /**
466
+     * returns a list of users with config key set to a specific value, or within the list of
467
+     * possible values
468
+     *
469
+     * @param string $app
470
+     * @param string $key
471
+     * @param string|array $value
472
+     * @param bool $caseInsensitive
473
+     *
474
+     * @return Generator<string>
475
+     */
476
+    private function searchUsersByTypedValue(string $app, string $key, string|array $value, bool $caseInsensitive = false): Generator {
477
+        $this->assertParams('', $app, $key, allowEmptyUser: true);
478
+        $this->matchAndApplyLexiconDefinition('', $app, $key);
479
+
480
+        $qb = $this->connection->getQueryBuilder();
481
+        $qb->from('preferences');
482
+        $qb->select('userid');
483
+        $qb->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
484
+        $qb->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
485
+
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
488
+        $configValueColumn = ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) ? $qb->expr()->castColumn('configvalue', IQueryBuilder::PARAM_STR) : 'configvalue';
489
+        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) {
499
+                $where = $qb->expr()->orX(
500
+                    $qb->expr()->eq($qb->func()->lower('indexed'), $qb->createNamedParameter(strtolower($value))),
501
+                    $qb->expr()->andX(
502
+                        $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)))
504
+                    )
505
+                );
506
+            } 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
+                );
514
+            }
515
+        }
516
+
517
+        $qb->andWhere($where);
518
+        $result = $qb->executeQuery();
519
+        while ($row = $result->fetch()) {
520
+            yield $row['userid'];
521
+        }
522
+    }
523
+
524
+    /**
525
+     * Get the config value as string.
526
+     * If the value does not exist the given default will be returned.
527
+     *
528
+     * Set lazy to `null` to ignore it and get the value from either source.
529
+     *
530
+     * **WARNING:** Method is internal and **SHOULD** not be used, as it is better to get the value with a type.
531
+     *
532
+     * @param string $userId id of the user
533
+     * @param string $app id of the app
534
+     * @param string $key config key
535
+     * @param string $default config value
536
+     * @param null|bool $lazy get config as lazy loaded or not. can be NULL
537
+     *
538
+     * @return string the value or $default
539
+     * @throws TypeConflictException
540
+     * @internal
541
+     * @since 31.0.0
542
+     * @see IUserConfig for explanation about lazy loading
543
+     * @see getValueString()
544
+     * @see getValueInt()
545
+     * @see getValueFloat()
546
+     * @see getValueBool()
547
+     * @see getValueArray()
548
+     */
549
+    public function getValueMixed(
550
+        string $userId,
551
+        string $app,
552
+        string $key,
553
+        string $default = '',
554
+        ?bool $lazy = false,
555
+    ): string {
556
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
557
+        try {
558
+            $lazy ??= $this->isLazy($userId, $app, $key);
559
+        } catch (UnknownKeyException) {
560
+            return $default;
561
+        }
562
+
563
+        return $this->getTypedValue(
564
+            $userId,
565
+            $app,
566
+            $key,
567
+            $default,
568
+            $lazy,
569
+            ValueType::MIXED
570
+        );
571
+    }
572
+
573
+    /**
574
+     * @inheritDoc
575
+     *
576
+     * @param string $userId id of the user
577
+     * @param string $app id of the app
578
+     * @param string $key config key
579
+     * @param string $default default value
580
+     * @param bool $lazy search within lazy loaded config
581
+     *
582
+     * @return string stored config value or $default if not set in database
583
+     * @throws InvalidArgumentException if one of the argument format is invalid
584
+     * @throws TypeConflictException in case of conflict with the value type set in database
585
+     * @since 31.0.0
586
+     * @see IUserConfig for explanation about lazy loading
587
+     */
588
+    public function getValueString(
589
+        string $userId,
590
+        string $app,
591
+        string $key,
592
+        string $default = '',
593
+        bool $lazy = false,
594
+    ): string {
595
+        return $this->getTypedValue($userId, $app, $key, $default, $lazy, ValueType::STRING);
596
+    }
597
+
598
+    /**
599
+     * @inheritDoc
600
+     *
601
+     * @param string $userId id of the user
602
+     * @param string $app id of the app
603
+     * @param string $key config key
604
+     * @param int $default default value
605
+     * @param bool $lazy search within lazy loaded config
606
+     *
607
+     * @return int stored config value or $default if not set in database
608
+     * @throws InvalidArgumentException if one of the argument format is invalid
609
+     * @throws TypeConflictException in case of conflict with the value type set in database
610
+     * @since 31.0.0
611
+     * @see IUserConfig for explanation about lazy loading
612
+     */
613
+    public function getValueInt(
614
+        string $userId,
615
+        string $app,
616
+        string $key,
617
+        int $default = 0,
618
+        bool $lazy = false,
619
+    ): int {
620
+        return (int)$this->getTypedValue($userId, $app, $key, (string)$default, $lazy, ValueType::INT);
621
+    }
622
+
623
+    /**
624
+     * @inheritDoc
625
+     *
626
+     * @param string $userId id of the user
627
+     * @param string $app id of the app
628
+     * @param string $key config key
629
+     * @param float $default default value
630
+     * @param bool $lazy search within lazy loaded config
631
+     *
632
+     * @return float stored config value or $default if not set in database
633
+     * @throws InvalidArgumentException if one of the argument format is invalid
634
+     * @throws TypeConflictException in case of conflict with the value type set in database
635
+     * @since 31.0.0
636
+     * @see IUserConfig for explanation about lazy loading
637
+     */
638
+    public function getValueFloat(
639
+        string $userId,
640
+        string $app,
641
+        string $key,
642
+        float $default = 0,
643
+        bool $lazy = false,
644
+    ): float {
645
+        return (float)$this->getTypedValue($userId, $app, $key, (string)$default, $lazy, ValueType::FLOAT);
646
+    }
647
+
648
+    /**
649
+     * @inheritDoc
650
+     *
651
+     * @param string $userId id of the user
652
+     * @param string $app id of the app
653
+     * @param string $key config key
654
+     * @param bool $default default value
655
+     * @param bool $lazy search within lazy loaded config
656
+     *
657
+     * @return bool stored config value or $default if not set in database
658
+     * @throws InvalidArgumentException if one of the argument format is invalid
659
+     * @throws TypeConflictException in case of conflict with the value type set in database
660
+     * @since 31.0.0
661
+     * @see IUserConfig for explanation about lazy loading
662
+     */
663
+    public function getValueBool(
664
+        string $userId,
665
+        string $app,
666
+        string $key,
667
+        bool $default = false,
668
+        bool $lazy = false,
669
+    ): bool {
670
+        $b = strtolower($this->getTypedValue($userId, $app, $key, $default ? 'true' : 'false', $lazy, ValueType::BOOL));
671
+        return in_array($b, ['1', 'true', 'yes', 'on']);
672
+    }
673
+
674
+    /**
675
+     * @inheritDoc
676
+     *
677
+     * @param string $userId id of the user
678
+     * @param string $app id of the app
679
+     * @param string $key config key
680
+     * @param array $default default value
681
+     * @param bool $lazy search within lazy loaded config
682
+     *
683
+     * @return array stored config value or $default if not set in database
684
+     * @throws InvalidArgumentException if one of the argument format is invalid
685
+     * @throws TypeConflictException in case of conflict with the value type set in database
686
+     * @since 31.0.0
687
+     * @see IUserConfig for explanation about lazy loading
688
+     */
689
+    public function getValueArray(
690
+        string $userId,
691
+        string $app,
692
+        string $key,
693
+        array $default = [],
694
+        bool $lazy = false,
695
+    ): array {
696
+        try {
697
+            $defaultJson = json_encode($default, JSON_THROW_ON_ERROR);
698
+            $value = json_decode($this->getTypedValue($userId, $app, $key, $defaultJson, $lazy, ValueType::ARRAY), true, flags: JSON_THROW_ON_ERROR);
699
+
700
+            return is_array($value) ? $value : [];
701
+        } catch (JsonException) {
702
+            return [];
703
+        }
704
+    }
705
+
706
+    /**
707
+     * @param string $userId
708
+     * @param string $app id of the app
709
+     * @param string $key config key
710
+     * @param string $default default value
711
+     * @param bool $lazy search within lazy loaded config
712
+     * @param ValueType $type value type
713
+     *
714
+     * @return string
715
+     * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
716
+     */
717
+    private function getTypedValue(
718
+        string $userId,
719
+        string $app,
720
+        string $key,
721
+        string $default,
722
+        bool $lazy,
723
+        ValueType $type,
724
+    ): string {
725
+        $this->assertParams($userId, $app, $key);
726
+        $origKey = $key;
727
+        $matched = $this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, default: $default);
728
+        if ($default === null) {
729
+            // there is no logical reason for it to be null
730
+            throw new \Exception('default cannot be null');
731
+        }
732
+
733
+        // returns default if strictness of lexicon is set to WARNING (block and report)
734
+        if (!$matched) {
735
+            return $default;
736
+        }
737
+
738
+        $this->loadConfig($userId, $lazy);
739
+
740
+        /**
741
+         * We ignore check if mixed type is requested.
742
+         * If type of stored value is set as mixed, we don't filter.
743
+         * If type of stored value is defined, we compare with the one requested.
744
+         */
745
+        $knownType = $this->valueDetails[$userId][$app][$key]['type'] ?? null;
746
+        if ($type !== ValueType::MIXED
747
+            && $knownType !== null
748
+            && $knownType !== ValueType::MIXED
749
+            && $type !== $knownType) {
750
+            $this->logger->warning('conflict with value type from database', ['app' => $app, 'key' => $key, 'type' => $type, 'knownType' => $knownType]);
751
+            throw new TypeConflictException('conflict with value type from database');
752
+        }
753
+
754
+        /**
755
+         * - the pair $app/$key cannot exist in both array,
756
+         * - we should still return an existing non-lazy value even if current method
757
+         *   is called with $lazy is true
758
+         *
759
+         * This way, lazyCache will be empty until the load for lazy config value is requested.
760
+         */
761
+        if (isset($this->lazyCache[$userId][$app][$key])) {
762
+            $value = $this->lazyCache[$userId][$app][$key];
763
+        } elseif (isset($this->fastCache[$userId][$app][$key])) {
764
+            $value = $this->fastCache[$userId][$app][$key];
765
+        } else {
766
+            return $default;
767
+        }
768
+
769
+        $this->decryptSensitiveValue($userId, $app, $key, $value);
770
+
771
+        // in case the key was modified while running matchAndApplyLexiconDefinition() we are
772
+        // interested to check options in case a modification of the value is needed
773
+        // ie inverting value from previous key when using lexicon option RENAME_INVERT_BOOLEAN
774
+        if ($origKey !== $key && $type === ValueType::BOOL) {
775
+            $value = ($this->configManager->convertToBool($value, $this->getLexiconEntry($app, $key))) ? '1' : '0';
776
+        }
777
+
778
+        return $value;
779
+    }
780
+
781
+    /**
782
+     * @inheritDoc
783
+     *
784
+     * @param string $userId id of the user
785
+     * @param string $app id of the app
786
+     * @param string $key config key
787
+     *
788
+     * @return ValueType type of the value
789
+     * @throws UnknownKeyException if config key is not known
790
+     * @throws IncorrectTypeException if config value type is not known
791
+     * @since 31.0.0
792
+     */
793
+    public function getValueType(string $userId, string $app, string $key, ?bool $lazy = null): ValueType {
794
+        $this->assertParams($userId, $app, $key);
795
+        $this->loadConfig($userId, $lazy);
796
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
797
+
798
+        if (!isset($this->valueDetails[$userId][$app][$key]['type'])) {
799
+            throw new UnknownKeyException('unknown config key');
800
+        }
801
+
802
+        return $this->valueDetails[$userId][$app][$key]['type'];
803
+    }
804
+
805
+    /**
806
+     * @inheritDoc
807
+     *
808
+     * @param string $userId id of the user
809
+     * @param string $app id of the app
810
+     * @param string $key config key
811
+     * @param bool $lazy lazy loading
812
+     *
813
+     * @return int flags applied to value
814
+     * @throws UnknownKeyException if config key is not known
815
+     * @throws IncorrectTypeException if config value type is not known
816
+     * @since 31.0.0
817
+     */
818
+    public function getValueFlags(string $userId, string $app, string $key, bool $lazy = false): int {
819
+        $this->assertParams($userId, $app, $key);
820
+        $this->loadConfig($userId, $lazy);
821
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
822
+
823
+        if (!isset($this->valueDetails[$userId][$app][$key])) {
824
+            throw new UnknownKeyException('unknown config key');
825
+        }
826
+
827
+        return $this->valueDetails[$userId][$app][$key]['flags'];
828
+    }
829
+
830
+    /**
831
+     * Store a config key and its value in database as VALUE_MIXED
832
+     *
833
+     * **WARNING:** Method is internal and **MUST** not be used as it is best to set a real value type
834
+     *
835
+     * @param string $userId id of the user
836
+     * @param string $app id of the app
837
+     * @param string $key config key
838
+     * @param string $value config value
839
+     * @param bool $lazy set config as lazy loaded
840
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
841
+     *
842
+     * @return bool TRUE if value was different, therefor updated in database
843
+     * @throws TypeConflictException if type from database is not VALUE_MIXED
844
+     * @internal
845
+     * @since 31.0.0
846
+     * @see IUserConfig for explanation about lazy loading
847
+     * @see setValueString()
848
+     * @see setValueInt()
849
+     * @see setValueFloat()
850
+     * @see setValueBool()
851
+     * @see setValueArray()
852
+     */
853
+    public function setValueMixed(
854
+        string $userId,
855
+        string $app,
856
+        string $key,
857
+        string $value,
858
+        bool $lazy = false,
859
+        int $flags = 0,
860
+    ): bool {
861
+        return $this->setTypedValue(
862
+            $userId,
863
+            $app,
864
+            $key,
865
+            $value,
866
+            $lazy,
867
+            $flags,
868
+            ValueType::MIXED
869
+        );
870
+    }
871
+
872
+
873
+    /**
874
+     * @inheritDoc
875
+     *
876
+     * @param string $userId id of the user
877
+     * @param string $app id of the app
878
+     * @param string $key config key
879
+     * @param string $value config value
880
+     * @param bool $lazy set config as lazy loaded
881
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
882
+     *
883
+     * @return bool TRUE if value was different, therefor updated in database
884
+     * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
885
+     * @since 31.0.0
886
+     * @see IUserConfig for explanation about lazy loading
887
+     */
888
+    public function setValueString(
889
+        string $userId,
890
+        string $app,
891
+        string $key,
892
+        string $value,
893
+        bool $lazy = false,
894
+        int $flags = 0,
895
+    ): bool {
896
+        return $this->setTypedValue(
897
+            $userId,
898
+            $app,
899
+            $key,
900
+            $value,
901
+            $lazy,
902
+            $flags,
903
+            ValueType::STRING
904
+        );
905
+    }
906
+
907
+    /**
908
+     * @inheritDoc
909
+     *
910
+     * @param string $userId id of the user
911
+     * @param string $app id of the app
912
+     * @param string $key config key
913
+     * @param int $value config value
914
+     * @param bool $lazy set config as lazy loaded
915
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
916
+     *
917
+     * @return bool TRUE if value was different, therefor updated in database
918
+     * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
919
+     * @since 31.0.0
920
+     * @see IUserConfig for explanation about lazy loading
921
+     */
922
+    public function setValueInt(
923
+        string $userId,
924
+        string $app,
925
+        string $key,
926
+        int $value,
927
+        bool $lazy = false,
928
+        int $flags = 0,
929
+    ): bool {
930
+        if ($value > 2000000000) {
931
+            $this->logger->debug('You are trying to store an integer value around/above 2,147,483,647. This is a reminder that reaching this theoretical limit on 32 bits system will throw an exception.');
932
+        }
933
+
934
+        return $this->setTypedValue(
935
+            $userId,
936
+            $app,
937
+            $key,
938
+            (string)$value,
939
+            $lazy,
940
+            $flags,
941
+            ValueType::INT
942
+        );
943
+    }
944
+
945
+    /**
946
+     * @inheritDoc
947
+     *
948
+     * @param string $userId id of the user
949
+     * @param string $app id of the app
950
+     * @param string $key config key
951
+     * @param float $value config value
952
+     * @param bool $lazy set config as lazy loaded
953
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
954
+     *
955
+     * @return bool TRUE if value was different, therefor updated in database
956
+     * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
957
+     * @since 31.0.0
958
+     * @see IUserConfig for explanation about lazy loading
959
+     */
960
+    public function setValueFloat(
961
+        string $userId,
962
+        string $app,
963
+        string $key,
964
+        float $value,
965
+        bool $lazy = false,
966
+        int $flags = 0,
967
+    ): bool {
968
+        return $this->setTypedValue(
969
+            $userId,
970
+            $app,
971
+            $key,
972
+            (string)$value,
973
+            $lazy,
974
+            $flags,
975
+            ValueType::FLOAT
976
+        );
977
+    }
978
+
979
+    /**
980
+     * @inheritDoc
981
+     *
982
+     * @param string $userId id of the user
983
+     * @param string $app id of the app
984
+     * @param string $key config key
985
+     * @param bool $value config value
986
+     * @param bool $lazy set config as lazy loaded
987
+     *
988
+     * @return bool TRUE if value was different, therefor updated in database
989
+     * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
990
+     * @since 31.0.0
991
+     * @see IUserConfig for explanation about lazy loading
992
+     */
993
+    public function setValueBool(
994
+        string $userId,
995
+        string $app,
996
+        string $key,
997
+        bool $value,
998
+        bool $lazy = false,
999
+        int $flags = 0,
1000
+    ): bool {
1001
+        return $this->setTypedValue(
1002
+            $userId,
1003
+            $app,
1004
+            $key,
1005
+            ($value) ? '1' : '0',
1006
+            $lazy,
1007
+            $flags,
1008
+            ValueType::BOOL
1009
+        );
1010
+    }
1011
+
1012
+    /**
1013
+     * @inheritDoc
1014
+     *
1015
+     * @param string $userId id of the user
1016
+     * @param string $app id of the app
1017
+     * @param string $key config key
1018
+     * @param array $value config value
1019
+     * @param bool $lazy set config as lazy loaded
1020
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
1021
+     *
1022
+     * @return bool TRUE if value was different, therefor updated in database
1023
+     * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
1024
+     * @throws JsonException
1025
+     * @since 31.0.0
1026
+     * @see IUserConfig for explanation about lazy loading
1027
+     */
1028
+    public function setValueArray(
1029
+        string $userId,
1030
+        string $app,
1031
+        string $key,
1032
+        array $value,
1033
+        bool $lazy = false,
1034
+        int $flags = 0,
1035
+    ): bool {
1036
+        try {
1037
+            return $this->setTypedValue(
1038
+                $userId,
1039
+                $app,
1040
+                $key,
1041
+                json_encode($value, JSON_THROW_ON_ERROR),
1042
+                $lazy,
1043
+                $flags,
1044
+                ValueType::ARRAY
1045
+            );
1046
+        } catch (JsonException $e) {
1047
+            $this->logger->warning('could not setValueArray', ['app' => $app, 'key' => $key, 'exception' => $e]);
1048
+            throw $e;
1049
+        }
1050
+    }
1051
+
1052
+    /**
1053
+     * Store a config key and its value in database
1054
+     *
1055
+     * If config key is already known with the exact same config value and same sensitive/lazy status, the
1056
+     * database is not updated. If config value was previously stored as sensitive, status will not be
1057
+     * altered.
1058
+     *
1059
+     * @param string $userId id of the user
1060
+     * @param string $app id of the app
1061
+     * @param string $key config key
1062
+     * @param string $value config value
1063
+     * @param bool $lazy config set as lazy loaded
1064
+     * @param ValueType $type value type
1065
+     *
1066
+     * @return bool TRUE if value was updated in database
1067
+     * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
1068
+     * @see IUserConfig for explanation about lazy loading
1069
+     */
1070
+    private function setTypedValue(
1071
+        string $userId,
1072
+        string $app,
1073
+        string $key,
1074
+        string $value,
1075
+        bool $lazy,
1076
+        int $flags,
1077
+        ValueType $type,
1078
+    ): bool {
1079
+        // Primary email addresses are always(!) expected to be lowercase
1080
+        if ($app === 'settings' && $key === 'email') {
1081
+            $value = strtolower($value);
1082
+        }
1083
+
1084
+        $this->assertParams($userId, $app, $key);
1085
+        if (!$this->matchAndApplyLexiconDefinition($userId, $app, $key, $lazy, $type, $flags)) {
1086
+            // returns false as database is not updated
1087
+            return false;
1088
+        }
1089
+        $this->loadConfig($userId, $lazy);
1090
+
1091
+        $inserted = $refreshCache = false;
1092
+        $origValue = $value;
1093
+        $sensitive = $this->isFlagged(self::FLAG_SENSITIVE, $flags);
1094
+        if ($sensitive || ($this->hasKey($userId, $app, $key, $lazy) && $this->isSensitive($userId, $app, $key, $lazy))) {
1095
+            $value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
1096
+            $flags |= self::FLAG_SENSITIVE;
1097
+        }
1098
+
1099
+        // if requested, we fill the 'indexed' field with current value
1100
+        $indexed = '';
1101
+        if ($type !== ValueType::ARRAY && $this->isFlagged(self::FLAG_INDEXED, $flags)) {
1102
+            if ($this->isFlagged(self::FLAG_SENSITIVE, $flags)) {
1103
+                $this->logger->warning('sensitive value are not to be indexed');
1104
+            } elseif (strlen($value) > self::USER_MAX_LENGTH) {
1105
+                $this->logger->warning('value is too lengthy to be indexed');
1106
+            } else {
1107
+                $indexed = $value;
1108
+            }
1109
+        }
1110
+
1111
+        if ($this->hasKey($userId, $app, $key, $lazy)) {
1112
+            /**
1113
+             * no update if key is already known with set lazy status and value is
1114
+             * not different, unless sensitivity is switched from false to true.
1115
+             */
1116
+            if ($origValue === $this->getTypedValue($userId, $app, $key, $value, $lazy, $type)
1117
+                && (!$sensitive || $this->isSensitive($userId, $app, $key, $lazy))) {
1118
+                return false;
1119
+            }
1120
+        } else {
1121
+            /**
1122
+             * if key is not known yet, we try to insert.
1123
+             * It might fail if the key exists with a different lazy flag.
1124
+             */
1125
+            try {
1126
+                $insert = $this->connection->getQueryBuilder();
1127
+                $insert->insert('preferences')
1128
+                    ->setValue('userid', $insert->createNamedParameter($userId))
1129
+                    ->setValue('appid', $insert->createNamedParameter($app))
1130
+                    ->setValue('lazy', $insert->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
1131
+                    ->setValue('type', $insert->createNamedParameter($type->value, IQueryBuilder::PARAM_INT))
1132
+                    ->setValue('flags', $insert->createNamedParameter($flags, IQueryBuilder::PARAM_INT))
1133
+                    ->setValue('indexed', $insert->createNamedParameter($indexed))
1134
+                    ->setValue('configkey', $insert->createNamedParameter($key))
1135
+                    ->setValue('configvalue', $insert->createNamedParameter($value));
1136
+                $insert->executeStatement();
1137
+                $inserted = true;
1138
+            } catch (DBException $e) {
1139
+                if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
1140
+                    // TODO: throw exception or just log and returns false !?
1141
+                    throw $e;
1142
+                }
1143
+            }
1144
+        }
1145
+
1146
+        /**
1147
+         * We cannot insert a new row, meaning we need to update an already existing one
1148
+         */
1149
+        if (!$inserted) {
1150
+            $currType = $this->valueDetails[$userId][$app][$key]['type'] ?? null;
1151
+            if ($currType === null) { // this might happen when switching lazy loading status
1152
+                $this->loadConfigAll($userId);
1153
+                $currType = $this->valueDetails[$userId][$app][$key]['type'];
1154
+            }
1155
+
1156
+            /**
1157
+             * We only log a warning and set it to VALUE_MIXED.
1158
+             */
1159
+            if ($currType === null) {
1160
+                $this->logger->warning('Value type is set to zero (0) in database. This is not supposed to happens', ['app' => $app, 'key' => $key]);
1161
+                $currType = ValueType::MIXED;
1162
+            }
1163
+
1164
+            /**
1165
+             * we only accept a different type from the one stored in database
1166
+             * if the one stored in database is not-defined (VALUE_MIXED)
1167
+             */
1168
+            if ($currType !== ValueType::MIXED
1169
+                && $currType !== $type) {
1170
+                try {
1171
+                    $currTypeDef = $currType->getDefinition();
1172
+                    $typeDef = $type->getDefinition();
1173
+                } catch (IncorrectTypeException) {
1174
+                    $currTypeDef = $currType->value;
1175
+                    $typeDef = $type->value;
1176
+                }
1177
+                throw new TypeConflictException('conflict between new type (' . $typeDef . ') and old type (' . $currTypeDef . ')');
1178
+            }
1179
+
1180
+            if ($lazy !== $this->isLazy($userId, $app, $key)) {
1181
+                $refreshCache = true;
1182
+            }
1183
+
1184
+            $update = $this->connection->getQueryBuilder();
1185
+            $update->update('preferences')
1186
+                ->set('configvalue', $update->createNamedParameter($value))
1187
+                ->set('lazy', $update->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
1188
+                ->set('type', $update->createNamedParameter($type->value, IQueryBuilder::PARAM_INT))
1189
+                ->set('flags', $update->createNamedParameter($flags, IQueryBuilder::PARAM_INT))
1190
+                ->set('indexed', $update->createNamedParameter($indexed))
1191
+                ->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
1192
+                ->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
1193
+                ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1194
+
1195
+            $update->executeStatement();
1196
+        }
1197
+
1198
+        if ($refreshCache) {
1199
+            $this->clearCache($userId);
1200
+            return true;
1201
+        }
1202
+
1203
+        // update local cache
1204
+        if ($lazy) {
1205
+            $this->lazyCache[$userId][$app][$key] = $value;
1206
+        } else {
1207
+            $this->fastCache[$userId][$app][$key] = $value;
1208
+        }
1209
+        $this->valueDetails[$userId][$app][$key] = [
1210
+            'type' => $type,
1211
+            'flags' => $flags
1212
+        ];
1213
+
1214
+        return true;
1215
+    }
1216
+
1217
+    /**
1218
+     * Change the type of config value.
1219
+     *
1220
+     * **WARNING:** Method is internal and **MUST** not be used as it may break things.
1221
+     *
1222
+     * @param string $userId id of the user
1223
+     * @param string $app id of the app
1224
+     * @param string $key config key
1225
+     * @param ValueType $type value type
1226
+     *
1227
+     * @return bool TRUE if database update were necessary
1228
+     * @throws UnknownKeyException if $key is now known in database
1229
+     * @throws IncorrectTypeException if $type is not valid
1230
+     * @internal
1231
+     * @since 31.0.0
1232
+     */
1233
+    public function updateType(string $userId, string $app, string $key, ValueType $type = ValueType::MIXED): bool {
1234
+        $this->assertParams($userId, $app, $key);
1235
+        $this->loadConfigAll($userId);
1236
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
1237
+        $this->isLazy($userId, $app, $key); // confirm key exists
1238
+
1239
+        $update = $this->connection->getQueryBuilder();
1240
+        $update->update('preferences')
1241
+            ->set('type', $update->createNamedParameter($type->value, IQueryBuilder::PARAM_INT))
1242
+            ->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
1243
+            ->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
1244
+            ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1245
+        $update->executeStatement();
1246
+
1247
+        $this->valueDetails[$userId][$app][$key]['type'] = $type;
1248
+
1249
+        return true;
1250
+    }
1251
+
1252
+    /**
1253
+     * @inheritDoc
1254
+     *
1255
+     * @param string $userId id of the user
1256
+     * @param string $app id of the app
1257
+     * @param string $key config key
1258
+     * @param bool $sensitive TRUE to set as sensitive, FALSE to unset
1259
+     *
1260
+     * @return bool TRUE if entry was found in database and an update was necessary
1261
+     * @since 31.0.0
1262
+     */
1263
+    public function updateSensitive(string $userId, string $app, string $key, bool $sensitive): bool {
1264
+        $this->assertParams($userId, $app, $key);
1265
+        $this->loadConfigAll($userId);
1266
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
1267
+
1268
+        try {
1269
+            if ($sensitive === $this->isSensitive($userId, $app, $key, null)) {
1270
+                return false;
1271
+            }
1272
+        } catch (UnknownKeyException) {
1273
+            return false;
1274
+        }
1275
+
1276
+        $lazy = $this->isLazy($userId, $app, $key);
1277
+        if ($lazy) {
1278
+            $cache = $this->lazyCache;
1279
+        } else {
1280
+            $cache = $this->fastCache;
1281
+        }
1282
+
1283
+        if (!isset($cache[$userId][$app][$key])) {
1284
+            throw new UnknownKeyException('unknown config key');
1285
+        }
1286
+
1287
+        $value = $cache[$userId][$app][$key];
1288
+        $flags = $this->getValueFlags($userId, $app, $key);
1289
+        if ($sensitive) {
1290
+            $flags |= self::FLAG_SENSITIVE;
1291
+            $value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
1292
+        } else {
1293
+            $flags &= ~self::FLAG_SENSITIVE;
1294
+            $this->decryptSensitiveValue($userId, $app, $key, $value);
1295
+        }
1296
+
1297
+        $update = $this->connection->getQueryBuilder();
1298
+        $update->update('preferences')
1299
+            ->set('flags', $update->createNamedParameter($flags, IQueryBuilder::PARAM_INT))
1300
+            ->set('configvalue', $update->createNamedParameter($value))
1301
+            ->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
1302
+            ->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
1303
+            ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1304
+        $update->executeStatement();
1305
+
1306
+        $this->valueDetails[$userId][$app][$key]['flags'] = $flags;
1307
+
1308
+        return true;
1309
+    }
1310
+
1311
+    /**
1312
+     * @inheritDoc
1313
+     *
1314
+     * @param string $app
1315
+     * @param string $key
1316
+     * @param bool $sensitive
1317
+     *
1318
+     * @since 31.0.0
1319
+     */
1320
+    public function updateGlobalSensitive(string $app, string $key, bool $sensitive): void {
1321
+        $this->assertParams('', $app, $key, allowEmptyUser: true);
1322
+        $this->matchAndApplyLexiconDefinition('', $app, $key);
1323
+
1324
+        foreach (array_keys($this->getValuesByUsers($app, $key)) as $userId) {
1325
+            try {
1326
+                $this->updateSensitive($userId, $app, $key, $sensitive);
1327
+            } catch (UnknownKeyException) {
1328
+                // should not happen and can be ignored
1329
+            }
1330
+        }
1331
+
1332
+        // we clear all cache
1333
+        $this->clearCacheAll();
1334
+    }
1335
+
1336
+    /**
1337
+     * @inheritDoc
1338
+     *
1339
+     * @param string $userId
1340
+     * @param string $app
1341
+     * @param string $key
1342
+     * @param bool $indexed
1343
+     *
1344
+     * @return bool
1345
+     * @throws DBException
1346
+     * @throws IncorrectTypeException
1347
+     * @throws UnknownKeyException
1348
+     * @since 31.0.0
1349
+     */
1350
+    public function updateIndexed(string $userId, string $app, string $key, bool $indexed): bool {
1351
+        $this->assertParams($userId, $app, $key);
1352
+        $this->loadConfigAll($userId);
1353
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
1354
+
1355
+        try {
1356
+            if ($indexed === $this->isIndexed($userId, $app, $key, null)) {
1357
+                return false;
1358
+            }
1359
+        } catch (UnknownKeyException) {
1360
+            return false;
1361
+        }
1362
+
1363
+        $lazy = $this->isLazy($userId, $app, $key);
1364
+        if ($lazy) {
1365
+            $cache = $this->lazyCache;
1366
+        } else {
1367
+            $cache = $this->fastCache;
1368
+        }
1369
+
1370
+        if (!isset($cache[$userId][$app][$key])) {
1371
+            throw new UnknownKeyException('unknown config key');
1372
+        }
1373
+
1374
+        $value = $cache[$userId][$app][$key];
1375
+        $flags = $this->getValueFlags($userId, $app, $key);
1376
+        if ($indexed) {
1377
+            $indexed = $value;
1378
+        } else {
1379
+            $flags &= ~self::FLAG_INDEXED;
1380
+            $indexed = '';
1381
+        }
1382
+
1383
+        $update = $this->connection->getQueryBuilder();
1384
+        $update->update('preferences')
1385
+            ->set('flags', $update->createNamedParameter($flags, IQueryBuilder::PARAM_INT))
1386
+            ->set('indexed', $update->createNamedParameter($indexed))
1387
+            ->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
1388
+            ->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
1389
+            ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1390
+        $update->executeStatement();
1391
+
1392
+        $this->valueDetails[$userId][$app][$key]['flags'] = $flags;
1393
+
1394
+        return true;
1395
+    }
1396
+
1397
+
1398
+    /**
1399
+     * @inheritDoc
1400
+     *
1401
+     * @param string $app
1402
+     * @param string $key
1403
+     * @param bool $indexed
1404
+     *
1405
+     * @since 31.0.0
1406
+     */
1407
+    public function updateGlobalIndexed(string $app, string $key, bool $indexed): void {
1408
+        $this->assertParams('', $app, $key, allowEmptyUser: true);
1409
+        $this->matchAndApplyLexiconDefinition('', $app, $key);
1410
+
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
+            }
1417
+        }
1418
+
1419
+        // we clear all cache
1420
+        $this->clearCacheAll();
1421
+    }
1422
+
1423
+    /**
1424
+     * @inheritDoc
1425
+     *
1426
+     * @param string $userId id of the user
1427
+     * @param string $app id of the app
1428
+     * @param string $key config key
1429
+     * @param bool $lazy TRUE to set as lazy loaded, FALSE to unset
1430
+     *
1431
+     * @return bool TRUE if entry was found in database and an update was necessary
1432
+     * @since 31.0.0
1433
+     */
1434
+    public function updateLazy(string $userId, string $app, string $key, bool $lazy): bool {
1435
+        $this->assertParams($userId, $app, $key);
1436
+        $this->loadConfigAll($userId);
1437
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
1438
+
1439
+        try {
1440
+            if ($lazy === $this->isLazy($userId, $app, $key)) {
1441
+                return false;
1442
+            }
1443
+        } catch (UnknownKeyException) {
1444
+            return false;
1445
+        }
1446
+
1447
+        $update = $this->connection->getQueryBuilder();
1448
+        $update->update('preferences')
1449
+            ->set('lazy', $update->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))
1450
+            ->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
1451
+            ->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
1452
+            ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1453
+        $update->executeStatement();
1454
+
1455
+        // At this point, it is a lot safer to clean cache
1456
+        $this->clearCache($userId);
1457
+
1458
+        return true;
1459
+    }
1460
+
1461
+    /**
1462
+     * @inheritDoc
1463
+     *
1464
+     * @param string $app id of the app
1465
+     * @param string $key config key
1466
+     * @param bool $lazy TRUE to set as lazy loaded, FALSE to unset
1467
+     *
1468
+     * @since 31.0.0
1469
+     */
1470
+    public function updateGlobalLazy(string $app, string $key, bool $lazy): void {
1471
+        $this->assertParams('', $app, $key, allowEmptyUser: true);
1472
+        $this->matchAndApplyLexiconDefinition('', $app, $key);
1473
+
1474
+        $update = $this->connection->getQueryBuilder();
1475
+        $update->update('preferences')
1476
+            ->set('lazy', $update->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))
1477
+            ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
1478
+            ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1479
+        $update->executeStatement();
1480
+
1481
+        $this->clearCacheAll();
1482
+    }
1483
+
1484
+    /**
1485
+     * @inheritDoc
1486
+     *
1487
+     * @param string $userId id of the user
1488
+     * @param string $app id of the app
1489
+     * @param string $key config key
1490
+     *
1491
+     * @return array
1492
+     * @throws UnknownKeyException if config key is not known in database
1493
+     * @since 31.0.0
1494
+     */
1495
+    public function getDetails(string $userId, string $app, string $key): array {
1496
+        $this->assertParams($userId, $app, $key);
1497
+        $this->loadConfigAll($userId);
1498
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
1499
+
1500
+        $lazy = $this->isLazy($userId, $app, $key);
1501
+
1502
+        if ($lazy) {
1503
+            $cache = $this->lazyCache[$userId];
1504
+        } else {
1505
+            $cache = $this->fastCache[$userId];
1506
+        }
1507
+
1508
+        $type = $this->getValueType($userId, $app, $key);
1509
+        try {
1510
+            $typeString = $type->getDefinition();
1511
+        } catch (IncorrectTypeException $e) {
1512
+            $this->logger->warning('type stored in database is not correct', ['exception' => $e, 'type' => $type]);
1513
+            $typeString = (string)$type->value;
1514
+        }
1515
+
1516
+        if (!isset($cache[$app][$key])) {
1517
+            throw new UnknownKeyException('unknown config key');
1518
+        }
1519
+
1520
+        $value = $cache[$app][$key];
1521
+        $sensitive = $this->isSensitive($userId, $app, $key, null);
1522
+        $this->decryptSensitiveValue($userId, $app, $key, $value);
1523
+
1524
+        return [
1525
+            'userId' => $userId,
1526
+            'app' => $app,
1527
+            'key' => $key,
1528
+            'value' => $value,
1529
+            'type' => $type->value,
1530
+            'lazy' => $lazy,
1531
+            'typeString' => $typeString,
1532
+            'sensitive' => $sensitive
1533
+        ];
1534
+    }
1535
+
1536
+    /**
1537
+     * @inheritDoc
1538
+     *
1539
+     * @param string $userId id of the user
1540
+     * @param string $app id of the app
1541
+     * @param string $key config key
1542
+     *
1543
+     * @since 31.0.0
1544
+     */
1545
+    public function deleteUserConfig(string $userId, string $app, string $key): void {
1546
+        $this->assertParams($userId, $app, $key);
1547
+        $this->matchAndApplyLexiconDefinition($userId, $app, $key);
1548
+
1549
+        $qb = $this->connection->getQueryBuilder();
1550
+        $qb->delete('preferences')
1551
+            ->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId)))
1552
+            ->andWhere($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
1553
+            ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
1554
+        $qb->executeStatement();
1555
+
1556
+        unset($this->lazyCache[$userId][$app][$key]);
1557
+        unset($this->fastCache[$userId][$app][$key]);
1558
+        unset($this->valueDetails[$userId][$app][$key]);
1559
+    }
1560
+
1561
+    /**
1562
+     * @inheritDoc
1563
+     *
1564
+     * @param string $app id of the app
1565
+     * @param string $key config key
1566
+     *
1567
+     * @since 31.0.0
1568
+     */
1569
+    public function deleteKey(string $app, string $key): void {
1570
+        $this->assertParams('', $app, $key, allowEmptyUser: true);
1571
+        $this->matchAndApplyLexiconDefinition('', $app, $key);
1572
+
1573
+        $qb = $this->connection->getQueryBuilder();
1574
+        $qb->delete('preferences')
1575
+            ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
1576
+            ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
1577
+        $qb->executeStatement();
1578
+
1579
+        $this->clearCacheAll();
1580
+    }
1581
+
1582
+    /**
1583
+     * @inheritDoc
1584
+     *
1585
+     * @param string $app id of the app
1586
+     *
1587
+     * @since 31.0.0
1588
+     */
1589
+    public function deleteApp(string $app): void {
1590
+        $this->assertParams('', $app, allowEmptyUser: true);
1591
+
1592
+        $qb = $this->connection->getQueryBuilder();
1593
+        $qb->delete('preferences')
1594
+            ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
1595
+        $qb->executeStatement();
1596
+
1597
+        $this->clearCacheAll();
1598
+    }
1599
+
1600
+    public function deleteAllUserConfig(string $userId): void {
1601
+        $this->assertParams($userId, '', allowEmptyApp: true);
1602
+        $qb = $this->connection->getQueryBuilder();
1603
+        $qb->delete('preferences')
1604
+            ->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId)));
1605
+        $qb->executeStatement();
1606
+
1607
+        $this->clearCache($userId);
1608
+    }
1609
+
1610
+    /**
1611
+     * @inheritDoc
1612
+     *
1613
+     * @param string $userId id of the user
1614
+     * @param bool $reload set to TRUE to refill cache instantly after clearing it.
1615
+     *
1616
+     * @since 31.0.0
1617
+     */
1618
+    public function clearCache(string $userId, bool $reload = false): void {
1619
+        $this->assertParams($userId, allowEmptyApp: true);
1620
+        $this->lazyLoaded[$userId] = $this->fastLoaded[$userId] = false;
1621
+        $this->lazyCache[$userId] = $this->fastCache[$userId] = $this->valueDetails[$userId] = [];
1622
+
1623
+        if (!$reload) {
1624
+            return;
1625
+        }
1626
+
1627
+        $this->loadConfigAll($userId);
1628
+    }
1629
+
1630
+    /**
1631
+     * @inheritDoc
1632
+     *
1633
+     * @since 31.0.0
1634
+     */
1635
+    public function clearCacheAll(): void {
1636
+        $this->lazyLoaded = $this->fastLoaded = [];
1637
+        $this->lazyCache = $this->fastCache = $this->valueDetails = $this->configLexiconDetails = [];
1638
+    }
1639
+
1640
+    /**
1641
+     * For debug purpose.
1642
+     * Returns the cached data.
1643
+     *
1644
+     * @return array
1645
+     * @since 31.0.0
1646
+     * @internal
1647
+     */
1648
+    public function statusCache(): array {
1649
+        return [
1650
+            'fastLoaded' => $this->fastLoaded,
1651
+            'fastCache' => $this->fastCache,
1652
+            'lazyLoaded' => $this->lazyLoaded,
1653
+            'lazyCache' => $this->lazyCache,
1654
+            'valueDetails' => $this->valueDetails,
1655
+        ];
1656
+    }
1657
+
1658
+    /**
1659
+     * @param int $needle bitflag to search
1660
+     * @param int $flags all flags
1661
+     *
1662
+     * @return bool TRUE if bitflag $needle is set in $flags
1663
+     */
1664
+    private function isFlagged(int $needle, int $flags): bool {
1665
+        return (($needle & $flags) !== 0);
1666
+    }
1667
+
1668
+    /**
1669
+     * Confirm the string set for app and key fit the database description
1670
+     *
1671
+     * @param string $userId
1672
+     * @param string $app assert $app fit in database
1673
+     * @param string $prefKey assert config key fit in database
1674
+     * @param bool $allowEmptyUser
1675
+     * @param bool $allowEmptyApp $app can be empty string
1676
+     * @param ValueType|null $valueType assert value type is only one type
1677
+     */
1678
+    private function assertParams(
1679
+        string $userId = '',
1680
+        string $app = '',
1681
+        string $prefKey = '',
1682
+        bool $allowEmptyUser = false,
1683
+        bool $allowEmptyApp = false,
1684
+    ): void {
1685
+        if (!$allowEmptyUser && $userId === '') {
1686
+            throw new InvalidArgumentException('userId cannot be an empty string');
1687
+        }
1688
+        if (!$allowEmptyApp && $app === '') {
1689
+            throw new InvalidArgumentException('app cannot be an empty string');
1690
+        }
1691
+        if (strlen($userId) > self::USER_MAX_LENGTH) {
1692
+            throw new InvalidArgumentException('Value (' . $userId . ') for userId is too long (' . self::USER_MAX_LENGTH . ')');
1693
+        }
1694
+        if (strlen($app) > self::APP_MAX_LENGTH) {
1695
+            throw new InvalidArgumentException('Value (' . $app . ') for app is too long (' . self::APP_MAX_LENGTH . ')');
1696
+        }
1697
+        if (strlen($prefKey) > self::KEY_MAX_LENGTH) {
1698
+            throw new InvalidArgumentException('Value (' . $prefKey . ') for key is too long (' . self::KEY_MAX_LENGTH . ')');
1699
+        }
1700
+    }
1701
+
1702
+    private function loadConfigAll(string $userId): void {
1703
+        $this->loadConfig($userId, null);
1704
+    }
1705
+
1706
+    /**
1707
+     * Load normal config or config set as lazy loaded
1708
+     *
1709
+     * @param bool|null $lazy set to TRUE to load config set as lazy loaded, set to NULL to load all config
1710
+     */
1711
+    private function loadConfig(string $userId, ?bool $lazy = false): void {
1712
+        if ($this->isLoaded($userId, $lazy)) {
1713
+            return;
1714
+        }
1715
+
1716
+        if (($lazy ?? true) !== false) { // if lazy is null or true, we debug log
1717
+            $this->logger->debug('The loading of lazy UserConfig values have been requested', ['exception' => new \RuntimeException('ignorable exception')]);
1718
+        }
1719
+
1720
+        $qb = $this->connection->getQueryBuilder();
1721
+        $qb->from('preferences');
1722
+        $qb->select('appid', 'configkey', 'configvalue', 'type', 'flags');
1723
+        $qb->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId)));
1724
+
1725
+        // we only need value from lazy when loadConfig does not specify it
1726
+        if ($lazy !== null) {
1727
+            $qb->andWhere($qb->expr()->eq('lazy', $qb->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT)));
1728
+        } else {
1729
+            $qb->addSelect('lazy');
1730
+        }
1731
+
1732
+        $result = $qb->executeQuery();
1733
+        $rows = $result->fetchAll();
1734
+        foreach ($rows as $row) {
1735
+            if (($row['lazy'] ?? ($lazy ?? 0) ? 1 : 0) === 1) {
1736
+                $this->lazyCache[$userId][$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1737
+            } else {
1738
+                $this->fastCache[$userId][$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1739
+            }
1740
+            $this->valueDetails[$userId][$row['appid']][$row['configkey']] = ['type' => ValueType::from((int)($row['type'] ?? 0)), 'flags' => (int)$row['flags']];
1741
+        }
1742
+        $result->closeCursor();
1743
+        $this->setAsLoaded($userId, $lazy);
1744
+    }
1745
+
1746
+    /**
1747
+     * if $lazy is:
1748
+     *  - false: will returns true if fast config are loaded
1749
+     *  - true : will returns true if lazy config are loaded
1750
+     *  - null : will returns true if both config are loaded
1751
+     *
1752
+     * @param string $userId
1753
+     * @param bool $lazy
1754
+     *
1755
+     * @return bool
1756
+     */
1757
+    private function isLoaded(string $userId, ?bool $lazy): bool {
1758
+        if ($lazy === null) {
1759
+            return ($this->lazyLoaded[$userId] ?? false) && ($this->fastLoaded[$userId] ?? false);
1760
+        }
1761
+
1762
+        return $lazy ? $this->lazyLoaded[$userId] ?? false : $this->fastLoaded[$userId] ?? false;
1763
+    }
1764
+
1765
+    /**
1766
+     * if $lazy is:
1767
+     * - false: set fast config as loaded
1768
+     * - true : set lazy config as loaded
1769
+     * - null : set both config as loaded
1770
+     *
1771
+     * @param string $userId
1772
+     * @param bool $lazy
1773
+     */
1774
+    private function setAsLoaded(string $userId, ?bool $lazy): void {
1775
+        if ($lazy === null) {
1776
+            $this->fastLoaded[$userId] = $this->lazyLoaded[$userId] = true;
1777
+            return;
1778
+        }
1779
+
1780
+        // We also create empty entry to keep both fastLoaded/lazyLoaded synced
1781
+        if ($lazy) {
1782
+            $this->lazyLoaded[$userId] = true;
1783
+            $this->fastLoaded[$userId] = $this->fastLoaded[$userId] ?? false;
1784
+            $this->fastCache[$userId] = $this->fastCache[$userId] ?? [];
1785
+        } else {
1786
+            $this->fastLoaded[$userId] = true;
1787
+            $this->lazyLoaded[$userId] = $this->lazyLoaded[$userId] ?? false;
1788
+            $this->lazyCache[$userId] = $this->lazyCache[$userId] ?? [];
1789
+        }
1790
+    }
1791
+
1792
+    /**
1793
+     * **Warning:** this will load all lazy values from the database
1794
+     *
1795
+     * @param string $userId id of the user
1796
+     * @param string $app id of the app
1797
+     * @param bool $filtered TRUE to hide sensitive config values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
1798
+     *
1799
+     * @return array<string, string|int|float|bool|array>
1800
+     */
1801
+    private function formatAppValues(string $userId, string $app, array $values, bool $filtered = false): array {
1802
+        foreach ($values as $key => $value) {
1803
+            //$key = (string)$key;
1804
+            try {
1805
+                $type = $this->getValueType($userId, $app, (string)$key);
1806
+            } catch (UnknownKeyException) {
1807
+                continue;
1808
+            }
1809
+
1810
+            if ($this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags'] ?? 0)) {
1811
+                if ($filtered) {
1812
+                    $value = IConfig::SENSITIVE_VALUE;
1813
+                    $type = ValueType::STRING;
1814
+                } else {
1815
+                    $this->decryptSensitiveValue($userId, $app, (string)$key, $value);
1816
+                }
1817
+            }
1818
+
1819
+            $values[$key] = $this->convertTypedValue($value, $type);
1820
+        }
1821
+
1822
+        return $values;
1823
+    }
1824
+
1825
+    /**
1826
+     * convert string value to the expected type
1827
+     *
1828
+     * @param string $value
1829
+     * @param ValueType $type
1830
+     *
1831
+     * @return string|int|float|bool|array
1832
+     */
1833
+    private function convertTypedValue(string $value, ValueType $type): string|int|float|bool|array {
1834
+        switch ($type) {
1835
+            case ValueType::INT:
1836
+                return (int)$value;
1837
+            case ValueType::FLOAT:
1838
+                return (float)$value;
1839
+            case ValueType::BOOL:
1840
+                return in_array(strtolower($value), ['1', 'true', 'yes', 'on']);
1841
+            case ValueType::ARRAY:
1842
+                try {
1843
+                    return json_decode($value, true, flags: JSON_THROW_ON_ERROR);
1844
+                } catch (JsonException) {
1845
+                    // ignoreable
1846
+                }
1847
+                break;
1848
+        }
1849
+        return $value;
1850
+    }
1851
+
1852
+
1853
+    /**
1854
+     * will change referenced $value with the decrypted value in case of encrypted (sensitive value)
1855
+     *
1856
+     * @param string $userId
1857
+     * @param string $app
1858
+     * @param string $key
1859
+     * @param string $value
1860
+     */
1861
+    private function decryptSensitiveValue(string $userId, string $app, string $key, string &$value): void {
1862
+        if (!$this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags'] ?? 0)) {
1863
+            return;
1864
+        }
1865
+
1866
+        if (!str_starts_with($value, self::ENCRYPTION_PREFIX)) {
1867
+            return;
1868
+        }
1869
+
1870
+        try {
1871
+            $value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
1872
+        } catch (\Exception $e) {
1873
+            $this->logger->warning('could not decrypt sensitive value', [
1874
+                'userId' => $userId,
1875
+                'app' => $app,
1876
+                'key' => $key,
1877
+                'value' => $value,
1878
+                'exception' => $e
1879
+            ]);
1880
+        }
1881
+    }
1882
+
1883
+    /**
1884
+     * Match and apply current use of config values with defined lexicon.
1885
+     * Set $lazy to NULL only if only interested into checking that $key is alias.
1886
+     *
1887
+     * @throws UnknownKeyException
1888
+     * @throws TypeConflictException
1889
+     * @return bool FALSE if conflict with defined lexicon were observed in the process
1890
+     */
1891
+    private function matchAndApplyLexiconDefinition(
1892
+        string $userId,
1893
+        string $app,
1894
+        string &$key,
1895
+        ?bool &$lazy = null,
1896
+        ValueType &$type = ValueType::MIXED,
1897
+        int &$flags = 0,
1898
+        ?string &$default = null,
1899
+    ): bool {
1900
+        $configDetails = $this->getConfigDetailsFromLexicon($app);
1901
+        if (array_key_exists($key, $configDetails['aliases']) && !$this->ignoreLexiconAliases) {
1902
+            // in case '$rename' is set in ConfigLexiconEntry, we use the new config key
1903
+            $key = $configDetails['aliases'][$key];
1904
+        }
1905
+
1906
+        if (!array_key_exists($key, $configDetails['entries'])) {
1907
+            return $this->applyLexiconStrictness($configDetails['strictness'], $app . '/' . $key);
1908
+        }
1909
+
1910
+        // if lazy is NULL, we ignore all check on the type/lazyness/default from Lexicon
1911
+        if ($lazy === null) {
1912
+            return true;
1913
+        }
1914
+
1915
+        /** @var Entry $configValue */
1916
+        $configValue = $configDetails['entries'][$key];
1917
+        if ($type === ValueType::MIXED) {
1918
+            // we overwrite if value was requested as mixed
1919
+            $type = $configValue->getValueType();
1920
+        } elseif ($configValue->getValueType() !== $type) {
1921
+            throw new TypeConflictException('The user config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
1922
+        }
1923
+
1924
+        $lazy = $configValue->isLazy();
1925
+        $flags = $configValue->getFlags();
1926
+        if ($configValue->isDeprecated()) {
1927
+            $this->logger->notice('User config key ' . $app . '/' . $key . ' is set as deprecated.');
1928
+        }
1929
+
1930
+        $enforcedValue = $this->config->getSystemValue('lexicon.default.userconfig.enforced', [])[$app][$key] ?? false;
1931
+        if (!$enforcedValue && $this->hasKey($userId, $app, $key, $lazy)) {
1932
+            // if key exists there should be no need to extract default
1933
+            return true;
1934
+        }
1935
+
1936
+        // only look for default if needed, default from Lexicon got priority if not overwritten by admin
1937
+        if ($default !== null) {
1938
+            $default = $this->getSystemDefault($app, $configValue) ?? $configValue->getDefault($this->presetManager->getLexiconPreset()) ?? $default;
1939
+        }
1940
+
1941
+        // returning false will make get() returning $default and set() not changing value in database
1942
+        return !$enforcedValue;
1943
+    }
1944
+
1945
+    /**
1946
+     * get default value set in config/config.php if stored in key:
1947
+     *
1948
+     * 'lexicon.default.userconfig' => [
1949
+     *        <appId> => [
1950
+     *           <configKey> => 'my value',
1951
+     *        ]
1952
+     *     ],
1953
+     *
1954
+     * The entry is converted to string to fit the expected type when managing default value
1955
+     */
1956
+    private function getSystemDefault(string $appId, Entry $configValue): ?string {
1957
+        $default = $this->config->getSystemValue('lexicon.default.userconfig', [])[$appId][$configValue->getKey()] ?? null;
1958
+        if ($default === null) {
1959
+            // no system default, using default default.
1960
+            return null;
1961
+        }
1962
+
1963
+        return $configValue->convertToString($default);
1964
+    }
1965
+
1966
+    /**
1967
+     * manage ConfigLexicon behavior based on strictness set in IConfigLexicon
1968
+     *
1969
+     * @param Strictness|null $strictness
1970
+     * @param string $line
1971
+     *
1972
+     * @return bool TRUE if conflict can be fully ignored
1973
+     * @throws UnknownKeyException
1974
+     * @see ILexicon::getStrictness()
1975
+     */
1976
+    private function applyLexiconStrictness(?Strictness $strictness, string $configAppKey): bool {
1977
+        if ($strictness === null) {
1978
+            return true;
1979
+        }
1980
+
1981
+        $line = 'The user config key ' . $configAppKey . ' is not defined in the config lexicon';
1982
+        switch ($strictness) {
1983
+            case Strictness::IGNORE:
1984
+                return true;
1985
+            case Strictness::NOTICE:
1986
+                if (!in_array($configAppKey, $this->strictnessApplied, true)) {
1987
+                    $this->strictnessApplied[] = $configAppKey;
1988
+                    $this->logger->notice($line);
1989
+                }
1990
+                return true;
1991
+            case Strictness::WARNING:
1992
+                if (!in_array($configAppKey, $this->strictnessApplied, true)) {
1993
+                    $this->strictnessApplied[] = $configAppKey;
1994
+                    $this->logger->warning($line);
1995
+                }
1996
+                return false;
1997
+            case Strictness::EXCEPTION:
1998
+                throw new UnknownKeyException($line);
1999
+        }
2000
+
2001
+        throw new UnknownKeyException($line);
2002
+    }
2003
+
2004
+    /**
2005
+     * extract details from registered $appId's config lexicon
2006
+     *
2007
+     * @param string $appId
2008
+     *
2009
+     * @return array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}
2010
+     * @internal
2011
+     */
2012
+    public function getConfigDetailsFromLexicon(string $appId): array {
2013
+        if (!array_key_exists($appId, $this->configLexiconDetails)) {
2014
+            $entries = $aliases = [];
2015
+            $bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
2016
+            $configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
2017
+            foreach ($configLexicon?->getUserConfigs() ?? [] as $configEntry) {
2018
+                $entries[$configEntry->getKey()] = $configEntry;
2019
+                if ($configEntry->getRename() !== null) {
2020
+                    $aliases[$configEntry->getRename()] = $configEntry->getKey();
2021
+                }
2022
+            }
2023
+
2024
+            $this->configLexiconDetails[$appId] = [
2025
+                'entries' => $entries,
2026
+                'aliases' => $aliases,
2027
+                'strictness' => $configLexicon?->getStrictness() ?? Strictness::IGNORE
2028
+            ];
2029
+        }
2030
+
2031
+        return $this->configLexiconDetails[$appId];
2032
+    }
2033
+
2034
+    /**
2035
+     * get Lexicon Entry using appId and config key entry
2036
+     *
2037
+     * @return Entry|null NULL if entry does not exist in user's Lexicon
2038
+     * @internal
2039
+     */
2040
+    public function getLexiconEntry(string $appId, string $key): ?Entry {
2041
+        return $this->getConfigDetailsFromLexicon($appId)['entries'][$key] ?? null;
2042
+    }
2043
+
2044
+    /**
2045
+     * if set to TRUE, ignore aliases defined in Config Lexicon during the use of the methods of this class
2046
+     *
2047
+     * @internal
2048
+     */
2049
+    public function ignoreLexiconAliases(bool $ignore): void {
2050
+        $this->ignoreLexiconAliases = $ignore;
2051
+    }
2052 2052
 }
Please login to merge, or discard this patch.
Spacing   +33 added lines, -33 removed lines patch added patch discarded remove patch
@@ -54,11 +54,11 @@  discard block
 block discarded – undo
54 54
 	private const ENCRYPTION_PREFIX_LENGTH = 22; // strlen(self::ENCRYPTION_PREFIX)
55 55
 
56 56
 	/** @var array<string, array<string, array<string, mixed>>> [ass'user_id' => ['app_id' => ['key' => 'value']]] */
57
-	private array $fastCache = [];   // cache for normal config keys
57
+	private array $fastCache = []; // cache for normal config keys
58 58
 	/** @var array<string, array<string, array<string, mixed>>> ['user_id' => ['app_id' => ['key' => 'value']]] */
59
-	private array $lazyCache = [];   // cache for lazy config keys
59
+	private array $lazyCache = []; // cache for lazy config keys
60 60
 	/** @var array<string, array<string, array<string, array<string, mixed>>>> ['user_id' => ['app_id' => ['key' => ['type' => ValueType, 'flags' => bitflag]]]] */
61
-	private array $valueDetails = [];  // type for all config values
61
+	private array $valueDetails = []; // type for all config values
62 62
 	/** @var array<string, boolean> ['user_id' => bool] */
63 63
 	private array $fastLoaded = [];
64 64
 	/** @var array<string, boolean> ['user_id' => bool] */
@@ -270,7 +270,7 @@  discard block
 block discarded – undo
270 270
 		// array_merge() will remove numeric keys (here config keys), so addition arrays instead
271 271
 		$values = array_filter(
272 272
 			$this->formatAppValues($userId, $app, ($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? []), $filtered),
273
-			function (string $key) use ($prefix): bool {
273
+			function(string $key) use ($prefix): bool {
274 274
 				// filter values based on $prefix
275 275
 				return str_starts_with($key, $prefix);
276 276
 			}, ARRAY_FILTER_USE_KEY
@@ -314,7 +314,7 @@  discard block
 block discarded – undo
314 314
 	 * @since 31.0.0
315 315
 	 */
316 316
 	public function getValuesByApps(string $userId, string $key, bool $lazy = false, ?ValueType $typedAs = null): array {
317
-		$this->assertParams($userId, '', $key, allowEmptyApp: true);
317
+		$this->assertParams($userId, '', $key, allowEmptyApp : true);
318 318
 		$this->loadConfig($userId, $lazy);
319 319
 
320 320
 		/** @var array<array-key, array<array-key, mixed>> $cache */
@@ -331,7 +331,7 @@  discard block
 block discarded – undo
331 331
 				try {
332 332
 					$this->decryptSensitiveValue($userId, $app, $key, $value);
333 333
 					$value = $this->convertTypedValue($value, $typedAs ?? $this->getValueType($userId, $app, $key, $lazy));
334
-				} catch (IncorrectTypeException|UnknownKeyException) {
334
+				} catch (IncorrectTypeException | UnknownKeyException) {
335 335
 				}
336 336
 				$values[$app] = $value;
337 337
 			}
@@ -358,7 +358,7 @@  discard block
 block discarded – undo
358 358
 		?ValueType $typedAs = null,
359 359
 		?array $userIds = null,
360 360
 	): array {
361
-		$this->assertParams('', $app, $key, allowEmptyUser: true);
361
+		$this->assertParams('', $app, $key, allowEmptyUser : true);
362 362
 		$this->matchAndApplyLexiconDefinition('', $app, $key);
363 363
 
364 364
 		$qb = $this->connection->getQueryBuilder();
@@ -369,12 +369,12 @@  discard block
 block discarded – undo
369 369
 
370 370
 		$values = [];
371 371
 		// this nested function will execute current Query and store result within $values.
372
-		$executeAndStoreValue = function (IQueryBuilder $qb) use (&$values, $typedAs): IResult {
372
+		$executeAndStoreValue = function(IQueryBuilder $qb) use (&$values, $typedAs): IResult {
373 373
 			$result = $qb->executeQuery();
374 374
 			while ($row = $result->fetch()) {
375 375
 				$value = $row['configvalue'];
376 376
 				try {
377
-					$value = $this->convertTypedValue($value, $typedAs ?? ValueType::from((int)$row['type']));
377
+					$value = $this->convertTypedValue($value, $typedAs ?? ValueType::from((int) $row['type']));
378 378
 				} catch (IncorrectTypeException) {
379 379
 				}
380 380
 				$values[$row['userid']] = $value;
@@ -427,7 +427,7 @@  discard block
 block discarded – undo
427 427
 	 * @since 31.0.0
428 428
 	 */
429 429
 	public function searchUsersByValueInt(string $app, string $key, int $value): Generator {
430
-		return $this->searchUsersByValueString($app, $key, (string)$value);
430
+		return $this->searchUsersByValueString($app, $key, (string) $value);
431 431
 	}
432 432
 
433 433
 	/**
@@ -473,7 +473,7 @@  discard block
 block discarded – undo
473 473
 	 *
474 474
 	 * @return Generator<string>
475 475
 	 */
476
-	private function searchUsersByTypedValue(string $app, string $key, string|array $value, bool $caseInsensitive = false): Generator {
476
+	private function searchUsersByTypedValue(string $app, string $key, string | array $value, bool $caseInsensitive = false): Generator {
477 477
 		$this->assertParams('', $app, $key, allowEmptyUser: true);
478 478
 		$this->matchAndApplyLexiconDefinition('', $app, $key);
479 479
 
@@ -617,7 +617,7 @@  discard block
 block discarded – undo
617 617
 		int $default = 0,
618 618
 		bool $lazy = false,
619 619
 	): int {
620
-		return (int)$this->getTypedValue($userId, $app, $key, (string)$default, $lazy, ValueType::INT);
620
+		return (int) $this->getTypedValue($userId, $app, $key, (string) $default, $lazy, ValueType::INT);
621 621
 	}
622 622
 
623 623
 	/**
@@ -642,7 +642,7 @@  discard block
 block discarded – undo
642 642
 		float $default = 0,
643 643
 		bool $lazy = false,
644 644
 	): float {
645
-		return (float)$this->getTypedValue($userId, $app, $key, (string)$default, $lazy, ValueType::FLOAT);
645
+		return (float) $this->getTypedValue($userId, $app, $key, (string) $default, $lazy, ValueType::FLOAT);
646 646
 	}
647 647
 
648 648
 	/**
@@ -935,7 +935,7 @@  discard block
 block discarded – undo
935 935
 			$userId,
936 936
 			$app,
937 937
 			$key,
938
-			(string)$value,
938
+			(string) $value,
939 939
 			$lazy,
940 940
 			$flags,
941 941
 			ValueType::INT
@@ -969,7 +969,7 @@  discard block
 block discarded – undo
969 969
 			$userId,
970 970
 			$app,
971 971
 			$key,
972
-			(string)$value,
972
+			(string) $value,
973 973
 			$lazy,
974 974
 			$flags,
975 975
 			ValueType::FLOAT
@@ -1092,7 +1092,7 @@  discard block
 block discarded – undo
1092 1092
 		$origValue = $value;
1093 1093
 		$sensitive = $this->isFlagged(self::FLAG_SENSITIVE, $flags);
1094 1094
 		if ($sensitive || ($this->hasKey($userId, $app, $key, $lazy) && $this->isSensitive($userId, $app, $key, $lazy))) {
1095
-			$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
1095
+			$value = self::ENCRYPTION_PREFIX.$this->crypto->encrypt($value);
1096 1096
 			$flags |= self::FLAG_SENSITIVE;
1097 1097
 		}
1098 1098
 
@@ -1174,7 +1174,7 @@  discard block
 block discarded – undo
1174 1174
 					$currTypeDef = $currType->value;
1175 1175
 					$typeDef = $type->value;
1176 1176
 				}
1177
-				throw new TypeConflictException('conflict between new type (' . $typeDef . ') and old type (' . $currTypeDef . ')');
1177
+				throw new TypeConflictException('conflict between new type ('.$typeDef.') and old type ('.$currTypeDef.')');
1178 1178
 			}
1179 1179
 
1180 1180
 			if ($lazy !== $this->isLazy($userId, $app, $key)) {
@@ -1288,7 +1288,7 @@  discard block
 block discarded – undo
1288 1288
 		$flags = $this->getValueFlags($userId, $app, $key);
1289 1289
 		if ($sensitive) {
1290 1290
 			$flags |= self::FLAG_SENSITIVE;
1291
-			$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
1291
+			$value = self::ENCRYPTION_PREFIX.$this->crypto->encrypt($value);
1292 1292
 		} else {
1293 1293
 			$flags &= ~self::FLAG_SENSITIVE;
1294 1294
 			$this->decryptSensitiveValue($userId, $app, $key, $value);
@@ -1510,7 +1510,7 @@  discard block
 block discarded – undo
1510 1510
 			$typeString = $type->getDefinition();
1511 1511
 		} catch (IncorrectTypeException $e) {
1512 1512
 			$this->logger->warning('type stored in database is not correct', ['exception' => $e, 'type' => $type]);
1513
-			$typeString = (string)$type->value;
1513
+			$typeString = (string) $type->value;
1514 1514
 		}
1515 1515
 
1516 1516
 		if (!isset($cache[$app][$key])) {
@@ -1689,13 +1689,13 @@  discard block
 block discarded – undo
1689 1689
 			throw new InvalidArgumentException('app cannot be an empty string');
1690 1690
 		}
1691 1691
 		if (strlen($userId) > self::USER_MAX_LENGTH) {
1692
-			throw new InvalidArgumentException('Value (' . $userId . ') for userId is too long (' . self::USER_MAX_LENGTH . ')');
1692
+			throw new InvalidArgumentException('Value ('.$userId.') for userId is too long ('.self::USER_MAX_LENGTH.')');
1693 1693
 		}
1694 1694
 		if (strlen($app) > self::APP_MAX_LENGTH) {
1695
-			throw new InvalidArgumentException('Value (' . $app . ') for app is too long (' . self::APP_MAX_LENGTH . ')');
1695
+			throw new InvalidArgumentException('Value ('.$app.') for app is too long ('.self::APP_MAX_LENGTH.')');
1696 1696
 		}
1697 1697
 		if (strlen($prefKey) > self::KEY_MAX_LENGTH) {
1698
-			throw new InvalidArgumentException('Value (' . $prefKey . ') for key is too long (' . self::KEY_MAX_LENGTH . ')');
1698
+			throw new InvalidArgumentException('Value ('.$prefKey.') for key is too long ('.self::KEY_MAX_LENGTH.')');
1699 1699
 		}
1700 1700
 	}
1701 1701
 
@@ -1737,7 +1737,7 @@  discard block
 block discarded – undo
1737 1737
 			} else {
1738 1738
 				$this->fastCache[$userId][$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1739 1739
 			}
1740
-			$this->valueDetails[$userId][$row['appid']][$row['configkey']] = ['type' => ValueType::from((int)($row['type'] ?? 0)), 'flags' => (int)$row['flags']];
1740
+			$this->valueDetails[$userId][$row['appid']][$row['configkey']] = ['type' => ValueType::from((int) ($row['type'] ?? 0)), 'flags' => (int) $row['flags']];
1741 1741
 		}
1742 1742
 		$result->closeCursor();
1743 1743
 		$this->setAsLoaded($userId, $lazy);
@@ -1802,7 +1802,7 @@  discard block
 block discarded – undo
1802 1802
 		foreach ($values as $key => $value) {
1803 1803
 			//$key = (string)$key;
1804 1804
 			try {
1805
-				$type = $this->getValueType($userId, $app, (string)$key);
1805
+				$type = $this->getValueType($userId, $app, (string) $key);
1806 1806
 			} catch (UnknownKeyException) {
1807 1807
 				continue;
1808 1808
 			}
@@ -1812,7 +1812,7 @@  discard block
 block discarded – undo
1812 1812
 					$value = IConfig::SENSITIVE_VALUE;
1813 1813
 					$type = ValueType::STRING;
1814 1814
 				} else {
1815
-					$this->decryptSensitiveValue($userId, $app, (string)$key, $value);
1815
+					$this->decryptSensitiveValue($userId, $app, (string) $key, $value);
1816 1816
 				}
1817 1817
 			}
1818 1818
 
@@ -1830,12 +1830,12 @@  discard block
 block discarded – undo
1830 1830
 	 *
1831 1831
 	 * @return string|int|float|bool|array
1832 1832
 	 */
1833
-	private function convertTypedValue(string $value, ValueType $type): string|int|float|bool|array {
1833
+	private function convertTypedValue(string $value, ValueType $type): string | int | float | bool | array {
1834 1834
 		switch ($type) {
1835 1835
 			case ValueType::INT:
1836
-				return (int)$value;
1836
+				return (int) $value;
1837 1837
 			case ValueType::FLOAT:
1838
-				return (float)$value;
1838
+				return (float) $value;
1839 1839
 			case ValueType::BOOL:
1840 1840
 				return in_array(strtolower($value), ['1', 'true', 'yes', 'on']);
1841 1841
 			case ValueType::ARRAY:
@@ -1893,7 +1893,7 @@  discard block
 block discarded – undo
1893 1893
 		string $app,
1894 1894
 		string &$key,
1895 1895
 		?bool &$lazy = null,
1896
-		ValueType &$type = ValueType::MIXED,
1896
+		ValueType & $type = ValueType::MIXED,
1897 1897
 		int &$flags = 0,
1898 1898
 		?string &$default = null,
1899 1899
 	): bool {
@@ -1904,7 +1904,7 @@  discard block
 block discarded – undo
1904 1904
 		}
1905 1905
 
1906 1906
 		if (!array_key_exists($key, $configDetails['entries'])) {
1907
-			return $this->applyLexiconStrictness($configDetails['strictness'], $app . '/' . $key);
1907
+			return $this->applyLexiconStrictness($configDetails['strictness'], $app.'/'.$key);
1908 1908
 		}
1909 1909
 
1910 1910
 		// if lazy is NULL, we ignore all check on the type/lazyness/default from Lexicon
@@ -1918,13 +1918,13 @@  discard block
 block discarded – undo
1918 1918
 			// we overwrite if value was requested as mixed
1919 1919
 			$type = $configValue->getValueType();
1920 1920
 		} elseif ($configValue->getValueType() !== $type) {
1921
-			throw new TypeConflictException('The user config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
1921
+			throw new TypeConflictException('The user config key '.$app.'/'.$key.' is typed incorrectly in relation to the config lexicon');
1922 1922
 		}
1923 1923
 
1924 1924
 		$lazy = $configValue->isLazy();
1925 1925
 		$flags = $configValue->getFlags();
1926 1926
 		if ($configValue->isDeprecated()) {
1927
-			$this->logger->notice('User config key ' . $app . '/' . $key . ' is set as deprecated.');
1927
+			$this->logger->notice('User config key '.$app.'/'.$key.' is set as deprecated.');
1928 1928
 		}
1929 1929
 
1930 1930
 		$enforcedValue = $this->config->getSystemValue('lexicon.default.userconfig.enforced', [])[$app][$key] ?? false;
@@ -1978,7 +1978,7 @@  discard block
 block discarded – undo
1978 1978
 			return true;
1979 1979
 		}
1980 1980
 
1981
-		$line = 'The user config key ' . $configAppKey . ' is not defined in the config lexicon';
1981
+		$line = 'The user config key '.$configAppKey.' is not defined in the config lexicon';
1982 1982
 		switch ($strictness) {
1983 1983
 			case Strictness::IGNORE:
1984 1984
 				return true;
Please login to merge, or discard this patch.
lib/private/AppConfig.php 2 patches
Indentation   +1793 added lines, -1793 removed lines patch added patch discarded remove patch
@@ -51,1797 +51,1797 @@
 block discarded – undo
51 51
  * @since 29.0.0 - Supporting types and lazy loading
52 52
  */
53 53
 class AppConfig implements IAppConfig {
54
-	private const APP_MAX_LENGTH = 32;
55
-	private const KEY_MAX_LENGTH = 64;
56
-	private const ENCRYPTION_PREFIX = '$AppConfigEncryption$';
57
-	private const ENCRYPTION_PREFIX_LENGTH = 21; // strlen(self::ENCRYPTION_PREFIX)
58
-	private const LOCAL_CACHE_KEY = 'OC\\AppConfig';
59
-	private const LOCAL_CACHE_TTL = 3;
60
-
61
-	/** @var array<string, array<string, string>> ['app_id' => ['config_key' => 'config_value']] */
62
-	private array $fastCache = [];   // cache for normal config keys
63
-	/** @var array<string, array<string, string>> ['app_id' => ['config_key' => 'config_value']] */
64
-	private array $lazyCache = [];   // cache for lazy config keys
65
-	/** @var array<string, array<string, int>> ['app_id' => ['config_key' => bitflag]] */
66
-	private array $valueTypes = [];  // type for all config values
67
-	private bool $fastLoaded = false;
68
-	private bool $lazyLoaded = false;
69
-	/** @var array<string, array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
70
-	private array $configLexiconDetails = [];
71
-	private bool $ignoreLexiconAliases = false;
72
-	private array $strictnessApplied = [];
73
-
74
-	/** @var ?array<string, string> */
75
-	private ?array $appVersionsCache = null;
76
-	private ?ICache $localCache = null;
77
-
78
-	public function __construct(
79
-		protected IDBConnection $connection,
80
-		protected IConfig $config,
81
-		private readonly ConfigManager $configManager,
82
-		private readonly PresetManager $presetManager,
83
-		protected LoggerInterface $logger,
84
-		protected ICrypto $crypto,
85
-		readonly CacheFactory $cacheFactory,
86
-	) {
87
-		if ($config->getSystemValueBool('cache_app_config', true) && $cacheFactory->isLocalCacheAvailable()) {
88
-			$cacheFactory->withServerVersionPrefix(function (ICacheFactory $factory) {
89
-				$this->localCache = $factory->createLocal();
90
-			});
91
-		}
92
-	}
93
-
94
-	/**
95
-	 * @inheritDoc
96
-	 *
97
-	 * @return list<string> list of app ids
98
-	 * @since 7.0.0
99
-	 */
100
-	public function getApps(): array {
101
-		$this->loadConfig(lazy: true);
102
-		$apps = array_merge(array_keys($this->fastCache), array_keys($this->lazyCache));
103
-		sort($apps);
104
-
105
-		return array_values(array_unique($apps));
106
-	}
107
-
108
-	/**
109
-	 * @inheritDoc
110
-	 *
111
-	 * @param string $app id of the app
112
-	 * @return list<string> list of stored config keys
113
-	 * @see searchKeys to not load lazy config keys
114
-	 *
115
-	 * @since 29.0.0
116
-	 */
117
-	public function getKeys(string $app): array {
118
-		$this->assertParams($app);
119
-		$this->loadConfig($app, true);
120
-		$keys = array_merge(array_keys($this->fastCache[$app] ?? []), array_keys($this->lazyCache[$app] ?? []));
121
-		sort($keys);
122
-
123
-		return array_values(array_unique($keys));
124
-	}
125
-
126
-	/**
127
-	 * @inheritDoc
128
-	 *
129
-	 * @param string $app id of the app
130
-	 * @param string $prefix returns only keys starting with this value
131
-	 * @param bool $lazy TRUE to search in lazy config keys
132
-	 * @return list<string> list of stored config keys
133
-	 * @since 32.0.0
134
-	 */
135
-	public function searchKeys(string $app, string $prefix = '', bool $lazy = false): array {
136
-		$this->assertParams($app);
137
-		$this->loadConfig($app, $lazy);
138
-		if ($lazy) {
139
-			$keys = array_keys($this->lazyCache[$app] ?? []);
140
-		} else {
141
-			$keys = array_keys($this->fastCache[$app] ?? []);
142
-		}
143
-
144
-		if ($prefix !== '') {
145
-			$keys = array_filter($keys, static fn (string $key): bool => str_starts_with($key, $prefix));
146
-		}
147
-
148
-		sort($keys);
149
-		return array_values(array_unique($keys));
150
-	}
151
-
152
-	/**
153
-	 * @inheritDoc
154
-	 *
155
-	 * @param string $app id of the app
156
-	 * @param string $key config key
157
-	 * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
158
-	 *
159
-	 * @return bool TRUE if key exists
160
-	 * @since 7.0.0
161
-	 * @since 29.0.0 Added the $lazy argument
162
-	 */
163
-	public function hasKey(string $app, string $key, ?bool $lazy = false): bool {
164
-		$this->assertParams($app, $key);
165
-		$this->loadConfig($app, $lazy ?? true);
166
-		$this->matchAndApplyLexiconDefinition($app, $key);
167
-
168
-		$hasLazy = isset($this->lazyCache[$app][$key]);
169
-		$hasFast = isset($this->fastCache[$app][$key]);
170
-		if ($lazy === null) {
171
-			return $hasLazy || $hasFast;
172
-		} else {
173
-			return $lazy ? $hasLazy : $hasFast;
174
-		}
175
-	}
176
-
177
-	/**
178
-	 * @param string $app id of the app
179
-	 * @param string $key config key
180
-	 * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
181
-	 *
182
-	 * @return bool
183
-	 * @throws AppConfigUnknownKeyException if config key is not known
184
-	 * @since 29.0.0
185
-	 */
186
-	public function isSensitive(string $app, string $key, ?bool $lazy = false): bool {
187
-		$this->assertParams($app, $key);
188
-		$this->loadConfig(null, $lazy ?? true);
189
-		$this->matchAndApplyLexiconDefinition($app, $key);
190
-
191
-		if (!isset($this->valueTypes[$app][$key])) {
192
-			throw new AppConfigUnknownKeyException('unknown config key');
193
-		}
194
-
195
-		return $this->isTyped(self::VALUE_SENSITIVE, $this->valueTypes[$app][$key]);
196
-	}
197
-
198
-	/**
199
-	 * @inheritDoc
200
-	 *
201
-	 * @param string $app if of the app
202
-	 * @param string $key config key
203
-	 *
204
-	 * @return bool TRUE if config is lazy loaded
205
-	 * @throws AppConfigUnknownKeyException if config key is not known
206
-	 * @see IAppConfig for details about lazy loading
207
-	 * @since 29.0.0
208
-	 */
209
-	public function isLazy(string $app, string $key): bool {
210
-		$this->assertParams($app, $key);
211
-		$this->matchAndApplyLexiconDefinition($app, $key);
212
-
213
-		// there is a huge probability the non-lazy config are already loaded
214
-		if ($this->hasKey($app, $key, false)) {
215
-			return false;
216
-		}
217
-
218
-		// key not found, we search in the lazy config
219
-		if ($this->hasKey($app, $key, true)) {
220
-			return true;
221
-		}
222
-
223
-		throw new AppConfigUnknownKeyException('unknown config key');
224
-	}
225
-
226
-
227
-	/**
228
-	 * @inheritDoc
229
-	 *
230
-	 * @param string $app id of the app
231
-	 * @param string $prefix config keys prefix to search
232
-	 * @param bool $filtered TRUE to hide sensitive config values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
233
-	 *
234
-	 * @return array<string, string|int|float|bool|array> [configKey => configValue]
235
-	 * @since 29.0.0
236
-	 */
237
-	public function getAllValues(string $app, string $prefix = '', bool $filtered = false): array {
238
-		$this->assertParams($app, $prefix);
239
-		// if we want to filter values, we need to get sensitivity
240
-		$this->loadConfig($app, true);
241
-		// array_merge() will remove numeric keys (here config keys), so addition arrays instead
242
-		$values = $this->formatAppValues($app, ($this->fastCache[$app] ?? []) + ($this->lazyCache[$app] ?? []));
243
-		$values = array_filter(
244
-			$values,
245
-			function (string $key) use ($prefix): bool {
246
-				return str_starts_with($key, $prefix); // filter values based on $prefix
247
-			}, ARRAY_FILTER_USE_KEY
248
-		);
249
-
250
-		if (!$filtered) {
251
-			return $values;
252
-		}
253
-
254
-		/**
255
-		 * Using the old (deprecated) list of sensitive values.
256
-		 */
257
-		foreach ($this->getSensitiveKeys($app) as $sensitiveKeyExp) {
258
-			$sensitiveKeys = preg_grep($sensitiveKeyExp, array_keys($values));
259
-			foreach ($sensitiveKeys as $sensitiveKey) {
260
-				$this->valueTypes[$app][$sensitiveKey] = ($this->valueTypes[$app][$sensitiveKey] ?? 0) | self::VALUE_SENSITIVE;
261
-			}
262
-		}
263
-
264
-		$result = [];
265
-		foreach ($values as $key => $value) {
266
-			$result[$key] = $this->isTyped(self::VALUE_SENSITIVE, $this->valueTypes[$app][$key] ?? 0) ? IConfig::SENSITIVE_VALUE : $value;
267
-		}
268
-
269
-		return $result;
270
-	}
271
-
272
-	/**
273
-	 * @inheritDoc
274
-	 *
275
-	 * @param string $key config key
276
-	 * @param bool $lazy search within lazy loaded config
277
-	 * @param int|null $typedAs enforce type for the returned values ({@see self::VALUE_STRING} and others)
278
-	 *
279
-	 * @return array<string, string|int|float|bool|array> [appId => configValue]
280
-	 * @since 29.0.0
281
-	 */
282
-	public function searchValues(string $key, bool $lazy = false, ?int $typedAs = null): array {
283
-		$this->assertParams('', $key, true);
284
-		$this->loadConfig(null, $lazy);
285
-
286
-		/** @var array<array-key, array<array-key, mixed>> $cache */
287
-		if ($lazy) {
288
-			$cache = $this->lazyCache;
289
-		} else {
290
-			$cache = $this->fastCache;
291
-		}
292
-
293
-		$values = [];
294
-		foreach (array_keys($cache) as $app) {
295
-			if (isset($cache[$app][$key])) {
296
-				$values[$app] = $this->convertTypedValue($cache[$app][$key], $typedAs ?? $this->getValueType((string)$app, $key, $lazy));
297
-			}
298
-		}
299
-
300
-		return $values;
301
-	}
302
-
303
-
304
-	/**
305
-	 * Get the config value as string.
306
-	 * If the value does not exist the given default will be returned.
307
-	 *
308
-	 * Set lazy to `null` to ignore it and get the value from either source.
309
-	 *
310
-	 * **WARNING:** Method is internal and **SHOULD** not be used, as it is better to get the value with a type.
311
-	 *
312
-	 * @param string $app id of the app
313
-	 * @param string $key config key
314
-	 * @param string $default config value
315
-	 * @param null|bool $lazy get config as lazy loaded or not. can be NULL
316
-	 *
317
-	 * @return string the value or $default
318
-	 * @internal
319
-	 * @since 29.0.0
320
-	 * @see IAppConfig for explanation about lazy loading
321
-	 * @see getValueString()
322
-	 * @see getValueInt()
323
-	 * @see getValueFloat()
324
-	 * @see getValueBool()
325
-	 * @see getValueArray()
326
-	 */
327
-	public function getValueMixed(
328
-		string $app,
329
-		string $key,
330
-		string $default = '',
331
-		?bool $lazy = false,
332
-	): string {
333
-		try {
334
-			$lazy = ($lazy === null) ? $this->isLazy($app, $key) : $lazy;
335
-		} catch (AppConfigUnknownKeyException) {
336
-			return $default;
337
-		}
338
-
339
-		return $this->getTypedValue(
340
-			$app,
341
-			$key,
342
-			$default,
343
-			$lazy,
344
-			self::VALUE_MIXED
345
-		);
346
-	}
347
-
348
-	/**
349
-	 * @inheritDoc
350
-	 *
351
-	 * @param string $app id of the app
352
-	 * @param string $key config key
353
-	 * @param string $default default value
354
-	 * @param bool $lazy search within lazy loaded config
355
-	 *
356
-	 * @return string stored config value or $default if not set in database
357
-	 * @throws InvalidArgumentException if one of the argument format is invalid
358
-	 * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
359
-	 * @since 29.0.0
360
-	 * @see IAppConfig for explanation about lazy loading
361
-	 */
362
-	public function getValueString(
363
-		string $app,
364
-		string $key,
365
-		string $default = '',
366
-		bool $lazy = false,
367
-	): string {
368
-		return $this->getTypedValue($app, $key, $default, $lazy, self::VALUE_STRING);
369
-	}
370
-
371
-	/**
372
-	 * @inheritDoc
373
-	 *
374
-	 * @param string $app id of the app
375
-	 * @param string $key config key
376
-	 * @param int $default default value
377
-	 * @param bool $lazy search within lazy loaded config
378
-	 *
379
-	 * @return int stored config value or $default if not set in database
380
-	 * @throws InvalidArgumentException if one of the argument format is invalid
381
-	 * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
382
-	 * @since 29.0.0
383
-	 * @see IAppConfig for explanation about lazy loading
384
-	 */
385
-	public function getValueInt(
386
-		string $app,
387
-		string $key,
388
-		int $default = 0,
389
-		bool $lazy = false,
390
-	): int {
391
-		return (int)$this->getTypedValue($app, $key, (string)$default, $lazy, self::VALUE_INT);
392
-	}
393
-
394
-	/**
395
-	 * @inheritDoc
396
-	 *
397
-	 * @param string $app id of the app
398
-	 * @param string $key config key
399
-	 * @param float $default default value
400
-	 * @param bool $lazy search within lazy loaded config
401
-	 *
402
-	 * @return float stored config value or $default if not set in database
403
-	 * @throws InvalidArgumentException if one of the argument format is invalid
404
-	 * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
405
-	 * @since 29.0.0
406
-	 * @see IAppConfig for explanation about lazy loading
407
-	 */
408
-	public function getValueFloat(string $app, string $key, float $default = 0, bool $lazy = false): float {
409
-		return (float)$this->getTypedValue($app, $key, (string)$default, $lazy, self::VALUE_FLOAT);
410
-	}
411
-
412
-	/**
413
-	 * @inheritDoc
414
-	 *
415
-	 * @param string $app id of the app
416
-	 * @param string $key config key
417
-	 * @param bool $default default value
418
-	 * @param bool $lazy search within lazy loaded config
419
-	 *
420
-	 * @return bool stored config value or $default if not set in database
421
-	 * @throws InvalidArgumentException if one of the argument format is invalid
422
-	 * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
423
-	 * @since 29.0.0
424
-	 * @see IAppConfig for explanation about lazy loading
425
-	 */
426
-	public function getValueBool(string $app, string $key, bool $default = false, bool $lazy = false): bool {
427
-		$b = strtolower($this->getTypedValue($app, $key, $default ? 'true' : 'false', $lazy, self::VALUE_BOOL));
428
-		return in_array($b, ['1', 'true', 'yes', 'on']);
429
-	}
430
-
431
-	/**
432
-	 * @inheritDoc
433
-	 *
434
-	 * @param string $app id of the app
435
-	 * @param string $key config key
436
-	 * @param array $default default value
437
-	 * @param bool $lazy search within lazy loaded config
438
-	 *
439
-	 * @return array stored config value or $default if not set in database
440
-	 * @throws InvalidArgumentException if one of the argument format is invalid
441
-	 * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
442
-	 * @since 29.0.0
443
-	 * @see IAppConfig for explanation about lazy loading
444
-	 */
445
-	public function getValueArray(
446
-		string $app,
447
-		string $key,
448
-		array $default = [],
449
-		bool $lazy = false,
450
-	): array {
451
-		try {
452
-			$defaultJson = json_encode($default, JSON_THROW_ON_ERROR);
453
-			$value = json_decode($this->getTypedValue($app, $key, $defaultJson, $lazy, self::VALUE_ARRAY), true, flags: JSON_THROW_ON_ERROR);
454
-
455
-			return is_array($value) ? $value : [];
456
-		} catch (JsonException) {
457
-			return [];
458
-		}
459
-	}
460
-
461
-	/**
462
-	 * @param string $app id of the app
463
-	 * @param string $key config key
464
-	 * @param string $default default value
465
-	 * @param bool $lazy search within lazy loaded config
466
-	 * @param int $type value type {@see VALUE_STRING} {@see VALUE_INT}{@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
467
-	 *
468
-	 * @return string
469
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
470
-	 * @throws InvalidArgumentException
471
-	 */
472
-	private function getTypedValue(
473
-		string $app,
474
-		string $key,
475
-		string $default,
476
-		bool $lazy,
477
-		int $type,
478
-	): string {
479
-		$this->assertParams($app, $key, valueType: $type);
480
-		$origKey = $key;
481
-		$matched = $this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $default);
482
-		if ($default === null) {
483
-			// there is no logical reason for it to be null
484
-			throw new \Exception('default cannot be null');
485
-		}
486
-
487
-		// returns default if strictness of lexicon is set to WARNING (block and report)
488
-		if (!$matched) {
489
-			return $default;
490
-		}
491
-
492
-		$this->loadConfig($app, $lazy ?? true);
493
-
494
-		/**
495
-		 * We ignore check if mixed type is requested.
496
-		 * If type of stored value is set as mixed, we don't filter.
497
-		 * If type of stored value is defined, we compare with the one requested.
498
-		 */
499
-		$knownType = $this->valueTypes[$app][$key] ?? 0;
500
-		if (!$this->isTyped(self::VALUE_MIXED, $type)
501
-			&& $knownType > 0
502
-			&& !$this->isTyped(self::VALUE_MIXED, $knownType)
503
-			&& !$this->isTyped($type, $knownType)) {
504
-			$this->logger->warning('conflict with value type from database', ['app' => $app, 'key' => $key, 'type' => $type, 'knownType' => $knownType]);
505
-			throw new AppConfigTypeConflictException('conflict with value type from database');
506
-		}
507
-
508
-		/**
509
-		 * - the pair $app/$key cannot exist in both array,
510
-		 * - we should still return an existing non-lazy value even if current method
511
-		 *   is called with $lazy is true
512
-		 *
513
-		 * This way, lazyCache will be empty until the load for lazy config value is requested.
514
-		 */
515
-		if (isset($this->lazyCache[$app][$key])) {
516
-			$value = $this->lazyCache[$app][$key];
517
-		} elseif (isset($this->fastCache[$app][$key])) {
518
-			$value = $this->fastCache[$app][$key];
519
-		} else {
520
-			return $default;
521
-		}
522
-
523
-		$sensitive = $this->isTyped(self::VALUE_SENSITIVE, $knownType);
524
-		if ($sensitive && str_starts_with($value, self::ENCRYPTION_PREFIX)) {
525
-			// Only decrypt values that are stored encrypted
526
-			$value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
527
-		}
528
-
529
-		// in case the key was modified while running matchAndApplyLexiconDefinition() we are
530
-		// interested to check options in case a modification of the value is needed
531
-		// ie inverting value from previous key when using lexicon option RENAME_INVERT_BOOLEAN
532
-		if ($origKey !== $key && $type === self::VALUE_BOOL) {
533
-			$value = ($this->configManager->convertToBool($value, $this->getLexiconEntry($app, $key))) ? '1' : '0';
534
-		}
535
-
536
-		return $value;
537
-	}
538
-
539
-	/**
540
-	 * @inheritDoc
541
-	 *
542
-	 * @param string $app id of the app
543
-	 * @param string $key config key
544
-	 *
545
-	 * @return int type of the value
546
-	 * @throws AppConfigUnknownKeyException if config key is not known
547
-	 * @since 29.0.0
548
-	 * @see VALUE_STRING
549
-	 * @see VALUE_INT
550
-	 * @see VALUE_FLOAT
551
-	 * @see VALUE_BOOL
552
-	 * @see VALUE_ARRAY
553
-	 */
554
-	public function getValueType(string $app, string $key, ?bool $lazy = null): int {
555
-		$type = self::VALUE_MIXED;
556
-		$ignorable = $lazy ?? false;
557
-		$this->matchAndApplyLexiconDefinition($app, $key, $ignorable, $type);
558
-		if ($type !== self::VALUE_MIXED) {
559
-			// a modified $type means config key is set in Lexicon
560
-			return $type;
561
-		}
562
-
563
-		$this->assertParams($app, $key);
564
-		$this->loadConfig($app, $lazy ?? true);
565
-
566
-		if (!isset($this->valueTypes[$app][$key])) {
567
-			throw new AppConfigUnknownKeyException('unknown config key');
568
-		}
569
-
570
-		$type = $this->valueTypes[$app][$key];
571
-		$type &= ~self::VALUE_SENSITIVE;
572
-		return $type;
573
-	}
574
-
575
-
576
-	/**
577
-	 * Store a config key and its value in database as VALUE_MIXED
578
-	 *
579
-	 * **WARNING:** Method is internal and **MUST** not be used as it is best to set a real value type
580
-	 *
581
-	 * @param string $app id of the app
582
-	 * @param string $key config key
583
-	 * @param string $value config value
584
-	 * @param bool $lazy set config as lazy loaded
585
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
586
-	 *
587
-	 * @return bool TRUE if value was different, therefor updated in database
588
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED
589
-	 * @internal
590
-	 * @since 29.0.0
591
-	 * @see IAppConfig for explanation about lazy loading
592
-	 * @see setValueString()
593
-	 * @see setValueInt()
594
-	 * @see setValueFloat()
595
-	 * @see setValueBool()
596
-	 * @see setValueArray()
597
-	 */
598
-	public function setValueMixed(
599
-		string $app,
600
-		string $key,
601
-		string $value,
602
-		bool $lazy = false,
603
-		bool $sensitive = false,
604
-	): bool {
605
-		return $this->setTypedValue(
606
-			$app,
607
-			$key,
608
-			$value,
609
-			$lazy,
610
-			self::VALUE_MIXED | ($sensitive ? self::VALUE_SENSITIVE : 0)
611
-		);
612
-	}
613
-
614
-
615
-	/**
616
-	 * @inheritDoc
617
-	 *
618
-	 * @param string $app id of the app
619
-	 * @param string $key config key
620
-	 * @param string $value config value
621
-	 * @param bool $lazy set config as lazy loaded
622
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
623
-	 *
624
-	 * @return bool TRUE if value was different, therefor updated in database
625
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
626
-	 * @since 29.0.0
627
-	 * @see IAppConfig for explanation about lazy loading
628
-	 */
629
-	public function setValueString(
630
-		string $app,
631
-		string $key,
632
-		string $value,
633
-		bool $lazy = false,
634
-		bool $sensitive = false,
635
-	): bool {
636
-		return $this->setTypedValue(
637
-			$app,
638
-			$key,
639
-			$value,
640
-			$lazy,
641
-			self::VALUE_STRING | ($sensitive ? self::VALUE_SENSITIVE : 0)
642
-		);
643
-	}
644
-
645
-	/**
646
-	 * @inheritDoc
647
-	 *
648
-	 * @param string $app id of the app
649
-	 * @param string $key config key
650
-	 * @param int $value config value
651
-	 * @param bool $lazy set config as lazy loaded
652
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
653
-	 *
654
-	 * @return bool TRUE if value was different, therefor updated in database
655
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
656
-	 * @since 29.0.0
657
-	 * @see IAppConfig for explanation about lazy loading
658
-	 */
659
-	public function setValueInt(
660
-		string $app,
661
-		string $key,
662
-		int $value,
663
-		bool $lazy = false,
664
-		bool $sensitive = false,
665
-	): bool {
666
-		if ($value > 2000000000) {
667
-			$this->logger->debug('You are trying to store an integer value around/above 2,147,483,647. This is a reminder that reaching this theoretical limit on 32 bits system will throw an exception.');
668
-		}
669
-
670
-		return $this->setTypedValue(
671
-			$app,
672
-			$key,
673
-			(string)$value,
674
-			$lazy,
675
-			self::VALUE_INT | ($sensitive ? self::VALUE_SENSITIVE : 0)
676
-		);
677
-	}
678
-
679
-	/**
680
-	 * @inheritDoc
681
-	 *
682
-	 * @param string $app id of the app
683
-	 * @param string $key config key
684
-	 * @param float $value config value
685
-	 * @param bool $lazy set config as lazy loaded
686
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
687
-	 *
688
-	 * @return bool TRUE if value was different, therefor updated in database
689
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
690
-	 * @since 29.0.0
691
-	 * @see IAppConfig for explanation about lazy loading
692
-	 */
693
-	public function setValueFloat(
694
-		string $app,
695
-		string $key,
696
-		float $value,
697
-		bool $lazy = false,
698
-		bool $sensitive = false,
699
-	): bool {
700
-		return $this->setTypedValue(
701
-			$app,
702
-			$key,
703
-			(string)$value,
704
-			$lazy,
705
-			self::VALUE_FLOAT | ($sensitive ? self::VALUE_SENSITIVE : 0)
706
-		);
707
-	}
708
-
709
-	/**
710
-	 * @inheritDoc
711
-	 *
712
-	 * @param string $app id of the app
713
-	 * @param string $key config key
714
-	 * @param bool $value config value
715
-	 * @param bool $lazy set config as lazy loaded
716
-	 *
717
-	 * @return bool TRUE if value was different, therefor updated in database
718
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
719
-	 * @since 29.0.0
720
-	 * @see IAppConfig for explanation about lazy loading
721
-	 */
722
-	public function setValueBool(
723
-		string $app,
724
-		string $key,
725
-		bool $value,
726
-		bool $lazy = false,
727
-	): bool {
728
-		return $this->setTypedValue(
729
-			$app,
730
-			$key,
731
-			($value) ? '1' : '0',
732
-			$lazy,
733
-			self::VALUE_BOOL
734
-		);
735
-	}
736
-
737
-	/**
738
-	 * @inheritDoc
739
-	 *
740
-	 * @param string $app id of the app
741
-	 * @param string $key config key
742
-	 * @param array $value config value
743
-	 * @param bool $lazy set config as lazy loaded
744
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
745
-	 *
746
-	 * @return bool TRUE if value was different, therefor updated in database
747
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
748
-	 * @throws JsonException
749
-	 * @since 29.0.0
750
-	 * @see IAppConfig for explanation about lazy loading
751
-	 */
752
-	public function setValueArray(
753
-		string $app,
754
-		string $key,
755
-		array $value,
756
-		bool $lazy = false,
757
-		bool $sensitive = false,
758
-	): bool {
759
-		try {
760
-			return $this->setTypedValue(
761
-				$app,
762
-				$key,
763
-				json_encode($value, JSON_THROW_ON_ERROR),
764
-				$lazy,
765
-				self::VALUE_ARRAY | ($sensitive ? self::VALUE_SENSITIVE : 0)
766
-			);
767
-		} catch (JsonException $e) {
768
-			$this->logger->warning('could not setValueArray', ['app' => $app, 'key' => $key, 'exception' => $e]);
769
-			throw $e;
770
-		}
771
-	}
772
-
773
-	/**
774
-	 * Store a config key and its value in database
775
-	 *
776
-	 * If config key is already known with the exact same config value and same sensitive/lazy status, the
777
-	 * database is not updated. If config value was previously stored as sensitive, status will not be
778
-	 * altered.
779
-	 *
780
-	 * @param string $app id of the app
781
-	 * @param string $key config key
782
-	 * @param string $value config value
783
-	 * @param bool $lazy config set as lazy loaded
784
-	 * @param int $type value type {@see VALUE_STRING} {@see VALUE_INT} {@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
785
-	 *
786
-	 * @return bool TRUE if value was updated in database
787
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
788
-	 * @see IAppConfig for explanation about lazy loading
789
-	 */
790
-	private function setTypedValue(
791
-		string $app,
792
-		string $key,
793
-		string $value,
794
-		bool $lazy,
795
-		int $type,
796
-	): bool {
797
-		$this->assertParams($app, $key);
798
-		if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type)) {
799
-			return false; // returns false as database is not updated
800
-		}
801
-		$this->loadConfig(null, $lazy ?? true);
802
-
803
-		$sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type);
804
-		$inserted = $refreshCache = false;
805
-
806
-		$origValue = $value;
807
-		if ($sensitive || ($this->hasKey($app, $key, $lazy) && $this->isSensitive($app, $key, $lazy))) {
808
-			$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
809
-		}
810
-
811
-		if ($this->hasKey($app, $key, $lazy)) {
812
-			/**
813
-			 * no update if key is already known with set lazy status and value is
814
-			 * not different, unless sensitivity is switched from false to true.
815
-			 */
816
-			if ($origValue === $this->getTypedValue($app, $key, $value, $lazy ?? true, $type)
817
-				&& (!$sensitive || $this->isSensitive($app, $key, $lazy))) {
818
-				return false;
819
-			}
820
-		} else {
821
-			/**
822
-			 * if key is not known yet, we try to insert.
823
-			 * It might fail if the key exists with a different lazy flag.
824
-			 */
825
-			try {
826
-				$insert = $this->connection->getQueryBuilder();
827
-				$insert->insert('appconfig')
828
-					->setValue('appid', $insert->createNamedParameter($app))
829
-					->setValue('lazy', $insert->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
830
-					->setValue('type', $insert->createNamedParameter($type, IQueryBuilder::PARAM_INT))
831
-					->setValue('configkey', $insert->createNamedParameter($key))
832
-					->setValue('configvalue', $insert->createNamedParameter($value));
833
-				$insert->executeStatement();
834
-				$inserted = true;
835
-			} catch (DBException $e) {
836
-				if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
837
-					throw $e; // TODO: throw exception or just log and returns false !?
838
-				}
839
-			}
840
-		}
841
-
842
-		/**
843
-		 * We cannot insert a new row, meaning we need to update an already existing one
844
-		 */
845
-		if (!$inserted) {
846
-			$currType = $this->valueTypes[$app][$key] ?? 0;
847
-			if ($currType === 0) { // this might happen when switching lazy loading status
848
-				$this->loadConfig(lazy: true);
849
-				$currType = $this->valueTypes[$app][$key] ?? 0;
850
-			}
851
-
852
-			/**
853
-			 * This should only happen during the upgrade process from 28 to 29.
854
-			 * We only log a warning and set it to VALUE_MIXED.
855
-			 */
856
-			if ($currType === 0) {
857
-				$this->logger->warning('Value type is set to zero (0) in database. This is fine only during the upgrade process from 28 to 29.', ['app' => $app, 'key' => $key]);
858
-				$currType = self::VALUE_MIXED;
859
-			}
860
-
861
-			/**
862
-			 * we only accept a different type from the one stored in database
863
-			 * if the one stored in database is not-defined (VALUE_MIXED)
864
-			 */
865
-			if (!$this->isTyped(self::VALUE_MIXED, $currType)
866
-				&& ($type | self::VALUE_SENSITIVE) !== ($currType | self::VALUE_SENSITIVE)) {
867
-				try {
868
-					$currType = $this->convertTypeToString($currType);
869
-					$type = $this->convertTypeToString($type);
870
-				} catch (AppConfigIncorrectTypeException) {
871
-					// can be ignored, this was just needed for a better exception message.
872
-				}
873
-				throw new AppConfigTypeConflictException('conflict between new type (' . $type . ') and old type (' . $currType . ')');
874
-			}
875
-
876
-			// we fix $type if the stored value, or the new value as it might be changed, is set as sensitive
877
-			if ($sensitive || $this->isTyped(self::VALUE_SENSITIVE, $currType)) {
878
-				$type |= self::VALUE_SENSITIVE;
879
-			}
880
-
881
-			try {
882
-				if ($lazy !== $this->isLazy($app, $key)) {
883
-					$refreshCache = true;
884
-				}
885
-			} catch (AppConfigUnknownKeyException) {
886
-				// pass
887
-			}
888
-
889
-			$update = $this->connection->getQueryBuilder();
890
-			$update->update('appconfig')
891
-				->set('configvalue', $update->createNamedParameter($value))
892
-				->set('lazy', $update->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
893
-				->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
894
-				->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
895
-				->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
896
-
897
-			$update->executeStatement();
898
-		}
899
-
900
-		if ($refreshCache) {
901
-			$this->clearCache();
902
-			return true;
903
-		}
904
-
905
-		// update local cache
906
-		if ($lazy) {
907
-			$this->lazyCache[$app][$key] = $value;
908
-		} else {
909
-			$this->fastCache[$app][$key] = $value;
910
-		}
911
-		$this->valueTypes[$app][$key] = $type;
912
-		$this->clearLocalCache();
913
-
914
-		return true;
915
-	}
916
-
917
-	/**
918
-	 * Change the type of config value.
919
-	 *
920
-	 * **WARNING:** Method is internal and **MUST** not be used as it may break things.
921
-	 *
922
-	 * @param string $app id of the app
923
-	 * @param string $key config key
924
-	 * @param int $type value type {@see VALUE_STRING} {@see VALUE_INT} {@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
925
-	 *
926
-	 * @return bool TRUE if database update were necessary
927
-	 * @throws AppConfigUnknownKeyException if $key is now known in database
928
-	 * @throws AppConfigIncorrectTypeException if $type is not valid
929
-	 * @internal
930
-	 * @since 29.0.0
931
-	 */
932
-	public function updateType(string $app, string $key, int $type = self::VALUE_MIXED): bool {
933
-		$this->assertParams($app, $key);
934
-		$this->loadConfig(lazy: true);
935
-		$this->matchAndApplyLexiconDefinition($app, $key);
936
-		$this->isLazy($app, $key); // confirm key exists
937
-
938
-		// type can only be one type
939
-		if (!in_array($type, [self::VALUE_MIXED, self::VALUE_STRING, self::VALUE_INT, self::VALUE_FLOAT, self::VALUE_BOOL, self::VALUE_ARRAY])) {
940
-			throw new AppConfigIncorrectTypeException('Unknown value type');
941
-		}
942
-
943
-		$currType = $this->valueTypes[$app][$key];
944
-		if (($type | self::VALUE_SENSITIVE) === ($currType | self::VALUE_SENSITIVE)) {
945
-			return false;
946
-		}
947
-
948
-		// we complete with sensitive flag if the stored value is set as sensitive
949
-		if ($this->isTyped(self::VALUE_SENSITIVE, $currType)) {
950
-			$type = $type | self::VALUE_SENSITIVE;
951
-		}
952
-
953
-		$update = $this->connection->getQueryBuilder();
954
-		$update->update('appconfig')
955
-			->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
956
-			->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
957
-			->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
958
-		$update->executeStatement();
959
-		$this->valueTypes[$app][$key] = $type;
960
-
961
-		return true;
962
-	}
963
-
964
-
965
-	/**
966
-	 * @inheritDoc
967
-	 *
968
-	 * @param string $app id of the app
969
-	 * @param string $key config key
970
-	 * @param bool $sensitive TRUE to set as sensitive, FALSE to unset
971
-	 *
972
-	 * @return bool TRUE if entry was found in database and an update was necessary
973
-	 * @since 29.0.0
974
-	 */
975
-	public function updateSensitive(string $app, string $key, bool $sensitive): bool {
976
-		$this->assertParams($app, $key);
977
-		$this->loadConfig(lazy: true);
978
-		$this->matchAndApplyLexiconDefinition($app, $key);
979
-
980
-		try {
981
-			if ($sensitive === $this->isSensitive($app, $key, null)) {
982
-				return false;
983
-			}
984
-		} catch (AppConfigUnknownKeyException $e) {
985
-			return false;
986
-		}
987
-
988
-		$lazy = $this->isLazy($app, $key);
989
-		if ($lazy) {
990
-			$cache = $this->lazyCache;
991
-		} else {
992
-			$cache = $this->fastCache;
993
-		}
994
-
995
-		if (!isset($cache[$app][$key])) {
996
-			throw new AppConfigUnknownKeyException('unknown config key');
997
-		}
998
-
999
-		/**
1000
-		 * type returned by getValueType() is already cleaned from sensitive flag
1001
-		 * we just need to update it based on $sensitive and store it in database
1002
-		 */
1003
-		$type = $this->getValueType($app, $key);
1004
-		$value = $cache[$app][$key];
1005
-		if ($sensitive) {
1006
-			$type |= self::VALUE_SENSITIVE;
1007
-			$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
1008
-		} else {
1009
-			$value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
1010
-		}
1011
-
1012
-		$update = $this->connection->getQueryBuilder();
1013
-		$update->update('appconfig')
1014
-			->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
1015
-			->set('configvalue', $update->createNamedParameter($value))
1016
-			->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
1017
-			->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1018
-		$update->executeStatement();
1019
-
1020
-		$this->valueTypes[$app][$key] = $type;
1021
-
1022
-		return true;
1023
-	}
1024
-
1025
-	/**
1026
-	 * @inheritDoc
1027
-	 *
1028
-	 * @param string $app id of the app
1029
-	 * @param string $key config key
1030
-	 * @param bool $lazy TRUE to set as lazy loaded, FALSE to unset
1031
-	 *
1032
-	 * @return bool TRUE if entry was found in database and an update was necessary
1033
-	 * @since 29.0.0
1034
-	 */
1035
-	public function updateLazy(string $app, string $key, bool $lazy): bool {
1036
-		$this->assertParams($app, $key);
1037
-		$this->loadConfig(lazy: true);
1038
-		$this->matchAndApplyLexiconDefinition($app, $key);
1039
-
1040
-		try {
1041
-			if ($lazy === $this->isLazy($app, $key)) {
1042
-				return false;
1043
-			}
1044
-		} catch (AppConfigUnknownKeyException $e) {
1045
-			return false;
1046
-		}
1047
-
1048
-		$update = $this->connection->getQueryBuilder();
1049
-		$update->update('appconfig')
1050
-			->set('lazy', $update->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))
1051
-			->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
1052
-			->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1053
-		$update->executeStatement();
1054
-
1055
-		// At this point, it is a lot safer to clean cache
1056
-		$this->clearCache();
1057
-
1058
-		return true;
1059
-	}
1060
-
1061
-	/**
1062
-	 * @inheritDoc
1063
-	 *
1064
-	 * @param string $app id of the app
1065
-	 * @param string $key config key
1066
-	 *
1067
-	 * @return array
1068
-	 * @throws AppConfigUnknownKeyException if config key is not known in database
1069
-	 * @since 29.0.0
1070
-	 */
1071
-	public function getDetails(string $app, string $key): array {
1072
-		$this->assertParams($app, $key);
1073
-		$this->loadConfig(lazy: true);
1074
-		$this->matchAndApplyLexiconDefinition($app, $key);
1075
-		$lazy = $this->isLazy($app, $key);
1076
-
1077
-		if ($lazy) {
1078
-			$cache = $this->lazyCache;
1079
-		} else {
1080
-			$cache = $this->fastCache;
1081
-		}
1082
-
1083
-		$type = $this->getValueType($app, $key);
1084
-		try {
1085
-			$typeString = $this->convertTypeToString($type);
1086
-		} catch (AppConfigIncorrectTypeException $e) {
1087
-			$this->logger->warning('type stored in database is not correct', ['exception' => $e, 'type' => $type]);
1088
-			$typeString = (string)$type;
1089
-		}
1090
-
1091
-		if (!isset($cache[$app][$key])) {
1092
-			throw new AppConfigUnknownKeyException('unknown config key');
1093
-		}
1094
-
1095
-		$value = $cache[$app][$key];
1096
-		$sensitive = $this->isSensitive($app, $key, null);
1097
-		if ($sensitive && str_starts_with($value, self::ENCRYPTION_PREFIX)) {
1098
-			$value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
1099
-		}
1100
-
1101
-		return [
1102
-			'app' => $app,
1103
-			'key' => $key,
1104
-			'value' => $value,
1105
-			'type' => $type,
1106
-			'lazy' => $lazy,
1107
-			'typeString' => $typeString,
1108
-			'sensitive' => $sensitive
1109
-		];
1110
-	}
1111
-
1112
-	/**
1113
-	 * @inheritDoc
1114
-	 *
1115
-	 * @param string $app id of the app
1116
-	 * @param string $key config key
1117
-	 *
1118
-	 * @return array{app: string, key: string, lazy?: bool, valueType?: ValueType, valueTypeName?: string, sensitive?: bool, default?: string, definition?: string, note?: string}
1119
-	 * @since 32.0.0
1120
-	 */
1121
-	public function getKeyDetails(string $app, string $key): array {
1122
-		$this->assertParams($app, $key);
1123
-		try {
1124
-			$details = $this->getDetails($app, $key);
1125
-		} catch (AppConfigUnknownKeyException) {
1126
-			$details = [
1127
-				'app' => $app,
1128
-				'key' => $key
1129
-			];
1130
-		}
1131
-
1132
-		/** @var Entry $lexiconEntry */
1133
-		try {
1134
-			$lazy = false;
1135
-			$this->matchAndApplyLexiconDefinition($app, $key, $lazy, lexiconEntry: $lexiconEntry);
1136
-		} catch (AppConfigTypeConflictException|AppConfigUnknownKeyException) {
1137
-			// can be ignored
1138
-		}
1139
-
1140
-		if ($lexiconEntry !== null) {
1141
-			$details = array_merge($details, [
1142
-				'lazy' => $lexiconEntry->isLazy(),
1143
-				'valueType' => $lexiconEntry->getValueType(),
1144
-				'valueTypeName' => $lexiconEntry->getValueType()->name,
1145
-				'sensitive' => $lexiconEntry->isFlagged(self::FLAG_SENSITIVE),
1146
-				'default' => $lexiconEntry->getDefault($this->presetManager->getLexiconPreset()),
1147
-				'definition' => $lexiconEntry->getDefinition(),
1148
-				'note' => $lexiconEntry->getNote(),
1149
-			]);
1150
-		}
1151
-
1152
-		return array_filter($details, static fn ($v): bool => ($v !== null));
1153
-	}
1154
-
1155
-	/**
1156
-	 * @param string $type
1157
-	 *
1158
-	 * @return int
1159
-	 * @throws AppConfigIncorrectTypeException
1160
-	 * @since 29.0.0
1161
-	 */
1162
-	public function convertTypeToInt(string $type): int {
1163
-		return match (strtolower($type)) {
1164
-			'mixed' => IAppConfig::VALUE_MIXED,
1165
-			'string' => IAppConfig::VALUE_STRING,
1166
-			'integer' => IAppConfig::VALUE_INT,
1167
-			'float' => IAppConfig::VALUE_FLOAT,
1168
-			'boolean' => IAppConfig::VALUE_BOOL,
1169
-			'array' => IAppConfig::VALUE_ARRAY,
1170
-			default => throw new AppConfigIncorrectTypeException('Unknown type ' . $type)
1171
-		};
1172
-	}
1173
-
1174
-	/**
1175
-	 * @param int $type
1176
-	 *
1177
-	 * @return string
1178
-	 * @throws AppConfigIncorrectTypeException
1179
-	 * @since 29.0.0
1180
-	 */
1181
-	public function convertTypeToString(int $type): string {
1182
-		$type &= ~self::VALUE_SENSITIVE;
1183
-
1184
-		return match ($type) {
1185
-			IAppConfig::VALUE_MIXED => 'mixed',
1186
-			IAppConfig::VALUE_STRING => 'string',
1187
-			IAppConfig::VALUE_INT => 'integer',
1188
-			IAppConfig::VALUE_FLOAT => 'float',
1189
-			IAppConfig::VALUE_BOOL => 'boolean',
1190
-			IAppConfig::VALUE_ARRAY => 'array',
1191
-			default => throw new AppConfigIncorrectTypeException('Unknown numeric type ' . $type)
1192
-		};
1193
-	}
1194
-
1195
-	/**
1196
-	 * @inheritDoc
1197
-	 *
1198
-	 * @param string $app id of the app
1199
-	 * @param string $key config key
1200
-	 *
1201
-	 * @since 29.0.0
1202
-	 */
1203
-	public function deleteKey(string $app, string $key): void {
1204
-		$this->assertParams($app, $key);
1205
-		$this->matchAndApplyLexiconDefinition($app, $key);
1206
-
1207
-		$qb = $this->connection->getQueryBuilder();
1208
-		$qb->delete('appconfig')
1209
-			->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
1210
-			->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
1211
-		$qb->executeStatement();
1212
-
1213
-		unset($this->lazyCache[$app][$key]);
1214
-		unset($this->fastCache[$app][$key]);
1215
-		unset($this->valueTypes[$app][$key]);
1216
-		$this->clearLocalCache();
1217
-	}
1218
-
1219
-	/**
1220
-	 * @inheritDoc
1221
-	 *
1222
-	 * @param string $app id of the app
1223
-	 *
1224
-	 * @since 29.0.0
1225
-	 */
1226
-	public function deleteApp(string $app): void {
1227
-		$this->assertParams($app);
1228
-		$qb = $this->connection->getQueryBuilder();
1229
-		$qb->delete('appconfig')
1230
-			->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
1231
-		$qb->executeStatement();
1232
-
1233
-		$this->clearCache();
1234
-	}
1235
-
1236
-	/**
1237
-	 * @inheritDoc
1238
-	 *
1239
-	 * @param bool $reload set to TRUE to refill cache instantly after clearing it
1240
-	 *
1241
-	 * @internal
1242
-	 * @since 29.0.0
1243
-	 */
1244
-	public function clearCache(bool $reload = false): void {
1245
-		$this->lazyLoaded = $this->fastLoaded = false;
1246
-		$this->lazyCache = $this->fastCache = $this->valueTypes = $this->configLexiconDetails = [];
1247
-		$this->localCache?->remove(self::LOCAL_CACHE_KEY);
1248
-
1249
-		if (!$reload) {
1250
-			return;
1251
-		}
1252
-
1253
-		$this->loadConfig(lazy: true);
1254
-	}
1255
-
1256
-
1257
-	/**
1258
-	 * For debug purpose.
1259
-	 * Returns the cached data.
1260
-	 *
1261
-	 * @return array
1262
-	 * @since 29.0.0
1263
-	 * @internal
1264
-	 */
1265
-	public function statusCache(): array {
1266
-		return [
1267
-			'fastLoaded' => $this->fastLoaded,
1268
-			'fastCache' => $this->fastCache,
1269
-			'lazyLoaded' => $this->lazyLoaded,
1270
-			'lazyCache' => $this->lazyCache,
1271
-		];
1272
-	}
1273
-
1274
-	/**
1275
-	 * @param int $needle bitflag to search
1276
-	 * @param int $type known value
1277
-	 *
1278
-	 * @return bool TRUE if bitflag $needle is set in $type
1279
-	 */
1280
-	private function isTyped(int $needle, int $type): bool {
1281
-		return (($needle & $type) !== 0);
1282
-	}
1283
-
1284
-	/**
1285
-	 * Confirm the string set for app and key fit the database description
1286
-	 *
1287
-	 * @param string $app assert $app fit in database
1288
-	 * @param string $configKey assert config key fit in database
1289
-	 * @param bool $allowEmptyApp $app can be empty string
1290
-	 * @param int $valueType assert value type is only one type
1291
-	 *
1292
-	 * @throws InvalidArgumentException
1293
-	 */
1294
-	private function assertParams(string $app = '', string $configKey = '', bool $allowEmptyApp = false, int $valueType = -1): void {
1295
-		if (!$allowEmptyApp && $app === '') {
1296
-			throw new InvalidArgumentException('app cannot be an empty string');
1297
-		}
1298
-		if (strlen($app) > self::APP_MAX_LENGTH) {
1299
-			throw new InvalidArgumentException(
1300
-				'Value (' . $app . ') for app is too long (' . self::APP_MAX_LENGTH . ')'
1301
-			);
1302
-		}
1303
-		if (strlen($configKey) > self::KEY_MAX_LENGTH) {
1304
-			throw new InvalidArgumentException('Value (' . $configKey . ') for key is too long (' . self::KEY_MAX_LENGTH . ')');
1305
-		}
1306
-		if ($valueType > -1) {
1307
-			$valueType &= ~self::VALUE_SENSITIVE;
1308
-			if (!in_array($valueType, [self::VALUE_MIXED, self::VALUE_STRING, self::VALUE_INT, self::VALUE_FLOAT, self::VALUE_BOOL, self::VALUE_ARRAY])) {
1309
-				throw new InvalidArgumentException('Unknown value type');
1310
-			}
1311
-		}
1312
-	}
1313
-
1314
-	/**
1315
-	 * Load normal config or config set as lazy loaded
1316
-	 *
1317
-	 * @param bool $lazy set to TRUE to also load config values set as lazy loaded
1318
-	 */
1319
-	private function loadConfig(?string $app = null, bool $lazy = false): void {
1320
-		if ($this->isLoaded($lazy)) {
1321
-			return;
1322
-		}
1323
-
1324
-		// if lazy is null or true, we debug log
1325
-		if ($lazy === true && $app !== null) {
1326
-			$exception = new \RuntimeException('The loading of lazy AppConfig values have been triggered by app "' . $app . '"');
1327
-			$this->logger->debug($exception->getMessage(), ['exception' => $exception, 'app' => $app]);
1328
-		}
1329
-
1330
-		$loadLazyOnly = $lazy && $this->isLoaded();
1331
-
1332
-		/** @var array<mixed> */
1333
-		$cacheContent = $this->localCache?->get(self::LOCAL_CACHE_KEY) ?? [];
1334
-		$includesLazyValues = !empty($cacheContent) && !empty($cacheContent['lazyCache']);
1335
-		if (!empty($cacheContent) && (!$lazy || $includesLazyValues)) {
1336
-			$this->valueTypes = $cacheContent['valueTypes'];
1337
-			$this->fastCache = $cacheContent['fastCache'];
1338
-			$this->fastLoaded = !empty($this->fastCache);
1339
-			if ($includesLazyValues) {
1340
-				$this->lazyCache = $cacheContent['lazyCache'];
1341
-				$this->lazyLoaded = !empty($this->lazyCache);
1342
-			}
1343
-			return;
1344
-		}
1345
-
1346
-		// Otherwise no cache available and we need to fetch from database
1347
-		$qb = $this->connection->getQueryBuilder();
1348
-		$qb->from('appconfig')
1349
-			->select('appid', 'configkey', 'configvalue', 'type');
1350
-
1351
-		if ($lazy === false) {
1352
-			$qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
1353
-		} else {
1354
-			if ($loadLazyOnly) {
1355
-				$qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT)));
1356
-			}
1357
-			$qb->addSelect('lazy');
1358
-		}
1359
-
1360
-		$result = $qb->executeQuery();
1361
-		$rows = $result->fetchAll();
1362
-		foreach ($rows as $row) {
1363
-			// most of the time, 'lazy' is not in the select because its value is already known
1364
-			if ($lazy && ((int)$row['lazy']) === 1) {
1365
-				$this->lazyCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1366
-			} else {
1367
-				$this->fastCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1368
-			}
1369
-			$this->valueTypes[$row['appid']][$row['configkey']] = (int)($row['type'] ?? 0);
1370
-		}
1371
-
1372
-		$result->closeCursor();
1373
-		$this->localCache?->set(
1374
-			self::LOCAL_CACHE_KEY,
1375
-			[
1376
-				'fastCache' => $this->fastCache,
1377
-				'lazyCache' => $this->lazyCache,
1378
-				'valueTypes' => $this->valueTypes,
1379
-			],
1380
-			self::LOCAL_CACHE_TTL,
1381
-		);
1382
-
1383
-		$this->fastLoaded = true;
1384
-		$this->lazyLoaded = $lazy;
1385
-	}
1386
-
1387
-	/**
1388
-	 * @param bool $lazy - If set to true then also check if lazy values are loaded
1389
-	 */
1390
-	private function isLoaded(bool $lazy = false): bool {
1391
-		return $this->fastLoaded && (!$lazy || $this->lazyLoaded);
1392
-	}
1393
-
1394
-	/**
1395
-	 * Gets the config value
1396
-	 *
1397
-	 * @param string $app app
1398
-	 * @param string $key key
1399
-	 * @param string $default - Default value if the key does not exist
1400
-	 *
1401
-	 * @return string the value or $default
1402
-	 * @deprecated 29.0.0 use getValue*()
1403
-	 *
1404
-	 * This function gets a value from the appconfig table. If the key does
1405
-	 * not exist the default value will be returned
1406
-	 */
1407
-	public function getValue($app, $key, $default = '') {
1408
-		$this->loadConfig($app);
1409
-		$this->matchAndApplyLexiconDefinition($app, $key);
1410
-
1411
-		return $this->fastCache[$app][$key] ?? $default;
1412
-	}
1413
-
1414
-	/**
1415
-	 * Sets a value. If the key did not exist before it will be created.
1416
-	 *
1417
-	 * @param string $app app
1418
-	 * @param string $key key
1419
-	 * @param string|float|int $value value
1420
-	 *
1421
-	 * @return bool True if the value was inserted or updated, false if the value was the same
1422
-	 * @throws AppConfigTypeConflictException
1423
-	 * @throws AppConfigUnknownKeyException
1424
-	 * @deprecated 29.0.0
1425
-	 */
1426
-	public function setValue($app, $key, $value) {
1427
-		/**
1428
-		 * TODO: would it be overkill, or decently improve performance, to catch
1429
-		 * call to this method with $key='enabled' and 'hide' config value related
1430
-		 * to $app when the app is disabled (by modifying entry in database: lazy=lazy+2)
1431
-		 * or enabled (lazy=lazy-2)
1432
-		 *
1433
-		 * this solution would remove the loading of config values from disabled app
1434
-		 * unless calling the method.
1435
-		 */
1436
-		return $this->setTypedValue($app, $key, (string)$value, false, self::VALUE_MIXED);
1437
-	}
1438
-
1439
-
1440
-	/**
1441
-	 * get multiple values, either the app or key can be used as wildcard by setting it to false
1442
-	 *
1443
-	 * @param string|false $app
1444
-	 * @param string|false $key
1445
-	 *
1446
-	 * @return array|false
1447
-	 * @deprecated 29.0.0 use {@see getAllValues()}
1448
-	 */
1449
-	public function getValues($app, $key) {
1450
-		if (($app !== false) === ($key !== false)) {
1451
-			return false;
1452
-		}
1453
-
1454
-		$key = ($key === false) ? '' : $key;
1455
-		if (!$app) {
1456
-			return $this->searchValues($key, false, self::VALUE_MIXED);
1457
-		} else {
1458
-			return $this->getAllValues($app, $key);
1459
-		}
1460
-	}
1461
-
1462
-	/**
1463
-	 * get all values of the app or and filters out sensitive data
1464
-	 *
1465
-	 * @param string $app
1466
-	 *
1467
-	 * @return array
1468
-	 * @deprecated 29.0.0 use {@see getAllValues()}
1469
-	 */
1470
-	public function getFilteredValues($app) {
1471
-		return $this->getAllValues($app, filtered: true);
1472
-	}
1473
-
1474
-
1475
-	/**
1476
-	 * **Warning:** avoid default NULL value for $lazy as this will
1477
-	 * load all lazy values from the database
1478
-	 *
1479
-	 * @param string $app
1480
-	 * @param array<string, string> $values ['key' => 'value']
1481
-	 * @param bool|null $lazy
1482
-	 *
1483
-	 * @return array<string, string|int|float|bool|array>
1484
-	 */
1485
-	private function formatAppValues(string $app, array $values, ?bool $lazy = null): array {
1486
-		foreach ($values as $key => $value) {
1487
-			try {
1488
-				$type = $this->getValueType($app, $key, $lazy);
1489
-			} catch (AppConfigUnknownKeyException) {
1490
-				continue;
1491
-			}
1492
-
1493
-			$values[$key] = $this->convertTypedValue($value, $type);
1494
-		}
1495
-
1496
-		return $values;
1497
-	}
1498
-
1499
-	/**
1500
-	 * convert string value to the expected type
1501
-	 *
1502
-	 * @param string $value
1503
-	 * @param int $type
1504
-	 *
1505
-	 * @return string|int|float|bool|array
1506
-	 */
1507
-	private function convertTypedValue(string $value, int $type): string|int|float|bool|array {
1508
-		switch ($type) {
1509
-			case self::VALUE_INT:
1510
-				return (int)$value;
1511
-			case self::VALUE_FLOAT:
1512
-				return (float)$value;
1513
-			case self::VALUE_BOOL:
1514
-				return in_array(strtolower($value), ['1', 'true', 'yes', 'on']);
1515
-			case self::VALUE_ARRAY:
1516
-				try {
1517
-					return json_decode($value, true, flags: JSON_THROW_ON_ERROR);
1518
-				} catch (JsonException $e) {
1519
-					// ignoreable
1520
-				}
1521
-				break;
1522
-		}
1523
-		return $value;
1524
-	}
1525
-
1526
-	/**
1527
-	 * @param string $app
1528
-	 *
1529
-	 * @return string[]
1530
-	 * @deprecated 29.0.0 data sensitivity should be set when calling setValue*()
1531
-	 */
1532
-	private function getSensitiveKeys(string $app): array {
1533
-		$sensitiveValues = [
1534
-			'circles' => [
1535
-				'/^key_pairs$/',
1536
-				'/^local_gskey$/',
1537
-			],
1538
-			'call_summary_bot' => [
1539
-				'/^secret_(.*)$/',
1540
-			],
1541
-			'external' => [
1542
-				'/^sites$/',
1543
-				'/^jwt_token_privkey_(.*)$/',
1544
-			],
1545
-			'globalsiteselector' => [
1546
-				'/^gss\.jwt\.key$/',
1547
-			],
1548
-			'gpgmailer' => [
1549
-				'/^GpgServerKey$/',
1550
-			],
1551
-			'integration_discourse' => [
1552
-				'/^private_key$/',
1553
-				'/^public_key$/',
1554
-			],
1555
-			'integration_dropbox' => [
1556
-				'/^client_id$/',
1557
-				'/^client_secret$/',
1558
-			],
1559
-			'integration_github' => [
1560
-				'/^client_id$/',
1561
-				'/^client_secret$/',
1562
-			],
1563
-			'integration_gitlab' => [
1564
-				'/^client_id$/',
1565
-				'/^client_secret$/',
1566
-				'/^oauth_instance_url$/',
1567
-			],
1568
-			'integration_google' => [
1569
-				'/^client_id$/',
1570
-				'/^client_secret$/',
1571
-			],
1572
-			'integration_jira' => [
1573
-				'/^client_id$/',
1574
-				'/^client_secret$/',
1575
-				'/^forced_instance_url$/',
1576
-			],
1577
-			'integration_onedrive' => [
1578
-				'/^client_id$/',
1579
-				'/^client_secret$/',
1580
-			],
1581
-			'integration_openproject' => [
1582
-				'/^client_id$/',
1583
-				'/^client_secret$/',
1584
-				'/^oauth_instance_url$/',
1585
-			],
1586
-			'integration_reddit' => [
1587
-				'/^client_id$/',
1588
-				'/^client_secret$/',
1589
-			],
1590
-			'integration_suitecrm' => [
1591
-				'/^client_id$/',
1592
-				'/^client_secret$/',
1593
-				'/^oauth_instance_url$/',
1594
-			],
1595
-			'integration_twitter' => [
1596
-				'/^consumer_key$/',
1597
-				'/^consumer_secret$/',
1598
-				'/^followed_user$/',
1599
-			],
1600
-			'integration_zammad' => [
1601
-				'/^client_id$/',
1602
-				'/^client_secret$/',
1603
-				'/^oauth_instance_url$/',
1604
-			],
1605
-			'maps' => [
1606
-				'/^mapboxAPIKEY$/',
1607
-			],
1608
-			'notify_push' => [
1609
-				'/^cookie$/',
1610
-			],
1611
-			'onlyoffice' => [
1612
-				'/^jwt_secret$/',
1613
-			],
1614
-			'passwords' => [
1615
-				'/^SSEv1ServerKey$/',
1616
-			],
1617
-			'serverinfo' => [
1618
-				'/^token$/',
1619
-			],
1620
-			'spreed' => [
1621
-				'/^bridge_bot_password$/',
1622
-				'/^hosted-signaling-server-(.*)$/',
1623
-				'/^recording_servers$/',
1624
-				'/^signaling_servers$/',
1625
-				'/^signaling_ticket_secret$/',
1626
-				'/^signaling_token_privkey_(.*)$/',
1627
-				'/^signaling_token_pubkey_(.*)$/',
1628
-				'/^sip_bridge_dialin_info$/',
1629
-				'/^sip_bridge_shared_secret$/',
1630
-				'/^stun_servers$/',
1631
-				'/^turn_servers$/',
1632
-				'/^turn_server_secret$/',
1633
-			],
1634
-			'support' => [
1635
-				'/^last_response$/',
1636
-				'/^potential_subscription_key$/',
1637
-				'/^subscription_key$/',
1638
-			],
1639
-			'theming' => [
1640
-				'/^imprintUrl$/',
1641
-				'/^privacyUrl$/',
1642
-				'/^slogan$/',
1643
-				'/^url$/',
1644
-			],
1645
-			'twofactor_gateway' => [
1646
-				'/^.*token$/',
1647
-			],
1648
-			'user_ldap' => [
1649
-				'/^(s..)?ldap_agent_password$/',
1650
-			],
1651
-			'user_saml' => [
1652
-				'/^idp-x509cert$/',
1653
-			],
1654
-			'whiteboard' => [
1655
-				'/^jwt_secret_key$/',
1656
-			],
1657
-		];
1658
-
1659
-		return $sensitiveValues[$app] ?? [];
1660
-	}
1661
-
1662
-	/**
1663
-	 * Clear all the cached app config values
1664
-	 * New cache will be generated next time a config value is retrieved
1665
-	 *
1666
-	 * @deprecated 29.0.0 use {@see clearCache()}
1667
-	 */
1668
-	public function clearCachedConfig(): void {
1669
-		$this->clearCache();
1670
-	}
1671
-
1672
-	/**
1673
-	 * Match and apply current use of config values with defined lexicon.
1674
-	 * Set $lazy to NULL only if only interested into checking that $key is alias.
1675
-	 *
1676
-	 * @throws AppConfigUnknownKeyException
1677
-	 * @throws AppConfigTypeConflictException
1678
-	 * @return bool TRUE if everything is fine compared to lexicon or lexicon does not exist
1679
-	 */
1680
-	private function matchAndApplyLexiconDefinition(
1681
-		string $app,
1682
-		string &$key,
1683
-		?bool &$lazy = null,
1684
-		int &$type = self::VALUE_MIXED,
1685
-		?string &$default = null,
1686
-		?Entry &$lexiconEntry = null,
1687
-	): bool {
1688
-		if (in_array($key,
1689
-			[
1690
-				'enabled',
1691
-				'installed_version',
1692
-				'types',
1693
-			])) {
1694
-			return true; // we don't break stuff for this list of config keys.
1695
-		}
1696
-		$configDetails = $this->getConfigDetailsFromLexicon($app);
1697
-		if (array_key_exists($key, $configDetails['aliases']) && !$this->ignoreLexiconAliases) {
1698
-			// in case '$rename' is set in ConfigLexiconEntry, we use the new config key
1699
-			$key = $configDetails['aliases'][$key];
1700
-		}
1701
-
1702
-		if (!array_key_exists($key, $configDetails['entries'])) {
1703
-			return $this->applyLexiconStrictness($configDetails['strictness'], $app . '/' . $key);
1704
-		}
1705
-
1706
-		// if lazy is NULL, we ignore all check on the type/lazyness/default from Lexicon
1707
-		if ($lazy === null) {
1708
-			return true;
1709
-		}
1710
-
1711
-		/** @var Entry $lexiconEntry */
1712
-		$lexiconEntry = $configDetails['entries'][$key];
1713
-		$type &= ~self::VALUE_SENSITIVE;
1714
-
1715
-		$appConfigValueType = $lexiconEntry->getValueType()->toAppConfigFlag();
1716
-		if ($type === self::VALUE_MIXED) {
1717
-			$type = $appConfigValueType; // we overwrite if value was requested as mixed
1718
-		} elseif ($appConfigValueType !== $type) {
1719
-			throw new AppConfigTypeConflictException('The app config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
1720
-		}
1721
-
1722
-		$lazy = $lexiconEntry->isLazy();
1723
-		// only look for default if needed, default from Lexicon got priority
1724
-		if ($default !== null) {
1725
-			$default = $lexiconEntry->getDefault($this->presetManager->getLexiconPreset()) ?? $default;
1726
-		}
1727
-
1728
-		if ($lexiconEntry->isFlagged(self::FLAG_SENSITIVE)) {
1729
-			$type |= self::VALUE_SENSITIVE;
1730
-		}
1731
-		if ($lexiconEntry->isDeprecated()) {
1732
-			$this->logger->notice('App config key ' . $app . '/' . $key . ' is set as deprecated.');
1733
-		}
1734
-
1735
-		return true;
1736
-	}
1737
-
1738
-	/**
1739
-	 * manage ConfigLexicon behavior based on strictness set in IConfigLexicon
1740
-	 *
1741
-	 * @param Strictness|null $strictness
1742
-	 * @param string $line
1743
-	 *
1744
-	 * @return bool TRUE if conflict can be fully ignored, FALSE if action should be not performed
1745
-	 * @throws AppConfigUnknownKeyException if strictness implies exception
1746
-	 * @see \OCP\Config\Lexicon\ILexicon::getStrictness()
1747
-	 */
1748
-	private function applyLexiconStrictness(?Strictness $strictness, string $configAppKey): bool {
1749
-		if ($strictness === null) {
1750
-			return true;
1751
-		}
1752
-
1753
-		$line = 'The app config key ' . $configAppKey . ' is not defined in the config lexicon';
1754
-		switch ($strictness) {
1755
-			case Strictness::IGNORE:
1756
-				return true;
1757
-			case Strictness::NOTICE:
1758
-				if (!in_array($configAppKey, $this->strictnessApplied, true)) {
1759
-					$this->strictnessApplied[] = $configAppKey;
1760
-					$this->logger->notice($line);
1761
-				}
1762
-				return true;
1763
-			case Strictness::WARNING:
1764
-				if (!in_array($configAppKey, $this->strictnessApplied, true)) {
1765
-					$this->strictnessApplied[] = $configAppKey;
1766
-					$this->logger->warning($line);
1767
-				}
1768
-				return false;
1769
-		}
1770
-
1771
-		throw new AppConfigUnknownKeyException($line);
1772
-	}
1773
-
1774
-	/**
1775
-	 * extract details from registered $appId's config lexicon
1776
-	 *
1777
-	 * @param string $appId
1778
-	 * @internal
1779
-	 *
1780
-	 * @return array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}
1781
-	 */
1782
-	public function getConfigDetailsFromLexicon(string $appId): array {
1783
-		if (!array_key_exists($appId, $this->configLexiconDetails)) {
1784
-			$entries = $aliases = [];
1785
-			$bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
1786
-			$configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
1787
-			foreach ($configLexicon?->getAppConfigs() ?? [] as $configEntry) {
1788
-				$entries[$configEntry->getKey()] = $configEntry;
1789
-				$newName = $configEntry->getRename();
1790
-				if ($newName !== null) {
1791
-					$aliases[$newName] = $configEntry->getKey();
1792
-				}
1793
-			}
1794
-
1795
-			$this->configLexiconDetails[$appId] = [
1796
-				'entries' => $entries,
1797
-				'aliases' => $aliases,
1798
-				'strictness' => $configLexicon?->getStrictness() ?? Strictness::IGNORE
1799
-			];
1800
-		}
1801
-
1802
-		return $this->configLexiconDetails[$appId];
1803
-	}
1804
-
1805
-	/**
1806
-	 * get Lexicon Entry using appId and config key entry
1807
-	 *
1808
-	 * @return Entry|null NULL if entry does not exist in app's Lexicon
1809
-	 * @internal
1810
-	 */
1811
-	public function getLexiconEntry(string $appId, string $key): ?Entry {
1812
-		return $this->getConfigDetailsFromLexicon($appId)['entries'][$key] ?? null;
1813
-	}
1814
-
1815
-	/**
1816
-	 * if set to TRUE, ignore aliases defined in Config Lexicon during the use of the methods of this class
1817
-	 *
1818
-	 * @internal
1819
-	 */
1820
-	public function ignoreLexiconAliases(bool $ignore): void {
1821
-		$this->ignoreLexiconAliases = $ignore;
1822
-	}
1823
-
1824
-	/**
1825
-	 * Returns the installed versions of all apps
1826
-	 *
1827
-	 * @return array<string, string>
1828
-	 */
1829
-	public function getAppInstalledVersions(bool $onlyEnabled = false): array {
1830
-		if ($this->appVersionsCache === null) {
1831
-			/** @var array<string, string> */
1832
-			$this->appVersionsCache = $this->searchValues('installed_version', false, IAppConfig::VALUE_STRING);
1833
-		}
1834
-		if ($onlyEnabled) {
1835
-			return array_filter(
1836
-				$this->appVersionsCache,
1837
-				fn (string $app): bool => $this->getValueString($app, 'enabled', 'no') !== 'no',
1838
-				ARRAY_FILTER_USE_KEY
1839
-			);
1840
-		}
1841
-		return $this->appVersionsCache;
1842
-	}
1843
-
1844
-	private function clearLocalCache(): void {
1845
-		$this->localCache?->remove(self::LOCAL_CACHE_KEY);
1846
-	}
54
+    private const APP_MAX_LENGTH = 32;
55
+    private const KEY_MAX_LENGTH = 64;
56
+    private const ENCRYPTION_PREFIX = '$AppConfigEncryption$';
57
+    private const ENCRYPTION_PREFIX_LENGTH = 21; // strlen(self::ENCRYPTION_PREFIX)
58
+    private const LOCAL_CACHE_KEY = 'OC\\AppConfig';
59
+    private const LOCAL_CACHE_TTL = 3;
60
+
61
+    /** @var array<string, array<string, string>> ['app_id' => ['config_key' => 'config_value']] */
62
+    private array $fastCache = [];   // cache for normal config keys
63
+    /** @var array<string, array<string, string>> ['app_id' => ['config_key' => 'config_value']] */
64
+    private array $lazyCache = [];   // cache for lazy config keys
65
+    /** @var array<string, array<string, int>> ['app_id' => ['config_key' => bitflag]] */
66
+    private array $valueTypes = [];  // type for all config values
67
+    private bool $fastLoaded = false;
68
+    private bool $lazyLoaded = false;
69
+    /** @var array<string, array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
70
+    private array $configLexiconDetails = [];
71
+    private bool $ignoreLexiconAliases = false;
72
+    private array $strictnessApplied = [];
73
+
74
+    /** @var ?array<string, string> */
75
+    private ?array $appVersionsCache = null;
76
+    private ?ICache $localCache = null;
77
+
78
+    public function __construct(
79
+        protected IDBConnection $connection,
80
+        protected IConfig $config,
81
+        private readonly ConfigManager $configManager,
82
+        private readonly PresetManager $presetManager,
83
+        protected LoggerInterface $logger,
84
+        protected ICrypto $crypto,
85
+        readonly CacheFactory $cacheFactory,
86
+    ) {
87
+        if ($config->getSystemValueBool('cache_app_config', true) && $cacheFactory->isLocalCacheAvailable()) {
88
+            $cacheFactory->withServerVersionPrefix(function (ICacheFactory $factory) {
89
+                $this->localCache = $factory->createLocal();
90
+            });
91
+        }
92
+    }
93
+
94
+    /**
95
+     * @inheritDoc
96
+     *
97
+     * @return list<string> list of app ids
98
+     * @since 7.0.0
99
+     */
100
+    public function getApps(): array {
101
+        $this->loadConfig(lazy: true);
102
+        $apps = array_merge(array_keys($this->fastCache), array_keys($this->lazyCache));
103
+        sort($apps);
104
+
105
+        return array_values(array_unique($apps));
106
+    }
107
+
108
+    /**
109
+     * @inheritDoc
110
+     *
111
+     * @param string $app id of the app
112
+     * @return list<string> list of stored config keys
113
+     * @see searchKeys to not load lazy config keys
114
+     *
115
+     * @since 29.0.0
116
+     */
117
+    public function getKeys(string $app): array {
118
+        $this->assertParams($app);
119
+        $this->loadConfig($app, true);
120
+        $keys = array_merge(array_keys($this->fastCache[$app] ?? []), array_keys($this->lazyCache[$app] ?? []));
121
+        sort($keys);
122
+
123
+        return array_values(array_unique($keys));
124
+    }
125
+
126
+    /**
127
+     * @inheritDoc
128
+     *
129
+     * @param string $app id of the app
130
+     * @param string $prefix returns only keys starting with this value
131
+     * @param bool $lazy TRUE to search in lazy config keys
132
+     * @return list<string> list of stored config keys
133
+     * @since 32.0.0
134
+     */
135
+    public function searchKeys(string $app, string $prefix = '', bool $lazy = false): array {
136
+        $this->assertParams($app);
137
+        $this->loadConfig($app, $lazy);
138
+        if ($lazy) {
139
+            $keys = array_keys($this->lazyCache[$app] ?? []);
140
+        } else {
141
+            $keys = array_keys($this->fastCache[$app] ?? []);
142
+        }
143
+
144
+        if ($prefix !== '') {
145
+            $keys = array_filter($keys, static fn (string $key): bool => str_starts_with($key, $prefix));
146
+        }
147
+
148
+        sort($keys);
149
+        return array_values(array_unique($keys));
150
+    }
151
+
152
+    /**
153
+     * @inheritDoc
154
+     *
155
+     * @param string $app id of the app
156
+     * @param string $key config key
157
+     * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
158
+     *
159
+     * @return bool TRUE if key exists
160
+     * @since 7.0.0
161
+     * @since 29.0.0 Added the $lazy argument
162
+     */
163
+    public function hasKey(string $app, string $key, ?bool $lazy = false): bool {
164
+        $this->assertParams($app, $key);
165
+        $this->loadConfig($app, $lazy ?? true);
166
+        $this->matchAndApplyLexiconDefinition($app, $key);
167
+
168
+        $hasLazy = isset($this->lazyCache[$app][$key]);
169
+        $hasFast = isset($this->fastCache[$app][$key]);
170
+        if ($lazy === null) {
171
+            return $hasLazy || $hasFast;
172
+        } else {
173
+            return $lazy ? $hasLazy : $hasFast;
174
+        }
175
+    }
176
+
177
+    /**
178
+     * @param string $app id of the app
179
+     * @param string $key config key
180
+     * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
181
+     *
182
+     * @return bool
183
+     * @throws AppConfigUnknownKeyException if config key is not known
184
+     * @since 29.0.0
185
+     */
186
+    public function isSensitive(string $app, string $key, ?bool $lazy = false): bool {
187
+        $this->assertParams($app, $key);
188
+        $this->loadConfig(null, $lazy ?? true);
189
+        $this->matchAndApplyLexiconDefinition($app, $key);
190
+
191
+        if (!isset($this->valueTypes[$app][$key])) {
192
+            throw new AppConfigUnknownKeyException('unknown config key');
193
+        }
194
+
195
+        return $this->isTyped(self::VALUE_SENSITIVE, $this->valueTypes[$app][$key]);
196
+    }
197
+
198
+    /**
199
+     * @inheritDoc
200
+     *
201
+     * @param string $app if of the app
202
+     * @param string $key config key
203
+     *
204
+     * @return bool TRUE if config is lazy loaded
205
+     * @throws AppConfigUnknownKeyException if config key is not known
206
+     * @see IAppConfig for details about lazy loading
207
+     * @since 29.0.0
208
+     */
209
+    public function isLazy(string $app, string $key): bool {
210
+        $this->assertParams($app, $key);
211
+        $this->matchAndApplyLexiconDefinition($app, $key);
212
+
213
+        // there is a huge probability the non-lazy config are already loaded
214
+        if ($this->hasKey($app, $key, false)) {
215
+            return false;
216
+        }
217
+
218
+        // key not found, we search in the lazy config
219
+        if ($this->hasKey($app, $key, true)) {
220
+            return true;
221
+        }
222
+
223
+        throw new AppConfigUnknownKeyException('unknown config key');
224
+    }
225
+
226
+
227
+    /**
228
+     * @inheritDoc
229
+     *
230
+     * @param string $app id of the app
231
+     * @param string $prefix config keys prefix to search
232
+     * @param bool $filtered TRUE to hide sensitive config values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
233
+     *
234
+     * @return array<string, string|int|float|bool|array> [configKey => configValue]
235
+     * @since 29.0.0
236
+     */
237
+    public function getAllValues(string $app, string $prefix = '', bool $filtered = false): array {
238
+        $this->assertParams($app, $prefix);
239
+        // if we want to filter values, we need to get sensitivity
240
+        $this->loadConfig($app, true);
241
+        // array_merge() will remove numeric keys (here config keys), so addition arrays instead
242
+        $values = $this->formatAppValues($app, ($this->fastCache[$app] ?? []) + ($this->lazyCache[$app] ?? []));
243
+        $values = array_filter(
244
+            $values,
245
+            function (string $key) use ($prefix): bool {
246
+                return str_starts_with($key, $prefix); // filter values based on $prefix
247
+            }, ARRAY_FILTER_USE_KEY
248
+        );
249
+
250
+        if (!$filtered) {
251
+            return $values;
252
+        }
253
+
254
+        /**
255
+         * Using the old (deprecated) list of sensitive values.
256
+         */
257
+        foreach ($this->getSensitiveKeys($app) as $sensitiveKeyExp) {
258
+            $sensitiveKeys = preg_grep($sensitiveKeyExp, array_keys($values));
259
+            foreach ($sensitiveKeys as $sensitiveKey) {
260
+                $this->valueTypes[$app][$sensitiveKey] = ($this->valueTypes[$app][$sensitiveKey] ?? 0) | self::VALUE_SENSITIVE;
261
+            }
262
+        }
263
+
264
+        $result = [];
265
+        foreach ($values as $key => $value) {
266
+            $result[$key] = $this->isTyped(self::VALUE_SENSITIVE, $this->valueTypes[$app][$key] ?? 0) ? IConfig::SENSITIVE_VALUE : $value;
267
+        }
268
+
269
+        return $result;
270
+    }
271
+
272
+    /**
273
+     * @inheritDoc
274
+     *
275
+     * @param string $key config key
276
+     * @param bool $lazy search within lazy loaded config
277
+     * @param int|null $typedAs enforce type for the returned values ({@see self::VALUE_STRING} and others)
278
+     *
279
+     * @return array<string, string|int|float|bool|array> [appId => configValue]
280
+     * @since 29.0.0
281
+     */
282
+    public function searchValues(string $key, bool $lazy = false, ?int $typedAs = null): array {
283
+        $this->assertParams('', $key, true);
284
+        $this->loadConfig(null, $lazy);
285
+
286
+        /** @var array<array-key, array<array-key, mixed>> $cache */
287
+        if ($lazy) {
288
+            $cache = $this->lazyCache;
289
+        } else {
290
+            $cache = $this->fastCache;
291
+        }
292
+
293
+        $values = [];
294
+        foreach (array_keys($cache) as $app) {
295
+            if (isset($cache[$app][$key])) {
296
+                $values[$app] = $this->convertTypedValue($cache[$app][$key], $typedAs ?? $this->getValueType((string)$app, $key, $lazy));
297
+            }
298
+        }
299
+
300
+        return $values;
301
+    }
302
+
303
+
304
+    /**
305
+     * Get the config value as string.
306
+     * If the value does not exist the given default will be returned.
307
+     *
308
+     * Set lazy to `null` to ignore it and get the value from either source.
309
+     *
310
+     * **WARNING:** Method is internal and **SHOULD** not be used, as it is better to get the value with a type.
311
+     *
312
+     * @param string $app id of the app
313
+     * @param string $key config key
314
+     * @param string $default config value
315
+     * @param null|bool $lazy get config as lazy loaded or not. can be NULL
316
+     *
317
+     * @return string the value or $default
318
+     * @internal
319
+     * @since 29.0.0
320
+     * @see IAppConfig for explanation about lazy loading
321
+     * @see getValueString()
322
+     * @see getValueInt()
323
+     * @see getValueFloat()
324
+     * @see getValueBool()
325
+     * @see getValueArray()
326
+     */
327
+    public function getValueMixed(
328
+        string $app,
329
+        string $key,
330
+        string $default = '',
331
+        ?bool $lazy = false,
332
+    ): string {
333
+        try {
334
+            $lazy = ($lazy === null) ? $this->isLazy($app, $key) : $lazy;
335
+        } catch (AppConfigUnknownKeyException) {
336
+            return $default;
337
+        }
338
+
339
+        return $this->getTypedValue(
340
+            $app,
341
+            $key,
342
+            $default,
343
+            $lazy,
344
+            self::VALUE_MIXED
345
+        );
346
+    }
347
+
348
+    /**
349
+     * @inheritDoc
350
+     *
351
+     * @param string $app id of the app
352
+     * @param string $key config key
353
+     * @param string $default default value
354
+     * @param bool $lazy search within lazy loaded config
355
+     *
356
+     * @return string stored config value or $default if not set in database
357
+     * @throws InvalidArgumentException if one of the argument format is invalid
358
+     * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
359
+     * @since 29.0.0
360
+     * @see IAppConfig for explanation about lazy loading
361
+     */
362
+    public function getValueString(
363
+        string $app,
364
+        string $key,
365
+        string $default = '',
366
+        bool $lazy = false,
367
+    ): string {
368
+        return $this->getTypedValue($app, $key, $default, $lazy, self::VALUE_STRING);
369
+    }
370
+
371
+    /**
372
+     * @inheritDoc
373
+     *
374
+     * @param string $app id of the app
375
+     * @param string $key config key
376
+     * @param int $default default value
377
+     * @param bool $lazy search within lazy loaded config
378
+     *
379
+     * @return int stored config value or $default if not set in database
380
+     * @throws InvalidArgumentException if one of the argument format is invalid
381
+     * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
382
+     * @since 29.0.0
383
+     * @see IAppConfig for explanation about lazy loading
384
+     */
385
+    public function getValueInt(
386
+        string $app,
387
+        string $key,
388
+        int $default = 0,
389
+        bool $lazy = false,
390
+    ): int {
391
+        return (int)$this->getTypedValue($app, $key, (string)$default, $lazy, self::VALUE_INT);
392
+    }
393
+
394
+    /**
395
+     * @inheritDoc
396
+     *
397
+     * @param string $app id of the app
398
+     * @param string $key config key
399
+     * @param float $default default value
400
+     * @param bool $lazy search within lazy loaded config
401
+     *
402
+     * @return float stored config value or $default if not set in database
403
+     * @throws InvalidArgumentException if one of the argument format is invalid
404
+     * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
405
+     * @since 29.0.0
406
+     * @see IAppConfig for explanation about lazy loading
407
+     */
408
+    public function getValueFloat(string $app, string $key, float $default = 0, bool $lazy = false): float {
409
+        return (float)$this->getTypedValue($app, $key, (string)$default, $lazy, self::VALUE_FLOAT);
410
+    }
411
+
412
+    /**
413
+     * @inheritDoc
414
+     *
415
+     * @param string $app id of the app
416
+     * @param string $key config key
417
+     * @param bool $default default value
418
+     * @param bool $lazy search within lazy loaded config
419
+     *
420
+     * @return bool stored config value or $default if not set in database
421
+     * @throws InvalidArgumentException if one of the argument format is invalid
422
+     * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
423
+     * @since 29.0.0
424
+     * @see IAppConfig for explanation about lazy loading
425
+     */
426
+    public function getValueBool(string $app, string $key, bool $default = false, bool $lazy = false): bool {
427
+        $b = strtolower($this->getTypedValue($app, $key, $default ? 'true' : 'false', $lazy, self::VALUE_BOOL));
428
+        return in_array($b, ['1', 'true', 'yes', 'on']);
429
+    }
430
+
431
+    /**
432
+     * @inheritDoc
433
+     *
434
+     * @param string $app id of the app
435
+     * @param string $key config key
436
+     * @param array $default default value
437
+     * @param bool $lazy search within lazy loaded config
438
+     *
439
+     * @return array stored config value or $default if not set in database
440
+     * @throws InvalidArgumentException if one of the argument format is invalid
441
+     * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
442
+     * @since 29.0.0
443
+     * @see IAppConfig for explanation about lazy loading
444
+     */
445
+    public function getValueArray(
446
+        string $app,
447
+        string $key,
448
+        array $default = [],
449
+        bool $lazy = false,
450
+    ): array {
451
+        try {
452
+            $defaultJson = json_encode($default, JSON_THROW_ON_ERROR);
453
+            $value = json_decode($this->getTypedValue($app, $key, $defaultJson, $lazy, self::VALUE_ARRAY), true, flags: JSON_THROW_ON_ERROR);
454
+
455
+            return is_array($value) ? $value : [];
456
+        } catch (JsonException) {
457
+            return [];
458
+        }
459
+    }
460
+
461
+    /**
462
+     * @param string $app id of the app
463
+     * @param string $key config key
464
+     * @param string $default default value
465
+     * @param bool $lazy search within lazy loaded config
466
+     * @param int $type value type {@see VALUE_STRING} {@see VALUE_INT}{@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
467
+     *
468
+     * @return string
469
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
470
+     * @throws InvalidArgumentException
471
+     */
472
+    private function getTypedValue(
473
+        string $app,
474
+        string $key,
475
+        string $default,
476
+        bool $lazy,
477
+        int $type,
478
+    ): string {
479
+        $this->assertParams($app, $key, valueType: $type);
480
+        $origKey = $key;
481
+        $matched = $this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $default);
482
+        if ($default === null) {
483
+            // there is no logical reason for it to be null
484
+            throw new \Exception('default cannot be null');
485
+        }
486
+
487
+        // returns default if strictness of lexicon is set to WARNING (block and report)
488
+        if (!$matched) {
489
+            return $default;
490
+        }
491
+
492
+        $this->loadConfig($app, $lazy ?? true);
493
+
494
+        /**
495
+         * We ignore check if mixed type is requested.
496
+         * If type of stored value is set as mixed, we don't filter.
497
+         * If type of stored value is defined, we compare with the one requested.
498
+         */
499
+        $knownType = $this->valueTypes[$app][$key] ?? 0;
500
+        if (!$this->isTyped(self::VALUE_MIXED, $type)
501
+            && $knownType > 0
502
+            && !$this->isTyped(self::VALUE_MIXED, $knownType)
503
+            && !$this->isTyped($type, $knownType)) {
504
+            $this->logger->warning('conflict with value type from database', ['app' => $app, 'key' => $key, 'type' => $type, 'knownType' => $knownType]);
505
+            throw new AppConfigTypeConflictException('conflict with value type from database');
506
+        }
507
+
508
+        /**
509
+         * - the pair $app/$key cannot exist in both array,
510
+         * - we should still return an existing non-lazy value even if current method
511
+         *   is called with $lazy is true
512
+         *
513
+         * This way, lazyCache will be empty until the load for lazy config value is requested.
514
+         */
515
+        if (isset($this->lazyCache[$app][$key])) {
516
+            $value = $this->lazyCache[$app][$key];
517
+        } elseif (isset($this->fastCache[$app][$key])) {
518
+            $value = $this->fastCache[$app][$key];
519
+        } else {
520
+            return $default;
521
+        }
522
+
523
+        $sensitive = $this->isTyped(self::VALUE_SENSITIVE, $knownType);
524
+        if ($sensitive && str_starts_with($value, self::ENCRYPTION_PREFIX)) {
525
+            // Only decrypt values that are stored encrypted
526
+            $value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
527
+        }
528
+
529
+        // in case the key was modified while running matchAndApplyLexiconDefinition() we are
530
+        // interested to check options in case a modification of the value is needed
531
+        // ie inverting value from previous key when using lexicon option RENAME_INVERT_BOOLEAN
532
+        if ($origKey !== $key && $type === self::VALUE_BOOL) {
533
+            $value = ($this->configManager->convertToBool($value, $this->getLexiconEntry($app, $key))) ? '1' : '0';
534
+        }
535
+
536
+        return $value;
537
+    }
538
+
539
+    /**
540
+     * @inheritDoc
541
+     *
542
+     * @param string $app id of the app
543
+     * @param string $key config key
544
+     *
545
+     * @return int type of the value
546
+     * @throws AppConfigUnknownKeyException if config key is not known
547
+     * @since 29.0.0
548
+     * @see VALUE_STRING
549
+     * @see VALUE_INT
550
+     * @see VALUE_FLOAT
551
+     * @see VALUE_BOOL
552
+     * @see VALUE_ARRAY
553
+     */
554
+    public function getValueType(string $app, string $key, ?bool $lazy = null): int {
555
+        $type = self::VALUE_MIXED;
556
+        $ignorable = $lazy ?? false;
557
+        $this->matchAndApplyLexiconDefinition($app, $key, $ignorable, $type);
558
+        if ($type !== self::VALUE_MIXED) {
559
+            // a modified $type means config key is set in Lexicon
560
+            return $type;
561
+        }
562
+
563
+        $this->assertParams($app, $key);
564
+        $this->loadConfig($app, $lazy ?? true);
565
+
566
+        if (!isset($this->valueTypes[$app][$key])) {
567
+            throw new AppConfigUnknownKeyException('unknown config key');
568
+        }
569
+
570
+        $type = $this->valueTypes[$app][$key];
571
+        $type &= ~self::VALUE_SENSITIVE;
572
+        return $type;
573
+    }
574
+
575
+
576
+    /**
577
+     * Store a config key and its value in database as VALUE_MIXED
578
+     *
579
+     * **WARNING:** Method is internal and **MUST** not be used as it is best to set a real value type
580
+     *
581
+     * @param string $app id of the app
582
+     * @param string $key config key
583
+     * @param string $value config value
584
+     * @param bool $lazy set config as lazy loaded
585
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
586
+     *
587
+     * @return bool TRUE if value was different, therefor updated in database
588
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED
589
+     * @internal
590
+     * @since 29.0.0
591
+     * @see IAppConfig for explanation about lazy loading
592
+     * @see setValueString()
593
+     * @see setValueInt()
594
+     * @see setValueFloat()
595
+     * @see setValueBool()
596
+     * @see setValueArray()
597
+     */
598
+    public function setValueMixed(
599
+        string $app,
600
+        string $key,
601
+        string $value,
602
+        bool $lazy = false,
603
+        bool $sensitive = false,
604
+    ): bool {
605
+        return $this->setTypedValue(
606
+            $app,
607
+            $key,
608
+            $value,
609
+            $lazy,
610
+            self::VALUE_MIXED | ($sensitive ? self::VALUE_SENSITIVE : 0)
611
+        );
612
+    }
613
+
614
+
615
+    /**
616
+     * @inheritDoc
617
+     *
618
+     * @param string $app id of the app
619
+     * @param string $key config key
620
+     * @param string $value config value
621
+     * @param bool $lazy set config as lazy loaded
622
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
623
+     *
624
+     * @return bool TRUE if value was different, therefor updated in database
625
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
626
+     * @since 29.0.0
627
+     * @see IAppConfig for explanation about lazy loading
628
+     */
629
+    public function setValueString(
630
+        string $app,
631
+        string $key,
632
+        string $value,
633
+        bool $lazy = false,
634
+        bool $sensitive = false,
635
+    ): bool {
636
+        return $this->setTypedValue(
637
+            $app,
638
+            $key,
639
+            $value,
640
+            $lazy,
641
+            self::VALUE_STRING | ($sensitive ? self::VALUE_SENSITIVE : 0)
642
+        );
643
+    }
644
+
645
+    /**
646
+     * @inheritDoc
647
+     *
648
+     * @param string $app id of the app
649
+     * @param string $key config key
650
+     * @param int $value config value
651
+     * @param bool $lazy set config as lazy loaded
652
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
653
+     *
654
+     * @return bool TRUE if value was different, therefor updated in database
655
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
656
+     * @since 29.0.0
657
+     * @see IAppConfig for explanation about lazy loading
658
+     */
659
+    public function setValueInt(
660
+        string $app,
661
+        string $key,
662
+        int $value,
663
+        bool $lazy = false,
664
+        bool $sensitive = false,
665
+    ): bool {
666
+        if ($value > 2000000000) {
667
+            $this->logger->debug('You are trying to store an integer value around/above 2,147,483,647. This is a reminder that reaching this theoretical limit on 32 bits system will throw an exception.');
668
+        }
669
+
670
+        return $this->setTypedValue(
671
+            $app,
672
+            $key,
673
+            (string)$value,
674
+            $lazy,
675
+            self::VALUE_INT | ($sensitive ? self::VALUE_SENSITIVE : 0)
676
+        );
677
+    }
678
+
679
+    /**
680
+     * @inheritDoc
681
+     *
682
+     * @param string $app id of the app
683
+     * @param string $key config key
684
+     * @param float $value config value
685
+     * @param bool $lazy set config as lazy loaded
686
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
687
+     *
688
+     * @return bool TRUE if value was different, therefor updated in database
689
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
690
+     * @since 29.0.0
691
+     * @see IAppConfig for explanation about lazy loading
692
+     */
693
+    public function setValueFloat(
694
+        string $app,
695
+        string $key,
696
+        float $value,
697
+        bool $lazy = false,
698
+        bool $sensitive = false,
699
+    ): bool {
700
+        return $this->setTypedValue(
701
+            $app,
702
+            $key,
703
+            (string)$value,
704
+            $lazy,
705
+            self::VALUE_FLOAT | ($sensitive ? self::VALUE_SENSITIVE : 0)
706
+        );
707
+    }
708
+
709
+    /**
710
+     * @inheritDoc
711
+     *
712
+     * @param string $app id of the app
713
+     * @param string $key config key
714
+     * @param bool $value config value
715
+     * @param bool $lazy set config as lazy loaded
716
+     *
717
+     * @return bool TRUE if value was different, therefor updated in database
718
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
719
+     * @since 29.0.0
720
+     * @see IAppConfig for explanation about lazy loading
721
+     */
722
+    public function setValueBool(
723
+        string $app,
724
+        string $key,
725
+        bool $value,
726
+        bool $lazy = false,
727
+    ): bool {
728
+        return $this->setTypedValue(
729
+            $app,
730
+            $key,
731
+            ($value) ? '1' : '0',
732
+            $lazy,
733
+            self::VALUE_BOOL
734
+        );
735
+    }
736
+
737
+    /**
738
+     * @inheritDoc
739
+     *
740
+     * @param string $app id of the app
741
+     * @param string $key config key
742
+     * @param array $value config value
743
+     * @param bool $lazy set config as lazy loaded
744
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
745
+     *
746
+     * @return bool TRUE if value was different, therefor updated in database
747
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
748
+     * @throws JsonException
749
+     * @since 29.0.0
750
+     * @see IAppConfig for explanation about lazy loading
751
+     */
752
+    public function setValueArray(
753
+        string $app,
754
+        string $key,
755
+        array $value,
756
+        bool $lazy = false,
757
+        bool $sensitive = false,
758
+    ): bool {
759
+        try {
760
+            return $this->setTypedValue(
761
+                $app,
762
+                $key,
763
+                json_encode($value, JSON_THROW_ON_ERROR),
764
+                $lazy,
765
+                self::VALUE_ARRAY | ($sensitive ? self::VALUE_SENSITIVE : 0)
766
+            );
767
+        } catch (JsonException $e) {
768
+            $this->logger->warning('could not setValueArray', ['app' => $app, 'key' => $key, 'exception' => $e]);
769
+            throw $e;
770
+        }
771
+    }
772
+
773
+    /**
774
+     * Store a config key and its value in database
775
+     *
776
+     * If config key is already known with the exact same config value and same sensitive/lazy status, the
777
+     * database is not updated. If config value was previously stored as sensitive, status will not be
778
+     * altered.
779
+     *
780
+     * @param string $app id of the app
781
+     * @param string $key config key
782
+     * @param string $value config value
783
+     * @param bool $lazy config set as lazy loaded
784
+     * @param int $type value type {@see VALUE_STRING} {@see VALUE_INT} {@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
785
+     *
786
+     * @return bool TRUE if value was updated in database
787
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
788
+     * @see IAppConfig for explanation about lazy loading
789
+     */
790
+    private function setTypedValue(
791
+        string $app,
792
+        string $key,
793
+        string $value,
794
+        bool $lazy,
795
+        int $type,
796
+    ): bool {
797
+        $this->assertParams($app, $key);
798
+        if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type)) {
799
+            return false; // returns false as database is not updated
800
+        }
801
+        $this->loadConfig(null, $lazy ?? true);
802
+
803
+        $sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type);
804
+        $inserted = $refreshCache = false;
805
+
806
+        $origValue = $value;
807
+        if ($sensitive || ($this->hasKey($app, $key, $lazy) && $this->isSensitive($app, $key, $lazy))) {
808
+            $value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
809
+        }
810
+
811
+        if ($this->hasKey($app, $key, $lazy)) {
812
+            /**
813
+             * no update if key is already known with set lazy status and value is
814
+             * not different, unless sensitivity is switched from false to true.
815
+             */
816
+            if ($origValue === $this->getTypedValue($app, $key, $value, $lazy ?? true, $type)
817
+                && (!$sensitive || $this->isSensitive($app, $key, $lazy))) {
818
+                return false;
819
+            }
820
+        } else {
821
+            /**
822
+             * if key is not known yet, we try to insert.
823
+             * It might fail if the key exists with a different lazy flag.
824
+             */
825
+            try {
826
+                $insert = $this->connection->getQueryBuilder();
827
+                $insert->insert('appconfig')
828
+                    ->setValue('appid', $insert->createNamedParameter($app))
829
+                    ->setValue('lazy', $insert->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
830
+                    ->setValue('type', $insert->createNamedParameter($type, IQueryBuilder::PARAM_INT))
831
+                    ->setValue('configkey', $insert->createNamedParameter($key))
832
+                    ->setValue('configvalue', $insert->createNamedParameter($value));
833
+                $insert->executeStatement();
834
+                $inserted = true;
835
+            } catch (DBException $e) {
836
+                if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
837
+                    throw $e; // TODO: throw exception or just log and returns false !?
838
+                }
839
+            }
840
+        }
841
+
842
+        /**
843
+         * We cannot insert a new row, meaning we need to update an already existing one
844
+         */
845
+        if (!$inserted) {
846
+            $currType = $this->valueTypes[$app][$key] ?? 0;
847
+            if ($currType === 0) { // this might happen when switching lazy loading status
848
+                $this->loadConfig(lazy: true);
849
+                $currType = $this->valueTypes[$app][$key] ?? 0;
850
+            }
851
+
852
+            /**
853
+             * This should only happen during the upgrade process from 28 to 29.
854
+             * We only log a warning and set it to VALUE_MIXED.
855
+             */
856
+            if ($currType === 0) {
857
+                $this->logger->warning('Value type is set to zero (0) in database. This is fine only during the upgrade process from 28 to 29.', ['app' => $app, 'key' => $key]);
858
+                $currType = self::VALUE_MIXED;
859
+            }
860
+
861
+            /**
862
+             * we only accept a different type from the one stored in database
863
+             * if the one stored in database is not-defined (VALUE_MIXED)
864
+             */
865
+            if (!$this->isTyped(self::VALUE_MIXED, $currType)
866
+                && ($type | self::VALUE_SENSITIVE) !== ($currType | self::VALUE_SENSITIVE)) {
867
+                try {
868
+                    $currType = $this->convertTypeToString($currType);
869
+                    $type = $this->convertTypeToString($type);
870
+                } catch (AppConfigIncorrectTypeException) {
871
+                    // can be ignored, this was just needed for a better exception message.
872
+                }
873
+                throw new AppConfigTypeConflictException('conflict between new type (' . $type . ') and old type (' . $currType . ')');
874
+            }
875
+
876
+            // we fix $type if the stored value, or the new value as it might be changed, is set as sensitive
877
+            if ($sensitive || $this->isTyped(self::VALUE_SENSITIVE, $currType)) {
878
+                $type |= self::VALUE_SENSITIVE;
879
+            }
880
+
881
+            try {
882
+                if ($lazy !== $this->isLazy($app, $key)) {
883
+                    $refreshCache = true;
884
+                }
885
+            } catch (AppConfigUnknownKeyException) {
886
+                // pass
887
+            }
888
+
889
+            $update = $this->connection->getQueryBuilder();
890
+            $update->update('appconfig')
891
+                ->set('configvalue', $update->createNamedParameter($value))
892
+                ->set('lazy', $update->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
893
+                ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
894
+                ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
895
+                ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
896
+
897
+            $update->executeStatement();
898
+        }
899
+
900
+        if ($refreshCache) {
901
+            $this->clearCache();
902
+            return true;
903
+        }
904
+
905
+        // update local cache
906
+        if ($lazy) {
907
+            $this->lazyCache[$app][$key] = $value;
908
+        } else {
909
+            $this->fastCache[$app][$key] = $value;
910
+        }
911
+        $this->valueTypes[$app][$key] = $type;
912
+        $this->clearLocalCache();
913
+
914
+        return true;
915
+    }
916
+
917
+    /**
918
+     * Change the type of config value.
919
+     *
920
+     * **WARNING:** Method is internal and **MUST** not be used as it may break things.
921
+     *
922
+     * @param string $app id of the app
923
+     * @param string $key config key
924
+     * @param int $type value type {@see VALUE_STRING} {@see VALUE_INT} {@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
925
+     *
926
+     * @return bool TRUE if database update were necessary
927
+     * @throws AppConfigUnknownKeyException if $key is now known in database
928
+     * @throws AppConfigIncorrectTypeException if $type is not valid
929
+     * @internal
930
+     * @since 29.0.0
931
+     */
932
+    public function updateType(string $app, string $key, int $type = self::VALUE_MIXED): bool {
933
+        $this->assertParams($app, $key);
934
+        $this->loadConfig(lazy: true);
935
+        $this->matchAndApplyLexiconDefinition($app, $key);
936
+        $this->isLazy($app, $key); // confirm key exists
937
+
938
+        // type can only be one type
939
+        if (!in_array($type, [self::VALUE_MIXED, self::VALUE_STRING, self::VALUE_INT, self::VALUE_FLOAT, self::VALUE_BOOL, self::VALUE_ARRAY])) {
940
+            throw new AppConfigIncorrectTypeException('Unknown value type');
941
+        }
942
+
943
+        $currType = $this->valueTypes[$app][$key];
944
+        if (($type | self::VALUE_SENSITIVE) === ($currType | self::VALUE_SENSITIVE)) {
945
+            return false;
946
+        }
947
+
948
+        // we complete with sensitive flag if the stored value is set as sensitive
949
+        if ($this->isTyped(self::VALUE_SENSITIVE, $currType)) {
950
+            $type = $type | self::VALUE_SENSITIVE;
951
+        }
952
+
953
+        $update = $this->connection->getQueryBuilder();
954
+        $update->update('appconfig')
955
+            ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
956
+            ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
957
+            ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
958
+        $update->executeStatement();
959
+        $this->valueTypes[$app][$key] = $type;
960
+
961
+        return true;
962
+    }
963
+
964
+
965
+    /**
966
+     * @inheritDoc
967
+     *
968
+     * @param string $app id of the app
969
+     * @param string $key config key
970
+     * @param bool $sensitive TRUE to set as sensitive, FALSE to unset
971
+     *
972
+     * @return bool TRUE if entry was found in database and an update was necessary
973
+     * @since 29.0.0
974
+     */
975
+    public function updateSensitive(string $app, string $key, bool $sensitive): bool {
976
+        $this->assertParams($app, $key);
977
+        $this->loadConfig(lazy: true);
978
+        $this->matchAndApplyLexiconDefinition($app, $key);
979
+
980
+        try {
981
+            if ($sensitive === $this->isSensitive($app, $key, null)) {
982
+                return false;
983
+            }
984
+        } catch (AppConfigUnknownKeyException $e) {
985
+            return false;
986
+        }
987
+
988
+        $lazy = $this->isLazy($app, $key);
989
+        if ($lazy) {
990
+            $cache = $this->lazyCache;
991
+        } else {
992
+            $cache = $this->fastCache;
993
+        }
994
+
995
+        if (!isset($cache[$app][$key])) {
996
+            throw new AppConfigUnknownKeyException('unknown config key');
997
+        }
998
+
999
+        /**
1000
+         * type returned by getValueType() is already cleaned from sensitive flag
1001
+         * we just need to update it based on $sensitive and store it in database
1002
+         */
1003
+        $type = $this->getValueType($app, $key);
1004
+        $value = $cache[$app][$key];
1005
+        if ($sensitive) {
1006
+            $type |= self::VALUE_SENSITIVE;
1007
+            $value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
1008
+        } else {
1009
+            $value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
1010
+        }
1011
+
1012
+        $update = $this->connection->getQueryBuilder();
1013
+        $update->update('appconfig')
1014
+            ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
1015
+            ->set('configvalue', $update->createNamedParameter($value))
1016
+            ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
1017
+            ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1018
+        $update->executeStatement();
1019
+
1020
+        $this->valueTypes[$app][$key] = $type;
1021
+
1022
+        return true;
1023
+    }
1024
+
1025
+    /**
1026
+     * @inheritDoc
1027
+     *
1028
+     * @param string $app id of the app
1029
+     * @param string $key config key
1030
+     * @param bool $lazy TRUE to set as lazy loaded, FALSE to unset
1031
+     *
1032
+     * @return bool TRUE if entry was found in database and an update was necessary
1033
+     * @since 29.0.0
1034
+     */
1035
+    public function updateLazy(string $app, string $key, bool $lazy): bool {
1036
+        $this->assertParams($app, $key);
1037
+        $this->loadConfig(lazy: true);
1038
+        $this->matchAndApplyLexiconDefinition($app, $key);
1039
+
1040
+        try {
1041
+            if ($lazy === $this->isLazy($app, $key)) {
1042
+                return false;
1043
+            }
1044
+        } catch (AppConfigUnknownKeyException $e) {
1045
+            return false;
1046
+        }
1047
+
1048
+        $update = $this->connection->getQueryBuilder();
1049
+        $update->update('appconfig')
1050
+            ->set('lazy', $update->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))
1051
+            ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
1052
+            ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
1053
+        $update->executeStatement();
1054
+
1055
+        // At this point, it is a lot safer to clean cache
1056
+        $this->clearCache();
1057
+
1058
+        return true;
1059
+    }
1060
+
1061
+    /**
1062
+     * @inheritDoc
1063
+     *
1064
+     * @param string $app id of the app
1065
+     * @param string $key config key
1066
+     *
1067
+     * @return array
1068
+     * @throws AppConfigUnknownKeyException if config key is not known in database
1069
+     * @since 29.0.0
1070
+     */
1071
+    public function getDetails(string $app, string $key): array {
1072
+        $this->assertParams($app, $key);
1073
+        $this->loadConfig(lazy: true);
1074
+        $this->matchAndApplyLexiconDefinition($app, $key);
1075
+        $lazy = $this->isLazy($app, $key);
1076
+
1077
+        if ($lazy) {
1078
+            $cache = $this->lazyCache;
1079
+        } else {
1080
+            $cache = $this->fastCache;
1081
+        }
1082
+
1083
+        $type = $this->getValueType($app, $key);
1084
+        try {
1085
+            $typeString = $this->convertTypeToString($type);
1086
+        } catch (AppConfigIncorrectTypeException $e) {
1087
+            $this->logger->warning('type stored in database is not correct', ['exception' => $e, 'type' => $type]);
1088
+            $typeString = (string)$type;
1089
+        }
1090
+
1091
+        if (!isset($cache[$app][$key])) {
1092
+            throw new AppConfigUnknownKeyException('unknown config key');
1093
+        }
1094
+
1095
+        $value = $cache[$app][$key];
1096
+        $sensitive = $this->isSensitive($app, $key, null);
1097
+        if ($sensitive && str_starts_with($value, self::ENCRYPTION_PREFIX)) {
1098
+            $value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
1099
+        }
1100
+
1101
+        return [
1102
+            'app' => $app,
1103
+            'key' => $key,
1104
+            'value' => $value,
1105
+            'type' => $type,
1106
+            'lazy' => $lazy,
1107
+            'typeString' => $typeString,
1108
+            'sensitive' => $sensitive
1109
+        ];
1110
+    }
1111
+
1112
+    /**
1113
+     * @inheritDoc
1114
+     *
1115
+     * @param string $app id of the app
1116
+     * @param string $key config key
1117
+     *
1118
+     * @return array{app: string, key: string, lazy?: bool, valueType?: ValueType, valueTypeName?: string, sensitive?: bool, default?: string, definition?: string, note?: string}
1119
+     * @since 32.0.0
1120
+     */
1121
+    public function getKeyDetails(string $app, string $key): array {
1122
+        $this->assertParams($app, $key);
1123
+        try {
1124
+            $details = $this->getDetails($app, $key);
1125
+        } catch (AppConfigUnknownKeyException) {
1126
+            $details = [
1127
+                'app' => $app,
1128
+                'key' => $key
1129
+            ];
1130
+        }
1131
+
1132
+        /** @var Entry $lexiconEntry */
1133
+        try {
1134
+            $lazy = false;
1135
+            $this->matchAndApplyLexiconDefinition($app, $key, $lazy, lexiconEntry: $lexiconEntry);
1136
+        } catch (AppConfigTypeConflictException|AppConfigUnknownKeyException) {
1137
+            // can be ignored
1138
+        }
1139
+
1140
+        if ($lexiconEntry !== null) {
1141
+            $details = array_merge($details, [
1142
+                'lazy' => $lexiconEntry->isLazy(),
1143
+                'valueType' => $lexiconEntry->getValueType(),
1144
+                'valueTypeName' => $lexiconEntry->getValueType()->name,
1145
+                'sensitive' => $lexiconEntry->isFlagged(self::FLAG_SENSITIVE),
1146
+                'default' => $lexiconEntry->getDefault($this->presetManager->getLexiconPreset()),
1147
+                'definition' => $lexiconEntry->getDefinition(),
1148
+                'note' => $lexiconEntry->getNote(),
1149
+            ]);
1150
+        }
1151
+
1152
+        return array_filter($details, static fn ($v): bool => ($v !== null));
1153
+    }
1154
+
1155
+    /**
1156
+     * @param string $type
1157
+     *
1158
+     * @return int
1159
+     * @throws AppConfigIncorrectTypeException
1160
+     * @since 29.0.0
1161
+     */
1162
+    public function convertTypeToInt(string $type): int {
1163
+        return match (strtolower($type)) {
1164
+            'mixed' => IAppConfig::VALUE_MIXED,
1165
+            'string' => IAppConfig::VALUE_STRING,
1166
+            'integer' => IAppConfig::VALUE_INT,
1167
+            'float' => IAppConfig::VALUE_FLOAT,
1168
+            'boolean' => IAppConfig::VALUE_BOOL,
1169
+            'array' => IAppConfig::VALUE_ARRAY,
1170
+            default => throw new AppConfigIncorrectTypeException('Unknown type ' . $type)
1171
+        };
1172
+    }
1173
+
1174
+    /**
1175
+     * @param int $type
1176
+     *
1177
+     * @return string
1178
+     * @throws AppConfigIncorrectTypeException
1179
+     * @since 29.0.0
1180
+     */
1181
+    public function convertTypeToString(int $type): string {
1182
+        $type &= ~self::VALUE_SENSITIVE;
1183
+
1184
+        return match ($type) {
1185
+            IAppConfig::VALUE_MIXED => 'mixed',
1186
+            IAppConfig::VALUE_STRING => 'string',
1187
+            IAppConfig::VALUE_INT => 'integer',
1188
+            IAppConfig::VALUE_FLOAT => 'float',
1189
+            IAppConfig::VALUE_BOOL => 'boolean',
1190
+            IAppConfig::VALUE_ARRAY => 'array',
1191
+            default => throw new AppConfigIncorrectTypeException('Unknown numeric type ' . $type)
1192
+        };
1193
+    }
1194
+
1195
+    /**
1196
+     * @inheritDoc
1197
+     *
1198
+     * @param string $app id of the app
1199
+     * @param string $key config key
1200
+     *
1201
+     * @since 29.0.0
1202
+     */
1203
+    public function deleteKey(string $app, string $key): void {
1204
+        $this->assertParams($app, $key);
1205
+        $this->matchAndApplyLexiconDefinition($app, $key);
1206
+
1207
+        $qb = $this->connection->getQueryBuilder();
1208
+        $qb->delete('appconfig')
1209
+            ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
1210
+            ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
1211
+        $qb->executeStatement();
1212
+
1213
+        unset($this->lazyCache[$app][$key]);
1214
+        unset($this->fastCache[$app][$key]);
1215
+        unset($this->valueTypes[$app][$key]);
1216
+        $this->clearLocalCache();
1217
+    }
1218
+
1219
+    /**
1220
+     * @inheritDoc
1221
+     *
1222
+     * @param string $app id of the app
1223
+     *
1224
+     * @since 29.0.0
1225
+     */
1226
+    public function deleteApp(string $app): void {
1227
+        $this->assertParams($app);
1228
+        $qb = $this->connection->getQueryBuilder();
1229
+        $qb->delete('appconfig')
1230
+            ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
1231
+        $qb->executeStatement();
1232
+
1233
+        $this->clearCache();
1234
+    }
1235
+
1236
+    /**
1237
+     * @inheritDoc
1238
+     *
1239
+     * @param bool $reload set to TRUE to refill cache instantly after clearing it
1240
+     *
1241
+     * @internal
1242
+     * @since 29.0.0
1243
+     */
1244
+    public function clearCache(bool $reload = false): void {
1245
+        $this->lazyLoaded = $this->fastLoaded = false;
1246
+        $this->lazyCache = $this->fastCache = $this->valueTypes = $this->configLexiconDetails = [];
1247
+        $this->localCache?->remove(self::LOCAL_CACHE_KEY);
1248
+
1249
+        if (!$reload) {
1250
+            return;
1251
+        }
1252
+
1253
+        $this->loadConfig(lazy: true);
1254
+    }
1255
+
1256
+
1257
+    /**
1258
+     * For debug purpose.
1259
+     * Returns the cached data.
1260
+     *
1261
+     * @return array
1262
+     * @since 29.0.0
1263
+     * @internal
1264
+     */
1265
+    public function statusCache(): array {
1266
+        return [
1267
+            'fastLoaded' => $this->fastLoaded,
1268
+            'fastCache' => $this->fastCache,
1269
+            'lazyLoaded' => $this->lazyLoaded,
1270
+            'lazyCache' => $this->lazyCache,
1271
+        ];
1272
+    }
1273
+
1274
+    /**
1275
+     * @param int $needle bitflag to search
1276
+     * @param int $type known value
1277
+     *
1278
+     * @return bool TRUE if bitflag $needle is set in $type
1279
+     */
1280
+    private function isTyped(int $needle, int $type): bool {
1281
+        return (($needle & $type) !== 0);
1282
+    }
1283
+
1284
+    /**
1285
+     * Confirm the string set for app and key fit the database description
1286
+     *
1287
+     * @param string $app assert $app fit in database
1288
+     * @param string $configKey assert config key fit in database
1289
+     * @param bool $allowEmptyApp $app can be empty string
1290
+     * @param int $valueType assert value type is only one type
1291
+     *
1292
+     * @throws InvalidArgumentException
1293
+     */
1294
+    private function assertParams(string $app = '', string $configKey = '', bool $allowEmptyApp = false, int $valueType = -1): void {
1295
+        if (!$allowEmptyApp && $app === '') {
1296
+            throw new InvalidArgumentException('app cannot be an empty string');
1297
+        }
1298
+        if (strlen($app) > self::APP_MAX_LENGTH) {
1299
+            throw new InvalidArgumentException(
1300
+                'Value (' . $app . ') for app is too long (' . self::APP_MAX_LENGTH . ')'
1301
+            );
1302
+        }
1303
+        if (strlen($configKey) > self::KEY_MAX_LENGTH) {
1304
+            throw new InvalidArgumentException('Value (' . $configKey . ') for key is too long (' . self::KEY_MAX_LENGTH . ')');
1305
+        }
1306
+        if ($valueType > -1) {
1307
+            $valueType &= ~self::VALUE_SENSITIVE;
1308
+            if (!in_array($valueType, [self::VALUE_MIXED, self::VALUE_STRING, self::VALUE_INT, self::VALUE_FLOAT, self::VALUE_BOOL, self::VALUE_ARRAY])) {
1309
+                throw new InvalidArgumentException('Unknown value type');
1310
+            }
1311
+        }
1312
+    }
1313
+
1314
+    /**
1315
+     * Load normal config or config set as lazy loaded
1316
+     *
1317
+     * @param bool $lazy set to TRUE to also load config values set as lazy loaded
1318
+     */
1319
+    private function loadConfig(?string $app = null, bool $lazy = false): void {
1320
+        if ($this->isLoaded($lazy)) {
1321
+            return;
1322
+        }
1323
+
1324
+        // if lazy is null or true, we debug log
1325
+        if ($lazy === true && $app !== null) {
1326
+            $exception = new \RuntimeException('The loading of lazy AppConfig values have been triggered by app "' . $app . '"');
1327
+            $this->logger->debug($exception->getMessage(), ['exception' => $exception, 'app' => $app]);
1328
+        }
1329
+
1330
+        $loadLazyOnly = $lazy && $this->isLoaded();
1331
+
1332
+        /** @var array<mixed> */
1333
+        $cacheContent = $this->localCache?->get(self::LOCAL_CACHE_KEY) ?? [];
1334
+        $includesLazyValues = !empty($cacheContent) && !empty($cacheContent['lazyCache']);
1335
+        if (!empty($cacheContent) && (!$lazy || $includesLazyValues)) {
1336
+            $this->valueTypes = $cacheContent['valueTypes'];
1337
+            $this->fastCache = $cacheContent['fastCache'];
1338
+            $this->fastLoaded = !empty($this->fastCache);
1339
+            if ($includesLazyValues) {
1340
+                $this->lazyCache = $cacheContent['lazyCache'];
1341
+                $this->lazyLoaded = !empty($this->lazyCache);
1342
+            }
1343
+            return;
1344
+        }
1345
+
1346
+        // Otherwise no cache available and we need to fetch from database
1347
+        $qb = $this->connection->getQueryBuilder();
1348
+        $qb->from('appconfig')
1349
+            ->select('appid', 'configkey', 'configvalue', 'type');
1350
+
1351
+        if ($lazy === false) {
1352
+            $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
1353
+        } else {
1354
+            if ($loadLazyOnly) {
1355
+                $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT)));
1356
+            }
1357
+            $qb->addSelect('lazy');
1358
+        }
1359
+
1360
+        $result = $qb->executeQuery();
1361
+        $rows = $result->fetchAll();
1362
+        foreach ($rows as $row) {
1363
+            // most of the time, 'lazy' is not in the select because its value is already known
1364
+            if ($lazy && ((int)$row['lazy']) === 1) {
1365
+                $this->lazyCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1366
+            } else {
1367
+                $this->fastCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1368
+            }
1369
+            $this->valueTypes[$row['appid']][$row['configkey']] = (int)($row['type'] ?? 0);
1370
+        }
1371
+
1372
+        $result->closeCursor();
1373
+        $this->localCache?->set(
1374
+            self::LOCAL_CACHE_KEY,
1375
+            [
1376
+                'fastCache' => $this->fastCache,
1377
+                'lazyCache' => $this->lazyCache,
1378
+                'valueTypes' => $this->valueTypes,
1379
+            ],
1380
+            self::LOCAL_CACHE_TTL,
1381
+        );
1382
+
1383
+        $this->fastLoaded = true;
1384
+        $this->lazyLoaded = $lazy;
1385
+    }
1386
+
1387
+    /**
1388
+     * @param bool $lazy - If set to true then also check if lazy values are loaded
1389
+     */
1390
+    private function isLoaded(bool $lazy = false): bool {
1391
+        return $this->fastLoaded && (!$lazy || $this->lazyLoaded);
1392
+    }
1393
+
1394
+    /**
1395
+     * Gets the config value
1396
+     *
1397
+     * @param string $app app
1398
+     * @param string $key key
1399
+     * @param string $default - Default value if the key does not exist
1400
+     *
1401
+     * @return string the value or $default
1402
+     * @deprecated 29.0.0 use getValue*()
1403
+     *
1404
+     * This function gets a value from the appconfig table. If the key does
1405
+     * not exist the default value will be returned
1406
+     */
1407
+    public function getValue($app, $key, $default = '') {
1408
+        $this->loadConfig($app);
1409
+        $this->matchAndApplyLexiconDefinition($app, $key);
1410
+
1411
+        return $this->fastCache[$app][$key] ?? $default;
1412
+    }
1413
+
1414
+    /**
1415
+     * Sets a value. If the key did not exist before it will be created.
1416
+     *
1417
+     * @param string $app app
1418
+     * @param string $key key
1419
+     * @param string|float|int $value value
1420
+     *
1421
+     * @return bool True if the value was inserted or updated, false if the value was the same
1422
+     * @throws AppConfigTypeConflictException
1423
+     * @throws AppConfigUnknownKeyException
1424
+     * @deprecated 29.0.0
1425
+     */
1426
+    public function setValue($app, $key, $value) {
1427
+        /**
1428
+         * TODO: would it be overkill, or decently improve performance, to catch
1429
+         * call to this method with $key='enabled' and 'hide' config value related
1430
+         * to $app when the app is disabled (by modifying entry in database: lazy=lazy+2)
1431
+         * or enabled (lazy=lazy-2)
1432
+         *
1433
+         * this solution would remove the loading of config values from disabled app
1434
+         * unless calling the method.
1435
+         */
1436
+        return $this->setTypedValue($app, $key, (string)$value, false, self::VALUE_MIXED);
1437
+    }
1438
+
1439
+
1440
+    /**
1441
+     * get multiple values, either the app or key can be used as wildcard by setting it to false
1442
+     *
1443
+     * @param string|false $app
1444
+     * @param string|false $key
1445
+     *
1446
+     * @return array|false
1447
+     * @deprecated 29.0.0 use {@see getAllValues()}
1448
+     */
1449
+    public function getValues($app, $key) {
1450
+        if (($app !== false) === ($key !== false)) {
1451
+            return false;
1452
+        }
1453
+
1454
+        $key = ($key === false) ? '' : $key;
1455
+        if (!$app) {
1456
+            return $this->searchValues($key, false, self::VALUE_MIXED);
1457
+        } else {
1458
+            return $this->getAllValues($app, $key);
1459
+        }
1460
+    }
1461
+
1462
+    /**
1463
+     * get all values of the app or and filters out sensitive data
1464
+     *
1465
+     * @param string $app
1466
+     *
1467
+     * @return array
1468
+     * @deprecated 29.0.0 use {@see getAllValues()}
1469
+     */
1470
+    public function getFilteredValues($app) {
1471
+        return $this->getAllValues($app, filtered: true);
1472
+    }
1473
+
1474
+
1475
+    /**
1476
+     * **Warning:** avoid default NULL value for $lazy as this will
1477
+     * load all lazy values from the database
1478
+     *
1479
+     * @param string $app
1480
+     * @param array<string, string> $values ['key' => 'value']
1481
+     * @param bool|null $lazy
1482
+     *
1483
+     * @return array<string, string|int|float|bool|array>
1484
+     */
1485
+    private function formatAppValues(string $app, array $values, ?bool $lazy = null): array {
1486
+        foreach ($values as $key => $value) {
1487
+            try {
1488
+                $type = $this->getValueType($app, $key, $lazy);
1489
+            } catch (AppConfigUnknownKeyException) {
1490
+                continue;
1491
+            }
1492
+
1493
+            $values[$key] = $this->convertTypedValue($value, $type);
1494
+        }
1495
+
1496
+        return $values;
1497
+    }
1498
+
1499
+    /**
1500
+     * convert string value to the expected type
1501
+     *
1502
+     * @param string $value
1503
+     * @param int $type
1504
+     *
1505
+     * @return string|int|float|bool|array
1506
+     */
1507
+    private function convertTypedValue(string $value, int $type): string|int|float|bool|array {
1508
+        switch ($type) {
1509
+            case self::VALUE_INT:
1510
+                return (int)$value;
1511
+            case self::VALUE_FLOAT:
1512
+                return (float)$value;
1513
+            case self::VALUE_BOOL:
1514
+                return in_array(strtolower($value), ['1', 'true', 'yes', 'on']);
1515
+            case self::VALUE_ARRAY:
1516
+                try {
1517
+                    return json_decode($value, true, flags: JSON_THROW_ON_ERROR);
1518
+                } catch (JsonException $e) {
1519
+                    // ignoreable
1520
+                }
1521
+                break;
1522
+        }
1523
+        return $value;
1524
+    }
1525
+
1526
+    /**
1527
+     * @param string $app
1528
+     *
1529
+     * @return string[]
1530
+     * @deprecated 29.0.0 data sensitivity should be set when calling setValue*()
1531
+     */
1532
+    private function getSensitiveKeys(string $app): array {
1533
+        $sensitiveValues = [
1534
+            'circles' => [
1535
+                '/^key_pairs$/',
1536
+                '/^local_gskey$/',
1537
+            ],
1538
+            'call_summary_bot' => [
1539
+                '/^secret_(.*)$/',
1540
+            ],
1541
+            'external' => [
1542
+                '/^sites$/',
1543
+                '/^jwt_token_privkey_(.*)$/',
1544
+            ],
1545
+            'globalsiteselector' => [
1546
+                '/^gss\.jwt\.key$/',
1547
+            ],
1548
+            'gpgmailer' => [
1549
+                '/^GpgServerKey$/',
1550
+            ],
1551
+            'integration_discourse' => [
1552
+                '/^private_key$/',
1553
+                '/^public_key$/',
1554
+            ],
1555
+            'integration_dropbox' => [
1556
+                '/^client_id$/',
1557
+                '/^client_secret$/',
1558
+            ],
1559
+            'integration_github' => [
1560
+                '/^client_id$/',
1561
+                '/^client_secret$/',
1562
+            ],
1563
+            'integration_gitlab' => [
1564
+                '/^client_id$/',
1565
+                '/^client_secret$/',
1566
+                '/^oauth_instance_url$/',
1567
+            ],
1568
+            'integration_google' => [
1569
+                '/^client_id$/',
1570
+                '/^client_secret$/',
1571
+            ],
1572
+            'integration_jira' => [
1573
+                '/^client_id$/',
1574
+                '/^client_secret$/',
1575
+                '/^forced_instance_url$/',
1576
+            ],
1577
+            'integration_onedrive' => [
1578
+                '/^client_id$/',
1579
+                '/^client_secret$/',
1580
+            ],
1581
+            'integration_openproject' => [
1582
+                '/^client_id$/',
1583
+                '/^client_secret$/',
1584
+                '/^oauth_instance_url$/',
1585
+            ],
1586
+            'integration_reddit' => [
1587
+                '/^client_id$/',
1588
+                '/^client_secret$/',
1589
+            ],
1590
+            'integration_suitecrm' => [
1591
+                '/^client_id$/',
1592
+                '/^client_secret$/',
1593
+                '/^oauth_instance_url$/',
1594
+            ],
1595
+            'integration_twitter' => [
1596
+                '/^consumer_key$/',
1597
+                '/^consumer_secret$/',
1598
+                '/^followed_user$/',
1599
+            ],
1600
+            'integration_zammad' => [
1601
+                '/^client_id$/',
1602
+                '/^client_secret$/',
1603
+                '/^oauth_instance_url$/',
1604
+            ],
1605
+            'maps' => [
1606
+                '/^mapboxAPIKEY$/',
1607
+            ],
1608
+            'notify_push' => [
1609
+                '/^cookie$/',
1610
+            ],
1611
+            'onlyoffice' => [
1612
+                '/^jwt_secret$/',
1613
+            ],
1614
+            'passwords' => [
1615
+                '/^SSEv1ServerKey$/',
1616
+            ],
1617
+            'serverinfo' => [
1618
+                '/^token$/',
1619
+            ],
1620
+            'spreed' => [
1621
+                '/^bridge_bot_password$/',
1622
+                '/^hosted-signaling-server-(.*)$/',
1623
+                '/^recording_servers$/',
1624
+                '/^signaling_servers$/',
1625
+                '/^signaling_ticket_secret$/',
1626
+                '/^signaling_token_privkey_(.*)$/',
1627
+                '/^signaling_token_pubkey_(.*)$/',
1628
+                '/^sip_bridge_dialin_info$/',
1629
+                '/^sip_bridge_shared_secret$/',
1630
+                '/^stun_servers$/',
1631
+                '/^turn_servers$/',
1632
+                '/^turn_server_secret$/',
1633
+            ],
1634
+            'support' => [
1635
+                '/^last_response$/',
1636
+                '/^potential_subscription_key$/',
1637
+                '/^subscription_key$/',
1638
+            ],
1639
+            'theming' => [
1640
+                '/^imprintUrl$/',
1641
+                '/^privacyUrl$/',
1642
+                '/^slogan$/',
1643
+                '/^url$/',
1644
+            ],
1645
+            'twofactor_gateway' => [
1646
+                '/^.*token$/',
1647
+            ],
1648
+            'user_ldap' => [
1649
+                '/^(s..)?ldap_agent_password$/',
1650
+            ],
1651
+            'user_saml' => [
1652
+                '/^idp-x509cert$/',
1653
+            ],
1654
+            'whiteboard' => [
1655
+                '/^jwt_secret_key$/',
1656
+            ],
1657
+        ];
1658
+
1659
+        return $sensitiveValues[$app] ?? [];
1660
+    }
1661
+
1662
+    /**
1663
+     * Clear all the cached app config values
1664
+     * New cache will be generated next time a config value is retrieved
1665
+     *
1666
+     * @deprecated 29.0.0 use {@see clearCache()}
1667
+     */
1668
+    public function clearCachedConfig(): void {
1669
+        $this->clearCache();
1670
+    }
1671
+
1672
+    /**
1673
+     * Match and apply current use of config values with defined lexicon.
1674
+     * Set $lazy to NULL only if only interested into checking that $key is alias.
1675
+     *
1676
+     * @throws AppConfigUnknownKeyException
1677
+     * @throws AppConfigTypeConflictException
1678
+     * @return bool TRUE if everything is fine compared to lexicon or lexicon does not exist
1679
+     */
1680
+    private function matchAndApplyLexiconDefinition(
1681
+        string $app,
1682
+        string &$key,
1683
+        ?bool &$lazy = null,
1684
+        int &$type = self::VALUE_MIXED,
1685
+        ?string &$default = null,
1686
+        ?Entry &$lexiconEntry = null,
1687
+    ): bool {
1688
+        if (in_array($key,
1689
+            [
1690
+                'enabled',
1691
+                'installed_version',
1692
+                'types',
1693
+            ])) {
1694
+            return true; // we don't break stuff for this list of config keys.
1695
+        }
1696
+        $configDetails = $this->getConfigDetailsFromLexicon($app);
1697
+        if (array_key_exists($key, $configDetails['aliases']) && !$this->ignoreLexiconAliases) {
1698
+            // in case '$rename' is set in ConfigLexiconEntry, we use the new config key
1699
+            $key = $configDetails['aliases'][$key];
1700
+        }
1701
+
1702
+        if (!array_key_exists($key, $configDetails['entries'])) {
1703
+            return $this->applyLexiconStrictness($configDetails['strictness'], $app . '/' . $key);
1704
+        }
1705
+
1706
+        // if lazy is NULL, we ignore all check on the type/lazyness/default from Lexicon
1707
+        if ($lazy === null) {
1708
+            return true;
1709
+        }
1710
+
1711
+        /** @var Entry $lexiconEntry */
1712
+        $lexiconEntry = $configDetails['entries'][$key];
1713
+        $type &= ~self::VALUE_SENSITIVE;
1714
+
1715
+        $appConfigValueType = $lexiconEntry->getValueType()->toAppConfigFlag();
1716
+        if ($type === self::VALUE_MIXED) {
1717
+            $type = $appConfigValueType; // we overwrite if value was requested as mixed
1718
+        } elseif ($appConfigValueType !== $type) {
1719
+            throw new AppConfigTypeConflictException('The app config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
1720
+        }
1721
+
1722
+        $lazy = $lexiconEntry->isLazy();
1723
+        // only look for default if needed, default from Lexicon got priority
1724
+        if ($default !== null) {
1725
+            $default = $lexiconEntry->getDefault($this->presetManager->getLexiconPreset()) ?? $default;
1726
+        }
1727
+
1728
+        if ($lexiconEntry->isFlagged(self::FLAG_SENSITIVE)) {
1729
+            $type |= self::VALUE_SENSITIVE;
1730
+        }
1731
+        if ($lexiconEntry->isDeprecated()) {
1732
+            $this->logger->notice('App config key ' . $app . '/' . $key . ' is set as deprecated.');
1733
+        }
1734
+
1735
+        return true;
1736
+    }
1737
+
1738
+    /**
1739
+     * manage ConfigLexicon behavior based on strictness set in IConfigLexicon
1740
+     *
1741
+     * @param Strictness|null $strictness
1742
+     * @param string $line
1743
+     *
1744
+     * @return bool TRUE if conflict can be fully ignored, FALSE if action should be not performed
1745
+     * @throws AppConfigUnknownKeyException if strictness implies exception
1746
+     * @see \OCP\Config\Lexicon\ILexicon::getStrictness()
1747
+     */
1748
+    private function applyLexiconStrictness(?Strictness $strictness, string $configAppKey): bool {
1749
+        if ($strictness === null) {
1750
+            return true;
1751
+        }
1752
+
1753
+        $line = 'The app config key ' . $configAppKey . ' is not defined in the config lexicon';
1754
+        switch ($strictness) {
1755
+            case Strictness::IGNORE:
1756
+                return true;
1757
+            case Strictness::NOTICE:
1758
+                if (!in_array($configAppKey, $this->strictnessApplied, true)) {
1759
+                    $this->strictnessApplied[] = $configAppKey;
1760
+                    $this->logger->notice($line);
1761
+                }
1762
+                return true;
1763
+            case Strictness::WARNING:
1764
+                if (!in_array($configAppKey, $this->strictnessApplied, true)) {
1765
+                    $this->strictnessApplied[] = $configAppKey;
1766
+                    $this->logger->warning($line);
1767
+                }
1768
+                return false;
1769
+        }
1770
+
1771
+        throw new AppConfigUnknownKeyException($line);
1772
+    }
1773
+
1774
+    /**
1775
+     * extract details from registered $appId's config lexicon
1776
+     *
1777
+     * @param string $appId
1778
+     * @internal
1779
+     *
1780
+     * @return array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}
1781
+     */
1782
+    public function getConfigDetailsFromLexicon(string $appId): array {
1783
+        if (!array_key_exists($appId, $this->configLexiconDetails)) {
1784
+            $entries = $aliases = [];
1785
+            $bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
1786
+            $configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
1787
+            foreach ($configLexicon?->getAppConfigs() ?? [] as $configEntry) {
1788
+                $entries[$configEntry->getKey()] = $configEntry;
1789
+                $newName = $configEntry->getRename();
1790
+                if ($newName !== null) {
1791
+                    $aliases[$newName] = $configEntry->getKey();
1792
+                }
1793
+            }
1794
+
1795
+            $this->configLexiconDetails[$appId] = [
1796
+                'entries' => $entries,
1797
+                'aliases' => $aliases,
1798
+                'strictness' => $configLexicon?->getStrictness() ?? Strictness::IGNORE
1799
+            ];
1800
+        }
1801
+
1802
+        return $this->configLexiconDetails[$appId];
1803
+    }
1804
+
1805
+    /**
1806
+     * get Lexicon Entry using appId and config key entry
1807
+     *
1808
+     * @return Entry|null NULL if entry does not exist in app's Lexicon
1809
+     * @internal
1810
+     */
1811
+    public function getLexiconEntry(string $appId, string $key): ?Entry {
1812
+        return $this->getConfigDetailsFromLexicon($appId)['entries'][$key] ?? null;
1813
+    }
1814
+
1815
+    /**
1816
+     * if set to TRUE, ignore aliases defined in Config Lexicon during the use of the methods of this class
1817
+     *
1818
+     * @internal
1819
+     */
1820
+    public function ignoreLexiconAliases(bool $ignore): void {
1821
+        $this->ignoreLexiconAliases = $ignore;
1822
+    }
1823
+
1824
+    /**
1825
+     * Returns the installed versions of all apps
1826
+     *
1827
+     * @return array<string, string>
1828
+     */
1829
+    public function getAppInstalledVersions(bool $onlyEnabled = false): array {
1830
+        if ($this->appVersionsCache === null) {
1831
+            /** @var array<string, string> */
1832
+            $this->appVersionsCache = $this->searchValues('installed_version', false, IAppConfig::VALUE_STRING);
1833
+        }
1834
+        if ($onlyEnabled) {
1835
+            return array_filter(
1836
+                $this->appVersionsCache,
1837
+                fn (string $app): bool => $this->getValueString($app, 'enabled', 'no') !== 'no',
1838
+                ARRAY_FILTER_USE_KEY
1839
+            );
1840
+        }
1841
+        return $this->appVersionsCache;
1842
+    }
1843
+
1844
+    private function clearLocalCache(): void {
1845
+        $this->localCache?->remove(self::LOCAL_CACHE_KEY);
1846
+    }
1847 1847
 }
Please login to merge, or discard this patch.
Spacing   +31 added lines, -31 removed lines patch added patch discarded remove patch
@@ -59,11 +59,11 @@  discard block
 block discarded – undo
59 59
 	private const LOCAL_CACHE_TTL = 3;
60 60
 
61 61
 	/** @var array<string, array<string, string>> ['app_id' => ['config_key' => 'config_value']] */
62
-	private array $fastCache = [];   // cache for normal config keys
62
+	private array $fastCache = []; // cache for normal config keys
63 63
 	/** @var array<string, array<string, string>> ['app_id' => ['config_key' => 'config_value']] */
64
-	private array $lazyCache = [];   // cache for lazy config keys
64
+	private array $lazyCache = []; // cache for lazy config keys
65 65
 	/** @var array<string, array<string, int>> ['app_id' => ['config_key' => bitflag]] */
66
-	private array $valueTypes = [];  // type for all config values
66
+	private array $valueTypes = []; // type for all config values
67 67
 	private bool $fastLoaded = false;
68 68
 	private bool $lazyLoaded = false;
69 69
 	/** @var array<string, array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
@@ -85,7 +85,7 @@  discard block
 block discarded – undo
85 85
 		readonly CacheFactory $cacheFactory,
86 86
 	) {
87 87
 		if ($config->getSystemValueBool('cache_app_config', true) && $cacheFactory->isLocalCacheAvailable()) {
88
-			$cacheFactory->withServerVersionPrefix(function (ICacheFactory $factory) {
88
+			$cacheFactory->withServerVersionPrefix(function(ICacheFactory $factory) {
89 89
 				$this->localCache = $factory->createLocal();
90 90
 			});
91 91
 		}
@@ -242,7 +242,7 @@  discard block
 block discarded – undo
242 242
 		$values = $this->formatAppValues($app, ($this->fastCache[$app] ?? []) + ($this->lazyCache[$app] ?? []));
243 243
 		$values = array_filter(
244 244
 			$values,
245
-			function (string $key) use ($prefix): bool {
245
+			function(string $key) use ($prefix): bool {
246 246
 				return str_starts_with($key, $prefix); // filter values based on $prefix
247 247
 			}, ARRAY_FILTER_USE_KEY
248 248
 		);
@@ -293,7 +293,7 @@  discard block
 block discarded – undo
293 293
 		$values = [];
294 294
 		foreach (array_keys($cache) as $app) {
295 295
 			if (isset($cache[$app][$key])) {
296
-				$values[$app] = $this->convertTypedValue($cache[$app][$key], $typedAs ?? $this->getValueType((string)$app, $key, $lazy));
296
+				$values[$app] = $this->convertTypedValue($cache[$app][$key], $typedAs ?? $this->getValueType((string) $app, $key, $lazy));
297 297
 			}
298 298
 		}
299 299
 
@@ -388,7 +388,7 @@  discard block
 block discarded – undo
388 388
 		int $default = 0,
389 389
 		bool $lazy = false,
390 390
 	): int {
391
-		return (int)$this->getTypedValue($app, $key, (string)$default, $lazy, self::VALUE_INT);
391
+		return (int) $this->getTypedValue($app, $key, (string) $default, $lazy, self::VALUE_INT);
392 392
 	}
393 393
 
394 394
 	/**
@@ -406,7 +406,7 @@  discard block
 block discarded – undo
406 406
 	 * @see IAppConfig for explanation about lazy loading
407 407
 	 */
408 408
 	public function getValueFloat(string $app, string $key, float $default = 0, bool $lazy = false): float {
409
-		return (float)$this->getTypedValue($app, $key, (string)$default, $lazy, self::VALUE_FLOAT);
409
+		return (float) $this->getTypedValue($app, $key, (string) $default, $lazy, self::VALUE_FLOAT);
410 410
 	}
411 411
 
412 412
 	/**
@@ -670,7 +670,7 @@  discard block
 block discarded – undo
670 670
 		return $this->setTypedValue(
671 671
 			$app,
672 672
 			$key,
673
-			(string)$value,
673
+			(string) $value,
674 674
 			$lazy,
675 675
 			self::VALUE_INT | ($sensitive ? self::VALUE_SENSITIVE : 0)
676 676
 		);
@@ -700,7 +700,7 @@  discard block
 block discarded – undo
700 700
 		return $this->setTypedValue(
701 701
 			$app,
702 702
 			$key,
703
-			(string)$value,
703
+			(string) $value,
704 704
 			$lazy,
705 705
 			self::VALUE_FLOAT | ($sensitive ? self::VALUE_SENSITIVE : 0)
706 706
 		);
@@ -805,7 +805,7 @@  discard block
 block discarded – undo
805 805
 
806 806
 		$origValue = $value;
807 807
 		if ($sensitive || ($this->hasKey($app, $key, $lazy) && $this->isSensitive($app, $key, $lazy))) {
808
-			$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
808
+			$value = self::ENCRYPTION_PREFIX.$this->crypto->encrypt($value);
809 809
 		}
810 810
 
811 811
 		if ($this->hasKey($app, $key, $lazy)) {
@@ -870,7 +870,7 @@  discard block
 block discarded – undo
870 870
 				} catch (AppConfigIncorrectTypeException) {
871 871
 					// can be ignored, this was just needed for a better exception message.
872 872
 				}
873
-				throw new AppConfigTypeConflictException('conflict between new type (' . $type . ') and old type (' . $currType . ')');
873
+				throw new AppConfigTypeConflictException('conflict between new type ('.$type.') and old type ('.$currType.')');
874 874
 			}
875 875
 
876 876
 			// we fix $type if the stored value, or the new value as it might be changed, is set as sensitive
@@ -1004,7 +1004,7 @@  discard block
 block discarded – undo
1004 1004
 		$value = $cache[$app][$key];
1005 1005
 		if ($sensitive) {
1006 1006
 			$type |= self::VALUE_SENSITIVE;
1007
-			$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
1007
+			$value = self::ENCRYPTION_PREFIX.$this->crypto->encrypt($value);
1008 1008
 		} else {
1009 1009
 			$value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
1010 1010
 		}
@@ -1085,7 +1085,7 @@  discard block
 block discarded – undo
1085 1085
 			$typeString = $this->convertTypeToString($type);
1086 1086
 		} catch (AppConfigIncorrectTypeException $e) {
1087 1087
 			$this->logger->warning('type stored in database is not correct', ['exception' => $e, 'type' => $type]);
1088
-			$typeString = (string)$type;
1088
+			$typeString = (string) $type;
1089 1089
 		}
1090 1090
 
1091 1091
 		if (!isset($cache[$app][$key])) {
@@ -1133,7 +1133,7 @@  discard block
 block discarded – undo
1133 1133
 		try {
1134 1134
 			$lazy = false;
1135 1135
 			$this->matchAndApplyLexiconDefinition($app, $key, $lazy, lexiconEntry: $lexiconEntry);
1136
-		} catch (AppConfigTypeConflictException|AppConfigUnknownKeyException) {
1136
+		} catch (AppConfigTypeConflictException | AppConfigUnknownKeyException) {
1137 1137
 			// can be ignored
1138 1138
 		}
1139 1139
 
@@ -1167,7 +1167,7 @@  discard block
 block discarded – undo
1167 1167
 			'float' => IAppConfig::VALUE_FLOAT,
1168 1168
 			'boolean' => IAppConfig::VALUE_BOOL,
1169 1169
 			'array' => IAppConfig::VALUE_ARRAY,
1170
-			default => throw new AppConfigIncorrectTypeException('Unknown type ' . $type)
1170
+			default => throw new AppConfigIncorrectTypeException('Unknown type '.$type)
1171 1171
 		};
1172 1172
 	}
1173 1173
 
@@ -1188,7 +1188,7 @@  discard block
 block discarded – undo
1188 1188
 			IAppConfig::VALUE_FLOAT => 'float',
1189 1189
 			IAppConfig::VALUE_BOOL => 'boolean',
1190 1190
 			IAppConfig::VALUE_ARRAY => 'array',
1191
-			default => throw new AppConfigIncorrectTypeException('Unknown numeric type ' . $type)
1191
+			default => throw new AppConfigIncorrectTypeException('Unknown numeric type '.$type)
1192 1192
 		};
1193 1193
 	}
1194 1194
 
@@ -1297,11 +1297,11 @@  discard block
 block discarded – undo
1297 1297
 		}
1298 1298
 		if (strlen($app) > self::APP_MAX_LENGTH) {
1299 1299
 			throw new InvalidArgumentException(
1300
-				'Value (' . $app . ') for app is too long (' . self::APP_MAX_LENGTH . ')'
1300
+				'Value ('.$app.') for app is too long ('.self::APP_MAX_LENGTH.')'
1301 1301
 			);
1302 1302
 		}
1303 1303
 		if (strlen($configKey) > self::KEY_MAX_LENGTH) {
1304
-			throw new InvalidArgumentException('Value (' . $configKey . ') for key is too long (' . self::KEY_MAX_LENGTH . ')');
1304
+			throw new InvalidArgumentException('Value ('.$configKey.') for key is too long ('.self::KEY_MAX_LENGTH.')');
1305 1305
 		}
1306 1306
 		if ($valueType > -1) {
1307 1307
 			$valueType &= ~self::VALUE_SENSITIVE;
@@ -1323,7 +1323,7 @@  discard block
 block discarded – undo
1323 1323
 
1324 1324
 		// if lazy is null or true, we debug log
1325 1325
 		if ($lazy === true && $app !== null) {
1326
-			$exception = new \RuntimeException('The loading of lazy AppConfig values have been triggered by app "' . $app . '"');
1326
+			$exception = new \RuntimeException('The loading of lazy AppConfig values have been triggered by app "'.$app.'"');
1327 1327
 			$this->logger->debug($exception->getMessage(), ['exception' => $exception, 'app' => $app]);
1328 1328
 		}
1329 1329
 
@@ -1361,12 +1361,12 @@  discard block
 block discarded – undo
1361 1361
 		$rows = $result->fetchAll();
1362 1362
 		foreach ($rows as $row) {
1363 1363
 			// most of the time, 'lazy' is not in the select because its value is already known
1364
-			if ($lazy && ((int)$row['lazy']) === 1) {
1364
+			if ($lazy && ((int) $row['lazy']) === 1) {
1365 1365
 				$this->lazyCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1366 1366
 			} else {
1367 1367
 				$this->fastCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1368 1368
 			}
1369
-			$this->valueTypes[$row['appid']][$row['configkey']] = (int)($row['type'] ?? 0);
1369
+			$this->valueTypes[$row['appid']][$row['configkey']] = (int) ($row['type'] ?? 0);
1370 1370
 		}
1371 1371
 
1372 1372
 		$result->closeCursor();
@@ -1433,7 +1433,7 @@  discard block
 block discarded – undo
1433 1433
 		 * this solution would remove the loading of config values from disabled app
1434 1434
 		 * unless calling the method.
1435 1435
 		 */
1436
-		return $this->setTypedValue($app, $key, (string)$value, false, self::VALUE_MIXED);
1436
+		return $this->setTypedValue($app, $key, (string) $value, false, self::VALUE_MIXED);
1437 1437
 	}
1438 1438
 
1439 1439
 
@@ -1504,12 +1504,12 @@  discard block
 block discarded – undo
1504 1504
 	 *
1505 1505
 	 * @return string|int|float|bool|array
1506 1506
 	 */
1507
-	private function convertTypedValue(string $value, int $type): string|int|float|bool|array {
1507
+	private function convertTypedValue(string $value, int $type): string | int | float | bool | array {
1508 1508
 		switch ($type) {
1509 1509
 			case self::VALUE_INT:
1510
-				return (int)$value;
1510
+				return (int) $value;
1511 1511
 			case self::VALUE_FLOAT:
1512
-				return (float)$value;
1512
+				return (float) $value;
1513 1513
 			case self::VALUE_BOOL:
1514 1514
 				return in_array(strtolower($value), ['1', 'true', 'yes', 'on']);
1515 1515
 			case self::VALUE_ARRAY:
@@ -1683,7 +1683,7 @@  discard block
 block discarded – undo
1683 1683
 		?bool &$lazy = null,
1684 1684
 		int &$type = self::VALUE_MIXED,
1685 1685
 		?string &$default = null,
1686
-		?Entry &$lexiconEntry = null,
1686
+		?Entry & $lexiconEntry = null,
1687 1687
 	): bool {
1688 1688
 		if (in_array($key,
1689 1689
 			[
@@ -1700,7 +1700,7 @@  discard block
 block discarded – undo
1700 1700
 		}
1701 1701
 
1702 1702
 		if (!array_key_exists($key, $configDetails['entries'])) {
1703
-			return $this->applyLexiconStrictness($configDetails['strictness'], $app . '/' . $key);
1703
+			return $this->applyLexiconStrictness($configDetails['strictness'], $app.'/'.$key);
1704 1704
 		}
1705 1705
 
1706 1706
 		// if lazy is NULL, we ignore all check on the type/lazyness/default from Lexicon
@@ -1716,7 +1716,7 @@  discard block
 block discarded – undo
1716 1716
 		if ($type === self::VALUE_MIXED) {
1717 1717
 			$type = $appConfigValueType; // we overwrite if value was requested as mixed
1718 1718
 		} elseif ($appConfigValueType !== $type) {
1719
-			throw new AppConfigTypeConflictException('The app config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
1719
+			throw new AppConfigTypeConflictException('The app config key '.$app.'/'.$key.' is typed incorrectly in relation to the config lexicon');
1720 1720
 		}
1721 1721
 
1722 1722
 		$lazy = $lexiconEntry->isLazy();
@@ -1729,7 +1729,7 @@  discard block
 block discarded – undo
1729 1729
 			$type |= self::VALUE_SENSITIVE;
1730 1730
 		}
1731 1731
 		if ($lexiconEntry->isDeprecated()) {
1732
-			$this->logger->notice('App config key ' . $app . '/' . $key . ' is set as deprecated.');
1732
+			$this->logger->notice('App config key '.$app.'/'.$key.' is set as deprecated.');
1733 1733
 		}
1734 1734
 
1735 1735
 		return true;
@@ -1750,7 +1750,7 @@  discard block
 block discarded – undo
1750 1750
 			return true;
1751 1751
 		}
1752 1752
 
1753
-		$line = 'The app config key ' . $configAppKey . ' is not defined in the config lexicon';
1753
+		$line = 'The app config key '.$configAppKey.' is not defined in the config lexicon';
1754 1754
 		switch ($strictness) {
1755 1755
 			case Strictness::IGNORE:
1756 1756
 				return true;
Please login to merge, or discard this patch.