Completed
Push — master ( 852891...cd06b2 )
by Maxence
25:09 queued 01:30
created
lib/private/AppConfig.php 1 patch
Indentation   +1631 added lines, -1631 removed lines patch added patch discarded remove patch
@@ -46,1635 +46,1635 @@
 block discarded – undo
46 46
  * @since 29.0.0 - Supporting types and lazy loading
47 47
  */
48 48
 class AppConfig implements IAppConfig {
49
-	private const APP_MAX_LENGTH = 32;
50
-	private const KEY_MAX_LENGTH = 64;
51
-	private const ENCRYPTION_PREFIX = '$AppConfigEncryption$';
52
-	private const ENCRYPTION_PREFIX_LENGTH = 21; // strlen(self::ENCRYPTION_PREFIX)
53
-
54
-	/** @var array<string, array<string, mixed>> ['app_id' => ['config_key' => 'config_value']] */
55
-	private array $fastCache = [];   // cache for normal config keys
56
-	/** @var array<string, array<string, mixed>> ['app_id' => ['config_key' => 'config_value']] */
57
-	private array $lazyCache = [];   // cache for lazy config keys
58
-	/** @var array<string, array<string, int>> ['app_id' => ['config_key' => bitflag]] */
59
-	private array $valueTypes = [];  // type for all config values
60
-	private bool $fastLoaded = false;
61
-	private bool $lazyLoaded = false;
62
-	/** @var array<array-key, array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
63
-	private array $configLexiconDetails = [];
64
-
65
-	/** @var ?array<string, string> */
66
-	private ?array $appVersionsCache = null;
67
-
68
-	public function __construct(
69
-		protected IDBConnection $connection,
70
-		protected LoggerInterface $logger,
71
-		protected ICrypto $crypto,
72
-	) {
73
-	}
74
-
75
-	/**
76
-	 * @inheritDoc
77
-	 *
78
-	 * @return list<string> list of app ids
79
-	 * @since 7.0.0
80
-	 */
81
-	public function getApps(): array {
82
-		$this->loadConfigAll();
83
-		$apps = array_merge(array_keys($this->fastCache), array_keys($this->lazyCache));
84
-		sort($apps);
85
-
86
-		return array_values(array_unique($apps));
87
-	}
88
-
89
-	/**
90
-	 * @inheritDoc
91
-	 *
92
-	 * @param string $app id of the app
93
-	 *
94
-	 * @return list<string> list of stored config keys
95
-	 * @since 29.0.0
96
-	 */
97
-	public function getKeys(string $app): array {
98
-		$this->assertParams($app);
99
-		$this->loadConfigAll($app);
100
-		$keys = array_merge(array_keys($this->fastCache[$app] ?? []), array_keys($this->lazyCache[$app] ?? []));
101
-		sort($keys);
102
-
103
-		return array_values(array_unique($keys));
104
-	}
105
-
106
-	/**
107
-	 * @inheritDoc
108
-	 *
109
-	 * @param string $app id of the app
110
-	 * @param string $key config key
111
-	 * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
112
-	 *
113
-	 * @return bool TRUE if key exists
114
-	 * @since 7.0.0
115
-	 * @since 29.0.0 Added the $lazy argument
116
-	 */
117
-	public function hasKey(string $app, string $key, ?bool $lazy = false): bool {
118
-		$this->assertParams($app, $key);
119
-		$this->loadConfig($app, $lazy);
120
-
121
-		if ($lazy === null) {
122
-			$appCache = $this->getAllValues($app);
123
-			return isset($appCache[$key]);
124
-		}
125
-
126
-		if ($lazy) {
127
-			return isset($this->lazyCache[$app][$key]);
128
-		}
129
-
130
-		return isset($this->fastCache[$app][$key]);
131
-	}
132
-
133
-	/**
134
-	 * @param string $app id of the app
135
-	 * @param string $key config key
136
-	 * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
137
-	 *
138
-	 * @return bool
139
-	 * @throws AppConfigUnknownKeyException if config key is not known
140
-	 * @since 29.0.0
141
-	 */
142
-	public function isSensitive(string $app, string $key, ?bool $lazy = false): bool {
143
-		$this->assertParams($app, $key);
144
-		$this->loadConfig(null, $lazy);
145
-
146
-		if (!isset($this->valueTypes[$app][$key])) {
147
-			throw new AppConfigUnknownKeyException('unknown config key');
148
-		}
149
-
150
-		return $this->isTyped(self::VALUE_SENSITIVE, $this->valueTypes[$app][$key]);
151
-	}
152
-
153
-	/**
154
-	 * @inheritDoc
155
-	 *
156
-	 * @param string $app if of the app
157
-	 * @param string $key config key
158
-	 *
159
-	 * @return bool TRUE if config is lazy loaded
160
-	 * @throws AppConfigUnknownKeyException if config key is not known
161
-	 * @see IAppConfig for details about lazy loading
162
-	 * @since 29.0.0
163
-	 */
164
-	public function isLazy(string $app, string $key): bool {
165
-		// there is a huge probability the non-lazy config are already loaded
166
-		if ($this->hasKey($app, $key, false)) {
167
-			return false;
168
-		}
169
-
170
-		// key not found, we search in the lazy config
171
-		if ($this->hasKey($app, $key, true)) {
172
-			return true;
173
-		}
174
-
175
-		throw new AppConfigUnknownKeyException('unknown config key');
176
-	}
177
-
178
-
179
-	/**
180
-	 * @inheritDoc
181
-	 *
182
-	 * @param string $app id of the app
183
-	 * @param string $prefix config keys prefix to search
184
-	 * @param bool $filtered TRUE to hide sensitive config values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
185
-	 *
186
-	 * @return array<string, string|int|float|bool|array> [configKey => configValue]
187
-	 * @since 29.0.0
188
-	 */
189
-	public function getAllValues(string $app, string $prefix = '', bool $filtered = false): array {
190
-		$this->assertParams($app, $prefix);
191
-		// if we want to filter values, we need to get sensitivity
192
-		$this->loadConfigAll($app);
193
-		// array_merge() will remove numeric keys (here config keys), so addition arrays instead
194
-		$values = $this->formatAppValues($app, ($this->fastCache[$app] ?? []) + ($this->lazyCache[$app] ?? []));
195
-		$values = array_filter(
196
-			$values,
197
-			function (string $key) use ($prefix): bool {
198
-				return str_starts_with($key, $prefix); // filter values based on $prefix
199
-			}, ARRAY_FILTER_USE_KEY
200
-		);
201
-
202
-		if (!$filtered) {
203
-			return $values;
204
-		}
205
-
206
-		/**
207
-		 * Using the old (deprecated) list of sensitive values.
208
-		 */
209
-		foreach ($this->getSensitiveKeys($app) as $sensitiveKeyExp) {
210
-			$sensitiveKeys = preg_grep($sensitiveKeyExp, array_keys($values));
211
-			foreach ($sensitiveKeys as $sensitiveKey) {
212
-				$this->valueTypes[$app][$sensitiveKey] = ($this->valueTypes[$app][$sensitiveKey] ?? 0) | self::VALUE_SENSITIVE;
213
-			}
214
-		}
215
-
216
-		$result = [];
217
-		foreach ($values as $key => $value) {
218
-			$result[$key] = $this->isTyped(self::VALUE_SENSITIVE, $this->valueTypes[$app][$key] ?? 0) ? IConfig::SENSITIVE_VALUE : $value;
219
-		}
220
-
221
-		return $result;
222
-	}
223
-
224
-	/**
225
-	 * @inheritDoc
226
-	 *
227
-	 * @param string $key config key
228
-	 * @param bool $lazy search within lazy loaded config
229
-	 * @param int|null $typedAs enforce type for the returned values ({@see self::VALUE_STRING} and others)
230
-	 *
231
-	 * @return array<string, string|int|float|bool|array> [appId => configValue]
232
-	 * @since 29.0.0
233
-	 */
234
-	public function searchValues(string $key, bool $lazy = false, ?int $typedAs = null): array {
235
-		$this->assertParams('', $key, true);
236
-		$this->loadConfig(null, $lazy);
237
-
238
-		/** @var array<array-key, array<array-key, mixed>> $cache */
239
-		if ($lazy) {
240
-			$cache = $this->lazyCache;
241
-		} else {
242
-			$cache = $this->fastCache;
243
-		}
244
-
245
-		$values = [];
246
-		foreach (array_keys($cache) as $app) {
247
-			if (isset($cache[$app][$key])) {
248
-				$values[$app] = $this->convertTypedValue($cache[$app][$key], $typedAs ?? $this->getValueType((string)$app, $key, $lazy));
249
-			}
250
-		}
251
-
252
-		return $values;
253
-	}
254
-
255
-
256
-	/**
257
-	 * Get the config value as string.
258
-	 * If the value does not exist the given default will be returned.
259
-	 *
260
-	 * Set lazy to `null` to ignore it and get the value from either source.
261
-	 *
262
-	 * **WARNING:** Method is internal and **SHOULD** not be used, as it is better to get the value with a type.
263
-	 *
264
-	 * @param string $app id of the app
265
-	 * @param string $key config key
266
-	 * @param string $default config value
267
-	 * @param null|bool $lazy get config as lazy loaded or not. can be NULL
268
-	 *
269
-	 * @return string the value or $default
270
-	 * @internal
271
-	 * @since 29.0.0
272
-	 * @see IAppConfig for explanation about lazy loading
273
-	 * @see getValueString()
274
-	 * @see getValueInt()
275
-	 * @see getValueFloat()
276
-	 * @see getValueBool()
277
-	 * @see getValueArray()
278
-	 */
279
-	public function getValueMixed(
280
-		string $app,
281
-		string $key,
282
-		string $default = '',
283
-		?bool $lazy = false,
284
-	): string {
285
-		try {
286
-			$lazy = ($lazy === null) ? $this->isLazy($app, $key) : $lazy;
287
-		} catch (AppConfigUnknownKeyException $e) {
288
-			return $default;
289
-		}
290
-
291
-		return $this->getTypedValue(
292
-			$app,
293
-			$key,
294
-			$default,
295
-			$lazy,
296
-			self::VALUE_MIXED
297
-		);
298
-	}
299
-
300
-	/**
301
-	 * @inheritDoc
302
-	 *
303
-	 * @param string $app id of the app
304
-	 * @param string $key config key
305
-	 * @param string $default default value
306
-	 * @param bool $lazy search within lazy loaded config
307
-	 *
308
-	 * @return string stored config value or $default if not set in database
309
-	 * @throws InvalidArgumentException if one of the argument format is invalid
310
-	 * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
311
-	 * @since 29.0.0
312
-	 * @see IAppConfig for explanation about lazy loading
313
-	 */
314
-	public function getValueString(
315
-		string $app,
316
-		string $key,
317
-		string $default = '',
318
-		bool $lazy = false,
319
-	): string {
320
-		return $this->getTypedValue($app, $key, $default, $lazy, self::VALUE_STRING);
321
-	}
322
-
323
-	/**
324
-	 * @inheritDoc
325
-	 *
326
-	 * @param string $app id of the app
327
-	 * @param string $key config key
328
-	 * @param int $default default value
329
-	 * @param bool $lazy search within lazy loaded config
330
-	 *
331
-	 * @return int stored config value or $default if not set in database
332
-	 * @throws InvalidArgumentException if one of the argument format is invalid
333
-	 * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
334
-	 * @since 29.0.0
335
-	 * @see IAppConfig for explanation about lazy loading
336
-	 */
337
-	public function getValueInt(
338
-		string $app,
339
-		string $key,
340
-		int $default = 0,
341
-		bool $lazy = false,
342
-	): int {
343
-		return (int)$this->getTypedValue($app, $key, (string)$default, $lazy, self::VALUE_INT);
344
-	}
345
-
346
-	/**
347
-	 * @inheritDoc
348
-	 *
349
-	 * @param string $app id of the app
350
-	 * @param string $key config key
351
-	 * @param float $default default value
352
-	 * @param bool $lazy search within lazy loaded config
353
-	 *
354
-	 * @return float stored config value or $default if not set in database
355
-	 * @throws InvalidArgumentException if one of the argument format is invalid
356
-	 * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
357
-	 * @since 29.0.0
358
-	 * @see IAppConfig for explanation about lazy loading
359
-	 */
360
-	public function getValueFloat(string $app, string $key, float $default = 0, bool $lazy = false): float {
361
-		return (float)$this->getTypedValue($app, $key, (string)$default, $lazy, self::VALUE_FLOAT);
362
-	}
363
-
364
-	/**
365
-	 * @inheritDoc
366
-	 *
367
-	 * @param string $app id of the app
368
-	 * @param string $key config key
369
-	 * @param bool $default default value
370
-	 * @param bool $lazy search within lazy loaded config
371
-	 *
372
-	 * @return bool stored config value or $default if not set in database
373
-	 * @throws InvalidArgumentException if one of the argument format is invalid
374
-	 * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
375
-	 * @since 29.0.0
376
-	 * @see IAppConfig for explanation about lazy loading
377
-	 */
378
-	public function getValueBool(string $app, string $key, bool $default = false, bool $lazy = false): bool {
379
-		$b = strtolower($this->getTypedValue($app, $key, $default ? 'true' : 'false', $lazy, self::VALUE_BOOL));
380
-		return in_array($b, ['1', 'true', 'yes', 'on']);
381
-	}
382
-
383
-	/**
384
-	 * @inheritDoc
385
-	 *
386
-	 * @param string $app id of the app
387
-	 * @param string $key config key
388
-	 * @param array $default default value
389
-	 * @param bool $lazy search within lazy loaded config
390
-	 *
391
-	 * @return array stored config value or $default if not set in database
392
-	 * @throws InvalidArgumentException if one of the argument format is invalid
393
-	 * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
394
-	 * @since 29.0.0
395
-	 * @see IAppConfig for explanation about lazy loading
396
-	 */
397
-	public function getValueArray(
398
-		string $app,
399
-		string $key,
400
-		array $default = [],
401
-		bool $lazy = false,
402
-	): array {
403
-		try {
404
-			$defaultJson = json_encode($default, JSON_THROW_ON_ERROR);
405
-			$value = json_decode($this->getTypedValue($app, $key, $defaultJson, $lazy, self::VALUE_ARRAY), true, flags: JSON_THROW_ON_ERROR);
406
-
407
-			return is_array($value) ? $value : [];
408
-		} catch (JsonException) {
409
-			return [];
410
-		}
411
-	}
412
-
413
-	/**
414
-	 * @param string $app id of the app
415
-	 * @param string $key config key
416
-	 * @param string $default default value
417
-	 * @param bool $lazy search within lazy loaded config
418
-	 * @param int $type value type {@see VALUE_STRING} {@see VALUE_INT}{@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
419
-	 *
420
-	 * @return string
421
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
422
-	 * @throws InvalidArgumentException
423
-	 */
424
-	private function getTypedValue(
425
-		string $app,
426
-		string $key,
427
-		string $default,
428
-		bool $lazy,
429
-		int $type,
430
-	): string {
431
-		$this->assertParams($app, $key, valueType: $type);
432
-		if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $default)) {
433
-			return $default; // returns default if strictness of lexicon is set to WARNING (block and report)
434
-		}
435
-		$this->loadConfig($app, $lazy);
436
-
437
-		/**
438
-		 * We ignore check if mixed type is requested.
439
-		 * If type of stored value is set as mixed, we don't filter.
440
-		 * If type of stored value is defined, we compare with the one requested.
441
-		 */
442
-		$knownType = $this->valueTypes[$app][$key] ?? 0;
443
-		if (!$this->isTyped(self::VALUE_MIXED, $type)
444
-			&& $knownType > 0
445
-			&& !$this->isTyped(self::VALUE_MIXED, $knownType)
446
-			&& !$this->isTyped($type, $knownType)) {
447
-			$this->logger->warning('conflict with value type from database', ['app' => $app, 'key' => $key, 'type' => $type, 'knownType' => $knownType]);
448
-			throw new AppConfigTypeConflictException('conflict with value type from database');
449
-		}
450
-
451
-		/**
452
-		 * - the pair $app/$key cannot exist in both array,
453
-		 * - we should still return an existing non-lazy value even if current method
454
-		 *   is called with $lazy is true
455
-		 *
456
-		 * This way, lazyCache will be empty until the load for lazy config value is requested.
457
-		 */
458
-		if (isset($this->lazyCache[$app][$key])) {
459
-			$value = $this->lazyCache[$app][$key];
460
-		} elseif (isset($this->fastCache[$app][$key])) {
461
-			$value = $this->fastCache[$app][$key];
462
-		} else {
463
-			return $default;
464
-		}
465
-
466
-		$sensitive = $this->isTyped(self::VALUE_SENSITIVE, $knownType);
467
-		if ($sensitive && str_starts_with($value, self::ENCRYPTION_PREFIX)) {
468
-			// Only decrypt values that are stored encrypted
469
-			$value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
470
-		}
471
-
472
-		return $value;
473
-	}
474
-
475
-	/**
476
-	 * @inheritDoc
477
-	 *
478
-	 * @param string $app id of the app
479
-	 * @param string $key config key
480
-	 *
481
-	 * @return int type of the value
482
-	 * @throws AppConfigUnknownKeyException if config key is not known
483
-	 * @since 29.0.0
484
-	 * @see VALUE_STRING
485
-	 * @see VALUE_INT
486
-	 * @see VALUE_FLOAT
487
-	 * @see VALUE_BOOL
488
-	 * @see VALUE_ARRAY
489
-	 */
490
-	public function getValueType(string $app, string $key, ?bool $lazy = null): int {
491
-		$type = self::VALUE_MIXED;
492
-		$ignorable = $lazy ?? false;
493
-		$this->matchAndApplyLexiconDefinition($app, $key, $ignorable, $type);
494
-		if ($type !== self::VALUE_MIXED) {
495
-			// a modified $type means config key is set in Lexicon
496
-			return $type;
497
-		}
498
-
499
-		$this->assertParams($app, $key);
500
-		$this->loadConfig($app, $lazy);
501
-
502
-		if (!isset($this->valueTypes[$app][$key])) {
503
-			throw new AppConfigUnknownKeyException('unknown config key');
504
-		}
505
-
506
-		$type = $this->valueTypes[$app][$key];
507
-		$type &= ~self::VALUE_SENSITIVE;
508
-		return $type;
509
-	}
510
-
511
-
512
-	/**
513
-	 * Store a config key and its value in database as VALUE_MIXED
514
-	 *
515
-	 * **WARNING:** Method is internal and **MUST** not be used as it is best to set a real value type
516
-	 *
517
-	 * @param string $app id of the app
518
-	 * @param string $key config key
519
-	 * @param string $value config value
520
-	 * @param bool $lazy set config as lazy loaded
521
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
522
-	 *
523
-	 * @return bool TRUE if value was different, therefor updated in database
524
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED
525
-	 * @internal
526
-	 * @since 29.0.0
527
-	 * @see IAppConfig for explanation about lazy loading
528
-	 * @see setValueString()
529
-	 * @see setValueInt()
530
-	 * @see setValueFloat()
531
-	 * @see setValueBool()
532
-	 * @see setValueArray()
533
-	 */
534
-	public function setValueMixed(
535
-		string $app,
536
-		string $key,
537
-		string $value,
538
-		bool $lazy = false,
539
-		bool $sensitive = false,
540
-	): bool {
541
-		return $this->setTypedValue(
542
-			$app,
543
-			$key,
544
-			$value,
545
-			$lazy,
546
-			self::VALUE_MIXED | ($sensitive ? self::VALUE_SENSITIVE : 0)
547
-		);
548
-	}
549
-
550
-
551
-	/**
552
-	 * @inheritDoc
553
-	 *
554
-	 * @param string $app id of the app
555
-	 * @param string $key config key
556
-	 * @param string $value config value
557
-	 * @param bool $lazy set config as lazy loaded
558
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
559
-	 *
560
-	 * @return bool TRUE if value was different, therefor updated in database
561
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
562
-	 * @since 29.0.0
563
-	 * @see IAppConfig for explanation about lazy loading
564
-	 */
565
-	public function setValueString(
566
-		string $app,
567
-		string $key,
568
-		string $value,
569
-		bool $lazy = false,
570
-		bool $sensitive = false,
571
-	): bool {
572
-		return $this->setTypedValue(
573
-			$app,
574
-			$key,
575
-			$value,
576
-			$lazy,
577
-			self::VALUE_STRING | ($sensitive ? self::VALUE_SENSITIVE : 0)
578
-		);
579
-	}
580
-
581
-	/**
582
-	 * @inheritDoc
583
-	 *
584
-	 * @param string $app id of the app
585
-	 * @param string $key config key
586
-	 * @param int $value config value
587
-	 * @param bool $lazy set config as lazy loaded
588
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
589
-	 *
590
-	 * @return bool TRUE if value was different, therefor updated in database
591
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
592
-	 * @since 29.0.0
593
-	 * @see IAppConfig for explanation about lazy loading
594
-	 */
595
-	public function setValueInt(
596
-		string $app,
597
-		string $key,
598
-		int $value,
599
-		bool $lazy = false,
600
-		bool $sensitive = false,
601
-	): bool {
602
-		if ($value > 2000000000) {
603
-			$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.');
604
-		}
605
-
606
-		return $this->setTypedValue(
607
-			$app,
608
-			$key,
609
-			(string)$value,
610
-			$lazy,
611
-			self::VALUE_INT | ($sensitive ? self::VALUE_SENSITIVE : 0)
612
-		);
613
-	}
614
-
615
-	/**
616
-	 * @inheritDoc
617
-	 *
618
-	 * @param string $app id of the app
619
-	 * @param string $key config key
620
-	 * @param float $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 setValueFloat(
630
-		string $app,
631
-		string $key,
632
-		float $value,
633
-		bool $lazy = false,
634
-		bool $sensitive = false,
635
-	): bool {
636
-		return $this->setTypedValue(
637
-			$app,
638
-			$key,
639
-			(string)$value,
640
-			$lazy,
641
-			self::VALUE_FLOAT | ($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 bool $value config value
651
-	 * @param bool $lazy set config as lazy loaded
652
-	 *
653
-	 * @return bool TRUE if value was different, therefor updated in database
654
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
655
-	 * @since 29.0.0
656
-	 * @see IAppConfig for explanation about lazy loading
657
-	 */
658
-	public function setValueBool(
659
-		string $app,
660
-		string $key,
661
-		bool $value,
662
-		bool $lazy = false,
663
-	): bool {
664
-		return $this->setTypedValue(
665
-			$app,
666
-			$key,
667
-			($value) ? '1' : '0',
668
-			$lazy,
669
-			self::VALUE_BOOL
670
-		);
671
-	}
672
-
673
-	/**
674
-	 * @inheritDoc
675
-	 *
676
-	 * @param string $app id of the app
677
-	 * @param string $key config key
678
-	 * @param array $value config value
679
-	 * @param bool $lazy set config as lazy loaded
680
-	 * @param bool $sensitive if TRUE value will be hidden when listing config values.
681
-	 *
682
-	 * @return bool TRUE if value was different, therefor updated in database
683
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
684
-	 * @throws JsonException
685
-	 * @since 29.0.0
686
-	 * @see IAppConfig for explanation about lazy loading
687
-	 */
688
-	public function setValueArray(
689
-		string $app,
690
-		string $key,
691
-		array $value,
692
-		bool $lazy = false,
693
-		bool $sensitive = false,
694
-	): bool {
695
-		try {
696
-			return $this->setTypedValue(
697
-				$app,
698
-				$key,
699
-				json_encode($value, JSON_THROW_ON_ERROR),
700
-				$lazy,
701
-				self::VALUE_ARRAY | ($sensitive ? self::VALUE_SENSITIVE : 0)
702
-			);
703
-		} catch (JsonException $e) {
704
-			$this->logger->warning('could not setValueArray', ['app' => $app, 'key' => $key, 'exception' => $e]);
705
-			throw $e;
706
-		}
707
-	}
708
-
709
-	/**
710
-	 * Store a config key and its value in database
711
-	 *
712
-	 * If config key is already known with the exact same config value and same sensitive/lazy status, the
713
-	 * database is not updated. If config value was previously stored as sensitive, status will not be
714
-	 * altered.
715
-	 *
716
-	 * @param string $app id of the app
717
-	 * @param string $key config key
718
-	 * @param string $value config value
719
-	 * @param bool $lazy config set as lazy loaded
720
-	 * @param int $type value type {@see VALUE_STRING} {@see VALUE_INT} {@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
721
-	 *
722
-	 * @return bool TRUE if value was updated in database
723
-	 * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
724
-	 * @see IAppConfig for explanation about lazy loading
725
-	 */
726
-	private function setTypedValue(
727
-		string $app,
728
-		string $key,
729
-		string $value,
730
-		bool $lazy,
731
-		int $type,
732
-	): bool {
733
-		$this->assertParams($app, $key);
734
-		if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type)) {
735
-			return false; // returns false as database is not updated
736
-		}
737
-		$this->loadConfig(null, $lazy);
738
-
739
-		$sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type);
740
-		$inserted = $refreshCache = false;
741
-
742
-		$origValue = $value;
743
-		if ($sensitive || ($this->hasKey($app, $key, $lazy) && $this->isSensitive($app, $key, $lazy))) {
744
-			$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
745
-		}
746
-
747
-		if ($this->hasKey($app, $key, $lazy)) {
748
-			/**
749
-			 * no update if key is already known with set lazy status and value is
750
-			 * not different, unless sensitivity is switched from false to true.
751
-			 */
752
-			if ($origValue === $this->getTypedValue($app, $key, $value, $lazy, $type)
753
-				&& (!$sensitive || $this->isSensitive($app, $key, $lazy))) {
754
-				return false;
755
-			}
756
-		} else {
757
-			/**
758
-			 * if key is not known yet, we try to insert.
759
-			 * It might fail if the key exists with a different lazy flag.
760
-			 */
761
-			try {
762
-				$insert = $this->connection->getQueryBuilder();
763
-				$insert->insert('appconfig')
764
-					->setValue('appid', $insert->createNamedParameter($app))
765
-					->setValue('lazy', $insert->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
766
-					->setValue('type', $insert->createNamedParameter($type, IQueryBuilder::PARAM_INT))
767
-					->setValue('configkey', $insert->createNamedParameter($key))
768
-					->setValue('configvalue', $insert->createNamedParameter($value));
769
-				$insert->executeStatement();
770
-				$inserted = true;
771
-			} catch (DBException $e) {
772
-				if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
773
-					throw $e; // TODO: throw exception or just log and returns false !?
774
-				}
775
-			}
776
-		}
777
-
778
-		/**
779
-		 * We cannot insert a new row, meaning we need to update an already existing one
780
-		 */
781
-		if (!$inserted) {
782
-			$currType = $this->valueTypes[$app][$key] ?? 0;
783
-			if ($currType === 0) { // this might happen when switching lazy loading status
784
-				$this->loadConfigAll();
785
-				$currType = $this->valueTypes[$app][$key] ?? 0;
786
-			}
787
-
788
-			/**
789
-			 * This should only happen during the upgrade process from 28 to 29.
790
-			 * We only log a warning and set it to VALUE_MIXED.
791
-			 */
792
-			if ($currType === 0) {
793
-				$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]);
794
-				$currType = self::VALUE_MIXED;
795
-			}
796
-
797
-			/**
798
-			 * we only accept a different type from the one stored in database
799
-			 * if the one stored in database is not-defined (VALUE_MIXED)
800
-			 */
801
-			if (!$this->isTyped(self::VALUE_MIXED, $currType) &&
802
-				($type | self::VALUE_SENSITIVE) !== ($currType | self::VALUE_SENSITIVE)) {
803
-				try {
804
-					$currType = $this->convertTypeToString($currType);
805
-					$type = $this->convertTypeToString($type);
806
-				} catch (AppConfigIncorrectTypeException) {
807
-					// can be ignored, this was just needed for a better exception message.
808
-				}
809
-				throw new AppConfigTypeConflictException('conflict between new type (' . $type . ') and old type (' . $currType . ')');
810
-			}
811
-
812
-			// we fix $type if the stored value, or the new value as it might be changed, is set as sensitive
813
-			if ($sensitive || $this->isTyped(self::VALUE_SENSITIVE, $currType)) {
814
-				$type |= self::VALUE_SENSITIVE;
815
-			}
816
-
817
-			if ($lazy !== $this->isLazy($app, $key)) {
818
-				$refreshCache = true;
819
-			}
820
-
821
-			$update = $this->connection->getQueryBuilder();
822
-			$update->update('appconfig')
823
-				->set('configvalue', $update->createNamedParameter($value))
824
-				->set('lazy', $update->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
825
-				->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
826
-				->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
827
-				->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
828
-
829
-			$update->executeStatement();
830
-		}
831
-
832
-		if ($refreshCache) {
833
-			$this->clearCache();
834
-			return true;
835
-		}
836
-
837
-		// update local cache
838
-		if ($lazy) {
839
-			$this->lazyCache[$app][$key] = $value;
840
-		} else {
841
-			$this->fastCache[$app][$key] = $value;
842
-		}
843
-		$this->valueTypes[$app][$key] = $type;
844
-
845
-		return true;
846
-	}
847
-
848
-	/**
849
-	 * Change the type of config value.
850
-	 *
851
-	 * **WARNING:** Method is internal and **MUST** not be used as it may break things.
852
-	 *
853
-	 * @param string $app id of the app
854
-	 * @param string $key config key
855
-	 * @param int $type value type {@see VALUE_STRING} {@see VALUE_INT} {@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
856
-	 *
857
-	 * @return bool TRUE if database update were necessary
858
-	 * @throws AppConfigUnknownKeyException if $key is now known in database
859
-	 * @throws AppConfigIncorrectTypeException if $type is not valid
860
-	 * @internal
861
-	 * @since 29.0.0
862
-	 */
863
-	public function updateType(string $app, string $key, int $type = self::VALUE_MIXED): bool {
864
-		$this->assertParams($app, $key);
865
-		$this->loadConfigAll();
866
-		$lazy = $this->isLazy($app, $key);
867
-
868
-		// type can only be one type
869
-		if (!in_array($type, [self::VALUE_MIXED, self::VALUE_STRING, self::VALUE_INT, self::VALUE_FLOAT, self::VALUE_BOOL, self::VALUE_ARRAY])) {
870
-			throw new AppConfigIncorrectTypeException('Unknown value type');
871
-		}
872
-
873
-		$currType = $this->valueTypes[$app][$key];
874
-		if (($type | self::VALUE_SENSITIVE) === ($currType | self::VALUE_SENSITIVE)) {
875
-			return false;
876
-		}
877
-
878
-		// we complete with sensitive flag if the stored value is set as sensitive
879
-		if ($this->isTyped(self::VALUE_SENSITIVE, $currType)) {
880
-			$type = $type | self::VALUE_SENSITIVE;
881
-		}
882
-
883
-		$update = $this->connection->getQueryBuilder();
884
-		$update->update('appconfig')
885
-			->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
886
-			->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
887
-			->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
888
-		$update->executeStatement();
889
-		$this->valueTypes[$app][$key] = $type;
890
-
891
-		return true;
892
-	}
893
-
894
-
895
-	/**
896
-	 * @inheritDoc
897
-	 *
898
-	 * @param string $app id of the app
899
-	 * @param string $key config key
900
-	 * @param bool $sensitive TRUE to set as sensitive, FALSE to unset
901
-	 *
902
-	 * @return bool TRUE if entry was found in database and an update was necessary
903
-	 * @since 29.0.0
904
-	 */
905
-	public function updateSensitive(string $app, string $key, bool $sensitive): bool {
906
-		$this->assertParams($app, $key);
907
-		$this->loadConfigAll();
908
-
909
-		try {
910
-			if ($sensitive === $this->isSensitive($app, $key, null)) {
911
-				return false;
912
-			}
913
-		} catch (AppConfigUnknownKeyException $e) {
914
-			return false;
915
-		}
916
-
917
-		$lazy = $this->isLazy($app, $key);
918
-		if ($lazy) {
919
-			$cache = $this->lazyCache;
920
-		} else {
921
-			$cache = $this->fastCache;
922
-		}
923
-
924
-		if (!isset($cache[$app][$key])) {
925
-			throw new AppConfigUnknownKeyException('unknown config key');
926
-		}
927
-
928
-		/**
929
-		 * type returned by getValueType() is already cleaned from sensitive flag
930
-		 * we just need to update it based on $sensitive and store it in database
931
-		 */
932
-		$type = $this->getValueType($app, $key);
933
-		$value = $cache[$app][$key];
934
-		if ($sensitive) {
935
-			$type |= self::VALUE_SENSITIVE;
936
-			$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
937
-		} else {
938
-			$value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
939
-		}
940
-
941
-		$update = $this->connection->getQueryBuilder();
942
-		$update->update('appconfig')
943
-			->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
944
-			->set('configvalue', $update->createNamedParameter($value))
945
-			->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
946
-			->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
947
-		$update->executeStatement();
948
-
949
-		$this->valueTypes[$app][$key] = $type;
950
-
951
-		return true;
952
-	}
953
-
954
-	/**
955
-	 * @inheritDoc
956
-	 *
957
-	 * @param string $app id of the app
958
-	 * @param string $key config key
959
-	 * @param bool $lazy TRUE to set as lazy loaded, FALSE to unset
960
-	 *
961
-	 * @return bool TRUE if entry was found in database and an update was necessary
962
-	 * @since 29.0.0
963
-	 */
964
-	public function updateLazy(string $app, string $key, bool $lazy): bool {
965
-		$this->assertParams($app, $key);
966
-		$this->loadConfigAll();
967
-
968
-		try {
969
-			if ($lazy === $this->isLazy($app, $key)) {
970
-				return false;
971
-			}
972
-		} catch (AppConfigUnknownKeyException $e) {
973
-			return false;
974
-		}
975
-
976
-		$update = $this->connection->getQueryBuilder();
977
-		$update->update('appconfig')
978
-			->set('lazy', $update->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))
979
-			->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
980
-			->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
981
-		$update->executeStatement();
982
-
983
-		// At this point, it is a lot safer to clean cache
984
-		$this->clearCache();
985
-
986
-		return true;
987
-	}
988
-
989
-	/**
990
-	 * @inheritDoc
991
-	 *
992
-	 * @param string $app id of the app
993
-	 * @param string $key config key
994
-	 *
995
-	 * @return array
996
-	 * @throws AppConfigUnknownKeyException if config key is not known in database
997
-	 * @since 29.0.0
998
-	 */
999
-	public function getDetails(string $app, string $key): array {
1000
-		$this->assertParams($app, $key);
1001
-		$this->loadConfigAll();
1002
-		$lazy = $this->isLazy($app, $key);
1003
-
1004
-		if ($lazy) {
1005
-			$cache = $this->lazyCache;
1006
-		} else {
1007
-			$cache = $this->fastCache;
1008
-		}
1009
-
1010
-		$type = $this->getValueType($app, $key);
1011
-		try {
1012
-			$typeString = $this->convertTypeToString($type);
1013
-		} catch (AppConfigIncorrectTypeException $e) {
1014
-			$this->logger->warning('type stored in database is not correct', ['exception' => $e, 'type' => $type]);
1015
-			$typeString = (string)$type;
1016
-		}
1017
-
1018
-		if (!isset($cache[$app][$key])) {
1019
-			throw new AppConfigUnknownKeyException('unknown config key');
1020
-		}
1021
-
1022
-		$value = $cache[$app][$key];
1023
-		$sensitive = $this->isSensitive($app, $key, null);
1024
-		if ($sensitive && str_starts_with($value, self::ENCRYPTION_PREFIX)) {
1025
-			$value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
1026
-		}
1027
-
1028
-		return [
1029
-			'app' => $app,
1030
-			'key' => $key,
1031
-			'value' => $value,
1032
-			'type' => $type,
1033
-			'lazy' => $lazy,
1034
-			'typeString' => $typeString,
1035
-			'sensitive' => $sensitive
1036
-		];
1037
-	}
1038
-
1039
-	/**
1040
-	 * @param string $type
1041
-	 *
1042
-	 * @return int
1043
-	 * @throws AppConfigIncorrectTypeException
1044
-	 * @since 29.0.0
1045
-	 */
1046
-	public function convertTypeToInt(string $type): int {
1047
-		return match (strtolower($type)) {
1048
-			'mixed' => IAppConfig::VALUE_MIXED,
1049
-			'string' => IAppConfig::VALUE_STRING,
1050
-			'integer' => IAppConfig::VALUE_INT,
1051
-			'float' => IAppConfig::VALUE_FLOAT,
1052
-			'boolean' => IAppConfig::VALUE_BOOL,
1053
-			'array' => IAppConfig::VALUE_ARRAY,
1054
-			default => throw new AppConfigIncorrectTypeException('Unknown type ' . $type)
1055
-		};
1056
-	}
1057
-
1058
-	/**
1059
-	 * @param int $type
1060
-	 *
1061
-	 * @return string
1062
-	 * @throws AppConfigIncorrectTypeException
1063
-	 * @since 29.0.0
1064
-	 */
1065
-	public function convertTypeToString(int $type): string {
1066
-		$type &= ~self::VALUE_SENSITIVE;
1067
-
1068
-		return match ($type) {
1069
-			IAppConfig::VALUE_MIXED => 'mixed',
1070
-			IAppConfig::VALUE_STRING => 'string',
1071
-			IAppConfig::VALUE_INT => 'integer',
1072
-			IAppConfig::VALUE_FLOAT => 'float',
1073
-			IAppConfig::VALUE_BOOL => 'boolean',
1074
-			IAppConfig::VALUE_ARRAY => 'array',
1075
-			default => throw new AppConfigIncorrectTypeException('Unknown numeric type ' . $type)
1076
-		};
1077
-	}
1078
-
1079
-	/**
1080
-	 * @inheritDoc
1081
-	 *
1082
-	 * @param string $app id of the app
1083
-	 * @param string $key config key
1084
-	 *
1085
-	 * @since 29.0.0
1086
-	 */
1087
-	public function deleteKey(string $app, string $key): void {
1088
-		$this->assertParams($app, $key);
1089
-		$qb = $this->connection->getQueryBuilder();
1090
-		$qb->delete('appconfig')
1091
-			->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
1092
-			->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
1093
-		$qb->executeStatement();
1094
-
1095
-		unset($this->lazyCache[$app][$key]);
1096
-		unset($this->fastCache[$app][$key]);
1097
-		unset($this->valueTypes[$app][$key]);
1098
-	}
1099
-
1100
-	/**
1101
-	 * @inheritDoc
1102
-	 *
1103
-	 * @param string $app id of the app
1104
-	 *
1105
-	 * @since 29.0.0
1106
-	 */
1107
-	public function deleteApp(string $app): void {
1108
-		$this->assertParams($app);
1109
-		$qb = $this->connection->getQueryBuilder();
1110
-		$qb->delete('appconfig')
1111
-			->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
1112
-		$qb->executeStatement();
1113
-
1114
-		$this->clearCache();
1115
-	}
1116
-
1117
-	/**
1118
-	 * @inheritDoc
1119
-	 *
1120
-	 * @param bool $reload set to TRUE to refill cache instantly after clearing it
1121
-	 *
1122
-	 * @since 29.0.0
1123
-	 */
1124
-	public function clearCache(bool $reload = false): void {
1125
-		$this->lazyLoaded = $this->fastLoaded = false;
1126
-		$this->lazyCache = $this->fastCache = $this->valueTypes = [];
1127
-
1128
-		if (!$reload) {
1129
-			return;
1130
-		}
1131
-
1132
-		$this->loadConfigAll();
1133
-	}
1134
-
1135
-
1136
-	/**
1137
-	 * For debug purpose.
1138
-	 * Returns the cached data.
1139
-	 *
1140
-	 * @return array
1141
-	 * @since 29.0.0
1142
-	 * @internal
1143
-	 */
1144
-	public function statusCache(): array {
1145
-		return [
1146
-			'fastLoaded' => $this->fastLoaded,
1147
-			'fastCache' => $this->fastCache,
1148
-			'lazyLoaded' => $this->lazyLoaded,
1149
-			'lazyCache' => $this->lazyCache,
1150
-		];
1151
-	}
1152
-
1153
-	/**
1154
-	 * @param int $needle bitflag to search
1155
-	 * @param int $type known value
1156
-	 *
1157
-	 * @return bool TRUE if bitflag $needle is set in $type
1158
-	 */
1159
-	private function isTyped(int $needle, int $type): bool {
1160
-		return (($needle & $type) !== 0);
1161
-	}
1162
-
1163
-	/**
1164
-	 * Confirm the string set for app and key fit the database description
1165
-	 *
1166
-	 * @param string $app assert $app fit in database
1167
-	 * @param string $configKey assert config key fit in database
1168
-	 * @param bool $allowEmptyApp $app can be empty string
1169
-	 * @param int $valueType assert value type is only one type
1170
-	 *
1171
-	 * @throws InvalidArgumentException
1172
-	 */
1173
-	private function assertParams(string $app = '', string $configKey = '', bool $allowEmptyApp = false, int $valueType = -1): void {
1174
-		if (!$allowEmptyApp && $app === '') {
1175
-			throw new InvalidArgumentException('app cannot be an empty string');
1176
-		}
1177
-		if (strlen($app) > self::APP_MAX_LENGTH) {
1178
-			throw new InvalidArgumentException(
1179
-				'Value (' . $app . ') for app is too long (' . self::APP_MAX_LENGTH . ')'
1180
-			);
1181
-		}
1182
-		if (strlen($configKey) > self::KEY_MAX_LENGTH) {
1183
-			throw new InvalidArgumentException('Value (' . $configKey . ') for key is too long (' . self::KEY_MAX_LENGTH . ')');
1184
-		}
1185
-		if ($valueType > -1) {
1186
-			$valueType &= ~self::VALUE_SENSITIVE;
1187
-			if (!in_array($valueType, [self::VALUE_MIXED, self::VALUE_STRING, self::VALUE_INT, self::VALUE_FLOAT, self::VALUE_BOOL, self::VALUE_ARRAY])) {
1188
-				throw new InvalidArgumentException('Unknown value type');
1189
-			}
1190
-		}
1191
-	}
1192
-
1193
-	private function loadConfigAll(?string $app = null): void {
1194
-		$this->loadConfig($app, null);
1195
-	}
1196
-
1197
-	/**
1198
-	 * Load normal config or config set as lazy loaded
1199
-	 *
1200
-	 * @param bool|null $lazy set to TRUE to load config set as lazy loaded, set to NULL to load all config
1201
-	 */
1202
-	private function loadConfig(?string $app = null, ?bool $lazy = false): void {
1203
-		if ($this->isLoaded($lazy)) {
1204
-			return;
1205
-		}
1206
-
1207
-		// if lazy is null or true, we debug log
1208
-		if (($lazy ?? true) !== false && $app !== null) {
1209
-			$exception = new \RuntimeException('The loading of lazy AppConfig values have been triggered by app "' . $app . '"');
1210
-			$this->logger->debug($exception->getMessage(), ['exception' => $exception, 'app' => $app]);
1211
-		}
1212
-
1213
-		$qb = $this->connection->getQueryBuilder();
1214
-		$qb->from('appconfig');
1215
-
1216
-		// we only need value from lazy when loadConfig does not specify it
1217
-		$qb->select('appid', 'configkey', 'configvalue', 'type');
1218
-
1219
-		if ($lazy !== null) {
1220
-			$qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT)));
1221
-		} else {
1222
-			$qb->addSelect('lazy');
1223
-		}
1224
-
1225
-		$result = $qb->executeQuery();
1226
-		$rows = $result->fetchAll();
1227
-		foreach ($rows as $row) {
1228
-			// most of the time, 'lazy' is not in the select because its value is already known
1229
-			if (($row['lazy'] ?? ($lazy ?? 0) ? 1 : 0) === 1) {
1230
-				$this->lazyCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1231
-			} else {
1232
-				$this->fastCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1233
-			}
1234
-			$this->valueTypes[$row['appid']][$row['configkey']] = (int)($row['type'] ?? 0);
1235
-		}
1236
-		$result->closeCursor();
1237
-		$this->setAsLoaded($lazy);
1238
-	}
1239
-
1240
-	/**
1241
-	 * if $lazy is:
1242
-	 *  - false: will returns true if fast config is loaded
1243
-	 *  - true : will returns true if lazy config is loaded
1244
-	 *  - null : will returns true if both config are loaded
1245
-	 *
1246
-	 * @param bool $lazy
1247
-	 *
1248
-	 * @return bool
1249
-	 */
1250
-	private function isLoaded(?bool $lazy): bool {
1251
-		if ($lazy === null) {
1252
-			return $this->lazyLoaded && $this->fastLoaded;
1253
-		}
1254
-
1255
-		return $lazy ? $this->lazyLoaded : $this->fastLoaded;
1256
-	}
1257
-
1258
-	/**
1259
-	 * if $lazy is:
1260
-	 * - false: set fast config as loaded
1261
-	 * - true : set lazy config as loaded
1262
-	 * - null : set both config as loaded
1263
-	 *
1264
-	 * @param bool $lazy
1265
-	 */
1266
-	private function setAsLoaded(?bool $lazy): void {
1267
-		if ($lazy === null) {
1268
-			$this->fastLoaded = true;
1269
-			$this->lazyLoaded = true;
1270
-
1271
-			return;
1272
-		}
1273
-
1274
-		if ($lazy) {
1275
-			$this->lazyLoaded = true;
1276
-		} else {
1277
-			$this->fastLoaded = true;
1278
-		}
1279
-	}
1280
-
1281
-	/**
1282
-	 * Gets the config value
1283
-	 *
1284
-	 * @param string $app app
1285
-	 * @param string $key key
1286
-	 * @param string $default = null, default value if the key does not exist
1287
-	 *
1288
-	 * @return string the value or $default
1289
-	 * @deprecated 29.0.0 use getValue*()
1290
-	 *
1291
-	 * This function gets a value from the appconfig table. If the key does
1292
-	 * not exist the default value will be returned
1293
-	 */
1294
-	public function getValue($app, $key, $default = null) {
1295
-		$this->loadConfig($app);
1296
-
1297
-		return $this->fastCache[$app][$key] ?? $default;
1298
-	}
1299
-
1300
-	/**
1301
-	 * Sets a value. If the key did not exist before it will be created.
1302
-	 *
1303
-	 * @param string $app app
1304
-	 * @param string $key key
1305
-	 * @param string|float|int $value value
1306
-	 *
1307
-	 * @return bool True if the value was inserted or updated, false if the value was the same
1308
-	 * @throws AppConfigTypeConflictException
1309
-	 * @throws AppConfigUnknownKeyException
1310
-	 * @deprecated 29.0.0
1311
-	 */
1312
-	public function setValue($app, $key, $value) {
1313
-		/**
1314
-		 * TODO: would it be overkill, or decently improve performance, to catch
1315
-		 * call to this method with $key='enabled' and 'hide' config value related
1316
-		 * to $app when the app is disabled (by modifying entry in database: lazy=lazy+2)
1317
-		 * or enabled (lazy=lazy-2)
1318
-		 *
1319
-		 * this solution would remove the loading of config values from disabled app
1320
-		 * unless calling the method {@see loadConfigAll()}
1321
-		 */
1322
-		return $this->setTypedValue($app, $key, (string)$value, false, self::VALUE_MIXED);
1323
-	}
1324
-
1325
-
1326
-	/**
1327
-	 * get multiple values, either the app or key can be used as wildcard by setting it to false
1328
-	 *
1329
-	 * @param string|false $app
1330
-	 * @param string|false $key
1331
-	 *
1332
-	 * @return array|false
1333
-	 * @deprecated 29.0.0 use {@see getAllValues()}
1334
-	 */
1335
-	public function getValues($app, $key) {
1336
-		if (($app !== false) === ($key !== false)) {
1337
-			return false;
1338
-		}
1339
-
1340
-		$key = ($key === false) ? '' : $key;
1341
-		if (!$app) {
1342
-			return $this->searchValues($key, false, self::VALUE_MIXED);
1343
-		} else {
1344
-			return $this->getAllValues($app, $key);
1345
-		}
1346
-	}
1347
-
1348
-	/**
1349
-	 * get all values of the app or and filters out sensitive data
1350
-	 *
1351
-	 * @param string $app
1352
-	 *
1353
-	 * @return array
1354
-	 * @deprecated 29.0.0 use {@see getAllValues()}
1355
-	 */
1356
-	public function getFilteredValues($app) {
1357
-		return $this->getAllValues($app, filtered: true);
1358
-	}
1359
-
1360
-
1361
-	/**
1362
-	 * **Warning:** avoid default NULL value for $lazy as this will
1363
-	 * load all lazy values from the database
1364
-	 *
1365
-	 * @param string $app
1366
-	 * @param array<string, string> $values ['key' => 'value']
1367
-	 * @param bool|null $lazy
1368
-	 *
1369
-	 * @return array<string, string|int|float|bool|array>
1370
-	 */
1371
-	private function formatAppValues(string $app, array $values, ?bool $lazy = null): array {
1372
-		foreach ($values as $key => $value) {
1373
-			try {
1374
-				$type = $this->getValueType($app, $key, $lazy);
1375
-			} catch (AppConfigUnknownKeyException $e) {
1376
-				continue;
1377
-			}
1378
-
1379
-			$values[$key] = $this->convertTypedValue($value, $type);
1380
-		}
1381
-
1382
-		return $values;
1383
-	}
1384
-
1385
-	/**
1386
-	 * convert string value to the expected type
1387
-	 *
1388
-	 * @param string $value
1389
-	 * @param int $type
1390
-	 *
1391
-	 * @return string|int|float|bool|array
1392
-	 */
1393
-	private function convertTypedValue(string $value, int $type): string|int|float|bool|array {
1394
-		switch ($type) {
1395
-			case self::VALUE_INT:
1396
-				return (int)$value;
1397
-			case self::VALUE_FLOAT:
1398
-				return (float)$value;
1399
-			case self::VALUE_BOOL:
1400
-				return in_array(strtolower($value), ['1', 'true', 'yes', 'on']);
1401
-			case self::VALUE_ARRAY:
1402
-				try {
1403
-					return json_decode($value, true, flags: JSON_THROW_ON_ERROR);
1404
-				} catch (JsonException $e) {
1405
-					// ignoreable
1406
-				}
1407
-				break;
1408
-		}
1409
-		return $value;
1410
-	}
1411
-
1412
-	/**
1413
-	 * @param string $app
1414
-	 *
1415
-	 * @return string[]
1416
-	 * @deprecated 29.0.0 data sensitivity should be set when calling setValue*()
1417
-	 */
1418
-	private function getSensitiveKeys(string $app): array {
1419
-		$sensitiveValues = [
1420
-			'circles' => [
1421
-				'/^key_pairs$/',
1422
-				'/^local_gskey$/',
1423
-			],
1424
-			'call_summary_bot' => [
1425
-				'/^secret_(.*)$/',
1426
-			],
1427
-			'external' => [
1428
-				'/^sites$/',
1429
-				'/^jwt_token_privkey_(.*)$/',
1430
-			],
1431
-			'globalsiteselector' => [
1432
-				'/^gss\.jwt\.key$/',
1433
-			],
1434
-			'gpgmailer' => [
1435
-				'/^GpgServerKey$/',
1436
-			],
1437
-			'integration_discourse' => [
1438
-				'/^private_key$/',
1439
-				'/^public_key$/',
1440
-			],
1441
-			'integration_dropbox' => [
1442
-				'/^client_id$/',
1443
-				'/^client_secret$/',
1444
-			],
1445
-			'integration_github' => [
1446
-				'/^client_id$/',
1447
-				'/^client_secret$/',
1448
-			],
1449
-			'integration_gitlab' => [
1450
-				'/^client_id$/',
1451
-				'/^client_secret$/',
1452
-				'/^oauth_instance_url$/',
1453
-			],
1454
-			'integration_google' => [
1455
-				'/^client_id$/',
1456
-				'/^client_secret$/',
1457
-			],
1458
-			'integration_jira' => [
1459
-				'/^client_id$/',
1460
-				'/^client_secret$/',
1461
-				'/^forced_instance_url$/',
1462
-			],
1463
-			'integration_onedrive' => [
1464
-				'/^client_id$/',
1465
-				'/^client_secret$/',
1466
-			],
1467
-			'integration_openproject' => [
1468
-				'/^client_id$/',
1469
-				'/^client_secret$/',
1470
-				'/^oauth_instance_url$/',
1471
-			],
1472
-			'integration_reddit' => [
1473
-				'/^client_id$/',
1474
-				'/^client_secret$/',
1475
-			],
1476
-			'integration_suitecrm' => [
1477
-				'/^client_id$/',
1478
-				'/^client_secret$/',
1479
-				'/^oauth_instance_url$/',
1480
-			],
1481
-			'integration_twitter' => [
1482
-				'/^consumer_key$/',
1483
-				'/^consumer_secret$/',
1484
-				'/^followed_user$/',
1485
-			],
1486
-			'integration_zammad' => [
1487
-				'/^client_id$/',
1488
-				'/^client_secret$/',
1489
-				'/^oauth_instance_url$/',
1490
-			],
1491
-			'maps' => [
1492
-				'/^mapboxAPIKEY$/',
1493
-			],
1494
-			'notify_push' => [
1495
-				'/^cookie$/',
1496
-			],
1497
-			'onlyoffice' => [
1498
-				'/^jwt_secret$/',
1499
-			],
1500
-			'passwords' => [
1501
-				'/^SSEv1ServerKey$/',
1502
-			],
1503
-			'serverinfo' => [
1504
-				'/^token$/',
1505
-			],
1506
-			'spreed' => [
1507
-				'/^bridge_bot_password$/',
1508
-				'/^hosted-signaling-server-(.*)$/',
1509
-				'/^recording_servers$/',
1510
-				'/^signaling_servers$/',
1511
-				'/^signaling_ticket_secret$/',
1512
-				'/^signaling_token_privkey_(.*)$/',
1513
-				'/^signaling_token_pubkey_(.*)$/',
1514
-				'/^sip_bridge_dialin_info$/',
1515
-				'/^sip_bridge_shared_secret$/',
1516
-				'/^stun_servers$/',
1517
-				'/^turn_servers$/',
1518
-				'/^turn_server_secret$/',
1519
-			],
1520
-			'support' => [
1521
-				'/^last_response$/',
1522
-				'/^potential_subscription_key$/',
1523
-				'/^subscription_key$/',
1524
-			],
1525
-			'theming' => [
1526
-				'/^imprintUrl$/',
1527
-				'/^privacyUrl$/',
1528
-				'/^slogan$/',
1529
-				'/^url$/',
1530
-			],
1531
-			'twofactor_gateway' => [
1532
-				'/^.*token$/',
1533
-			],
1534
-			'user_ldap' => [
1535
-				'/^(s..)?ldap_agent_password$/',
1536
-			],
1537
-			'user_saml' => [
1538
-				'/^idp-x509cert$/',
1539
-			],
1540
-			'whiteboard' => [
1541
-				'/^jwt_secret_key$/',
1542
-			],
1543
-		];
1544
-
1545
-		return $sensitiveValues[$app] ?? [];
1546
-	}
1547
-
1548
-	/**
1549
-	 * Clear all the cached app config values
1550
-	 * New cache will be generated next time a config value is retrieved
1551
-	 *
1552
-	 * @deprecated 29.0.0 use {@see clearCache()}
1553
-	 */
1554
-	public function clearCachedConfig(): void {
1555
-		$this->clearCache();
1556
-	}
1557
-
1558
-	/**
1559
-	 * match and apply current use of config values with defined lexicon
1560
-	 *
1561
-	 * @throws AppConfigUnknownKeyException
1562
-	 * @throws AppConfigTypeConflictException
1563
-	 * @return bool TRUE if everything is fine compared to lexicon or lexicon does not exist
1564
-	 */
1565
-	private function matchAndApplyLexiconDefinition(
1566
-		string $app,
1567
-		string $key,
1568
-		bool &$lazy,
1569
-		int &$type,
1570
-		string &$default = '',
1571
-	): bool {
1572
-		if (in_array($key,
1573
-			[
1574
-				'enabled',
1575
-				'installed_version',
1576
-				'types',
1577
-			])) {
1578
-			return true; // we don't break stuff for this list of config keys.
1579
-		}
1580
-		$configDetails = $this->getConfigDetailsFromLexicon($app);
1581
-		if (!array_key_exists($key, $configDetails['entries'])) {
1582
-			return $this->applyLexiconStrictness(
1583
-				$configDetails['strictness'],
1584
-				'The app config key ' . $app . '/' . $key . ' is not defined in the config lexicon'
1585
-			);
1586
-		}
1587
-
1588
-		/** @var ConfigLexiconEntry $configValue */
1589
-		$configValue = $configDetails['entries'][$key];
1590
-		$type &= ~self::VALUE_SENSITIVE;
1591
-
1592
-		$appConfigValueType = $configValue->getValueType()->toAppConfigFlag();
1593
-		if ($type === self::VALUE_MIXED) {
1594
-			$type = $appConfigValueType; // we overwrite if value was requested as mixed
1595
-		} elseif ($appConfigValueType !== $type) {
1596
-			throw new AppConfigTypeConflictException('The app config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
1597
-		}
1598
-
1599
-		$lazy = $configValue->isLazy();
1600
-		$default = $configValue->getDefault() ?? $default; // default from Lexicon got priority
1601
-		if ($configValue->isFlagged(self::FLAG_SENSITIVE)) {
1602
-			$type |= self::VALUE_SENSITIVE;
1603
-		}
1604
-		if ($configValue->isDeprecated()) {
1605
-			$this->logger->notice('App config key ' . $app . '/' . $key . ' is set as deprecated.');
1606
-		}
1607
-
1608
-		return true;
1609
-	}
1610
-
1611
-	/**
1612
-	 * manage ConfigLexicon behavior based on strictness set in IConfigLexicon
1613
-	 *
1614
-	 * @param ConfigLexiconStrictness|null $strictness
1615
-	 * @param string $line
1616
-	 *
1617
-	 * @return bool TRUE if conflict can be fully ignored, FALSE if action should be not performed
1618
-	 * @throws AppConfigUnknownKeyException if strictness implies exception
1619
-	 * @see IConfigLexicon::getStrictness()
1620
-	 */
1621
-	private function applyLexiconStrictness(
1622
-		?ConfigLexiconStrictness $strictness,
1623
-		string $line = '',
1624
-	): bool {
1625
-		if ($strictness === null) {
1626
-			return true;
1627
-		}
1628
-
1629
-		switch ($strictness) {
1630
-			case ConfigLexiconStrictness::IGNORE:
1631
-				return true;
1632
-			case ConfigLexiconStrictness::NOTICE:
1633
-				$this->logger->notice($line);
1634
-				return true;
1635
-			case ConfigLexiconStrictness::WARNING:
1636
-				$this->logger->warning($line);
1637
-				return false;
1638
-		}
1639
-
1640
-		throw new AppConfigUnknownKeyException($line);
1641
-	}
1642
-
1643
-	/**
1644
-	 * extract details from registered $appId's config lexicon
1645
-	 *
1646
-	 * @param string $appId
1647
-	 *
1648
-	 * @return array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}
1649
-	 */
1650
-	private function getConfigDetailsFromLexicon(string $appId): array {
1651
-		if (!array_key_exists($appId, $this->configLexiconDetails)) {
1652
-			$entries = [];
1653
-			$bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
1654
-			$configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
1655
-			foreach ($configLexicon?->getAppConfigs() ?? [] as $configEntry) {
1656
-				$entries[$configEntry->getKey()] = $configEntry;
1657
-			}
1658
-
1659
-			$this->configLexiconDetails[$appId] = [
1660
-				'entries' => $entries,
1661
-				'strictness' => $configLexicon?->getStrictness() ?? ConfigLexiconStrictness::IGNORE
1662
-			];
1663
-		}
1664
-
1665
-		return $this->configLexiconDetails[$appId];
1666
-	}
1667
-
1668
-	/**
1669
-	 * Returns the installed versions of all apps
1670
-	 *
1671
-	 * @return array<string, string>
1672
-	 */
1673
-	public function getAppInstalledVersions(): array {
1674
-		if ($this->appVersionsCache === null) {
1675
-			/** @var array<string, string> */
1676
-			$this->appVersionsCache = $this->searchValues('installed_version', false, IAppConfig::VALUE_STRING);
1677
-		}
1678
-		return $this->appVersionsCache;
1679
-	}
49
+    private const APP_MAX_LENGTH = 32;
50
+    private const KEY_MAX_LENGTH = 64;
51
+    private const ENCRYPTION_PREFIX = '$AppConfigEncryption$';
52
+    private const ENCRYPTION_PREFIX_LENGTH = 21; // strlen(self::ENCRYPTION_PREFIX)
53
+
54
+    /** @var array<string, array<string, mixed>> ['app_id' => ['config_key' => 'config_value']] */
55
+    private array $fastCache = [];   // cache for normal config keys
56
+    /** @var array<string, array<string, mixed>> ['app_id' => ['config_key' => 'config_value']] */
57
+    private array $lazyCache = [];   // cache for lazy config keys
58
+    /** @var array<string, array<string, int>> ['app_id' => ['config_key' => bitflag]] */
59
+    private array $valueTypes = [];  // type for all config values
60
+    private bool $fastLoaded = false;
61
+    private bool $lazyLoaded = false;
62
+    /** @var array<array-key, array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
63
+    private array $configLexiconDetails = [];
64
+
65
+    /** @var ?array<string, string> */
66
+    private ?array $appVersionsCache = null;
67
+
68
+    public function __construct(
69
+        protected IDBConnection $connection,
70
+        protected LoggerInterface $logger,
71
+        protected ICrypto $crypto,
72
+    ) {
73
+    }
74
+
75
+    /**
76
+     * @inheritDoc
77
+     *
78
+     * @return list<string> list of app ids
79
+     * @since 7.0.0
80
+     */
81
+    public function getApps(): array {
82
+        $this->loadConfigAll();
83
+        $apps = array_merge(array_keys($this->fastCache), array_keys($this->lazyCache));
84
+        sort($apps);
85
+
86
+        return array_values(array_unique($apps));
87
+    }
88
+
89
+    /**
90
+     * @inheritDoc
91
+     *
92
+     * @param string $app id of the app
93
+     *
94
+     * @return list<string> list of stored config keys
95
+     * @since 29.0.0
96
+     */
97
+    public function getKeys(string $app): array {
98
+        $this->assertParams($app);
99
+        $this->loadConfigAll($app);
100
+        $keys = array_merge(array_keys($this->fastCache[$app] ?? []), array_keys($this->lazyCache[$app] ?? []));
101
+        sort($keys);
102
+
103
+        return array_values(array_unique($keys));
104
+    }
105
+
106
+    /**
107
+     * @inheritDoc
108
+     *
109
+     * @param string $app id of the app
110
+     * @param string $key config key
111
+     * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
112
+     *
113
+     * @return bool TRUE if key exists
114
+     * @since 7.0.0
115
+     * @since 29.0.0 Added the $lazy argument
116
+     */
117
+    public function hasKey(string $app, string $key, ?bool $lazy = false): bool {
118
+        $this->assertParams($app, $key);
119
+        $this->loadConfig($app, $lazy);
120
+
121
+        if ($lazy === null) {
122
+            $appCache = $this->getAllValues($app);
123
+            return isset($appCache[$key]);
124
+        }
125
+
126
+        if ($lazy) {
127
+            return isset($this->lazyCache[$app][$key]);
128
+        }
129
+
130
+        return isset($this->fastCache[$app][$key]);
131
+    }
132
+
133
+    /**
134
+     * @param string $app id of the app
135
+     * @param string $key config key
136
+     * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
137
+     *
138
+     * @return bool
139
+     * @throws AppConfigUnknownKeyException if config key is not known
140
+     * @since 29.0.0
141
+     */
142
+    public function isSensitive(string $app, string $key, ?bool $lazy = false): bool {
143
+        $this->assertParams($app, $key);
144
+        $this->loadConfig(null, $lazy);
145
+
146
+        if (!isset($this->valueTypes[$app][$key])) {
147
+            throw new AppConfigUnknownKeyException('unknown config key');
148
+        }
149
+
150
+        return $this->isTyped(self::VALUE_SENSITIVE, $this->valueTypes[$app][$key]);
151
+    }
152
+
153
+    /**
154
+     * @inheritDoc
155
+     *
156
+     * @param string $app if of the app
157
+     * @param string $key config key
158
+     *
159
+     * @return bool TRUE if config is lazy loaded
160
+     * @throws AppConfigUnknownKeyException if config key is not known
161
+     * @see IAppConfig for details about lazy loading
162
+     * @since 29.0.0
163
+     */
164
+    public function isLazy(string $app, string $key): bool {
165
+        // there is a huge probability the non-lazy config are already loaded
166
+        if ($this->hasKey($app, $key, false)) {
167
+            return false;
168
+        }
169
+
170
+        // key not found, we search in the lazy config
171
+        if ($this->hasKey($app, $key, true)) {
172
+            return true;
173
+        }
174
+
175
+        throw new AppConfigUnknownKeyException('unknown config key');
176
+    }
177
+
178
+
179
+    /**
180
+     * @inheritDoc
181
+     *
182
+     * @param string $app id of the app
183
+     * @param string $prefix config keys prefix to search
184
+     * @param bool $filtered TRUE to hide sensitive config values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
185
+     *
186
+     * @return array<string, string|int|float|bool|array> [configKey => configValue]
187
+     * @since 29.0.0
188
+     */
189
+    public function getAllValues(string $app, string $prefix = '', bool $filtered = false): array {
190
+        $this->assertParams($app, $prefix);
191
+        // if we want to filter values, we need to get sensitivity
192
+        $this->loadConfigAll($app);
193
+        // array_merge() will remove numeric keys (here config keys), so addition arrays instead
194
+        $values = $this->formatAppValues($app, ($this->fastCache[$app] ?? []) + ($this->lazyCache[$app] ?? []));
195
+        $values = array_filter(
196
+            $values,
197
+            function (string $key) use ($prefix): bool {
198
+                return str_starts_with($key, $prefix); // filter values based on $prefix
199
+            }, ARRAY_FILTER_USE_KEY
200
+        );
201
+
202
+        if (!$filtered) {
203
+            return $values;
204
+        }
205
+
206
+        /**
207
+         * Using the old (deprecated) list of sensitive values.
208
+         */
209
+        foreach ($this->getSensitiveKeys($app) as $sensitiveKeyExp) {
210
+            $sensitiveKeys = preg_grep($sensitiveKeyExp, array_keys($values));
211
+            foreach ($sensitiveKeys as $sensitiveKey) {
212
+                $this->valueTypes[$app][$sensitiveKey] = ($this->valueTypes[$app][$sensitiveKey] ?? 0) | self::VALUE_SENSITIVE;
213
+            }
214
+        }
215
+
216
+        $result = [];
217
+        foreach ($values as $key => $value) {
218
+            $result[$key] = $this->isTyped(self::VALUE_SENSITIVE, $this->valueTypes[$app][$key] ?? 0) ? IConfig::SENSITIVE_VALUE : $value;
219
+        }
220
+
221
+        return $result;
222
+    }
223
+
224
+    /**
225
+     * @inheritDoc
226
+     *
227
+     * @param string $key config key
228
+     * @param bool $lazy search within lazy loaded config
229
+     * @param int|null $typedAs enforce type for the returned values ({@see self::VALUE_STRING} and others)
230
+     *
231
+     * @return array<string, string|int|float|bool|array> [appId => configValue]
232
+     * @since 29.0.0
233
+     */
234
+    public function searchValues(string $key, bool $lazy = false, ?int $typedAs = null): array {
235
+        $this->assertParams('', $key, true);
236
+        $this->loadConfig(null, $lazy);
237
+
238
+        /** @var array<array-key, array<array-key, mixed>> $cache */
239
+        if ($lazy) {
240
+            $cache = $this->lazyCache;
241
+        } else {
242
+            $cache = $this->fastCache;
243
+        }
244
+
245
+        $values = [];
246
+        foreach (array_keys($cache) as $app) {
247
+            if (isset($cache[$app][$key])) {
248
+                $values[$app] = $this->convertTypedValue($cache[$app][$key], $typedAs ?? $this->getValueType((string)$app, $key, $lazy));
249
+            }
250
+        }
251
+
252
+        return $values;
253
+    }
254
+
255
+
256
+    /**
257
+     * Get the config value as string.
258
+     * If the value does not exist the given default will be returned.
259
+     *
260
+     * Set lazy to `null` to ignore it and get the value from either source.
261
+     *
262
+     * **WARNING:** Method is internal and **SHOULD** not be used, as it is better to get the value with a type.
263
+     *
264
+     * @param string $app id of the app
265
+     * @param string $key config key
266
+     * @param string $default config value
267
+     * @param null|bool $lazy get config as lazy loaded or not. can be NULL
268
+     *
269
+     * @return string the value or $default
270
+     * @internal
271
+     * @since 29.0.0
272
+     * @see IAppConfig for explanation about lazy loading
273
+     * @see getValueString()
274
+     * @see getValueInt()
275
+     * @see getValueFloat()
276
+     * @see getValueBool()
277
+     * @see getValueArray()
278
+     */
279
+    public function getValueMixed(
280
+        string $app,
281
+        string $key,
282
+        string $default = '',
283
+        ?bool $lazy = false,
284
+    ): string {
285
+        try {
286
+            $lazy = ($lazy === null) ? $this->isLazy($app, $key) : $lazy;
287
+        } catch (AppConfigUnknownKeyException $e) {
288
+            return $default;
289
+        }
290
+
291
+        return $this->getTypedValue(
292
+            $app,
293
+            $key,
294
+            $default,
295
+            $lazy,
296
+            self::VALUE_MIXED
297
+        );
298
+    }
299
+
300
+    /**
301
+     * @inheritDoc
302
+     *
303
+     * @param string $app id of the app
304
+     * @param string $key config key
305
+     * @param string $default default value
306
+     * @param bool $lazy search within lazy loaded config
307
+     *
308
+     * @return string stored config value or $default if not set in database
309
+     * @throws InvalidArgumentException if one of the argument format is invalid
310
+     * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
311
+     * @since 29.0.0
312
+     * @see IAppConfig for explanation about lazy loading
313
+     */
314
+    public function getValueString(
315
+        string $app,
316
+        string $key,
317
+        string $default = '',
318
+        bool $lazy = false,
319
+    ): string {
320
+        return $this->getTypedValue($app, $key, $default, $lazy, self::VALUE_STRING);
321
+    }
322
+
323
+    /**
324
+     * @inheritDoc
325
+     *
326
+     * @param string $app id of the app
327
+     * @param string $key config key
328
+     * @param int $default default value
329
+     * @param bool $lazy search within lazy loaded config
330
+     *
331
+     * @return int stored config value or $default if not set in database
332
+     * @throws InvalidArgumentException if one of the argument format is invalid
333
+     * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
334
+     * @since 29.0.0
335
+     * @see IAppConfig for explanation about lazy loading
336
+     */
337
+    public function getValueInt(
338
+        string $app,
339
+        string $key,
340
+        int $default = 0,
341
+        bool $lazy = false,
342
+    ): int {
343
+        return (int)$this->getTypedValue($app, $key, (string)$default, $lazy, self::VALUE_INT);
344
+    }
345
+
346
+    /**
347
+     * @inheritDoc
348
+     *
349
+     * @param string $app id of the app
350
+     * @param string $key config key
351
+     * @param float $default default value
352
+     * @param bool $lazy search within lazy loaded config
353
+     *
354
+     * @return float stored config value or $default if not set in database
355
+     * @throws InvalidArgumentException if one of the argument format is invalid
356
+     * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
357
+     * @since 29.0.0
358
+     * @see IAppConfig for explanation about lazy loading
359
+     */
360
+    public function getValueFloat(string $app, string $key, float $default = 0, bool $lazy = false): float {
361
+        return (float)$this->getTypedValue($app, $key, (string)$default, $lazy, self::VALUE_FLOAT);
362
+    }
363
+
364
+    /**
365
+     * @inheritDoc
366
+     *
367
+     * @param string $app id of the app
368
+     * @param string $key config key
369
+     * @param bool $default default value
370
+     * @param bool $lazy search within lazy loaded config
371
+     *
372
+     * @return bool stored config value or $default if not set in database
373
+     * @throws InvalidArgumentException if one of the argument format is invalid
374
+     * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
375
+     * @since 29.0.0
376
+     * @see IAppConfig for explanation about lazy loading
377
+     */
378
+    public function getValueBool(string $app, string $key, bool $default = false, bool $lazy = false): bool {
379
+        $b = strtolower($this->getTypedValue($app, $key, $default ? 'true' : 'false', $lazy, self::VALUE_BOOL));
380
+        return in_array($b, ['1', 'true', 'yes', 'on']);
381
+    }
382
+
383
+    /**
384
+     * @inheritDoc
385
+     *
386
+     * @param string $app id of the app
387
+     * @param string $key config key
388
+     * @param array $default default value
389
+     * @param bool $lazy search within lazy loaded config
390
+     *
391
+     * @return array stored config value or $default if not set in database
392
+     * @throws InvalidArgumentException if one of the argument format is invalid
393
+     * @throws AppConfigTypeConflictException in case of conflict with the value type set in database
394
+     * @since 29.0.0
395
+     * @see IAppConfig for explanation about lazy loading
396
+     */
397
+    public function getValueArray(
398
+        string $app,
399
+        string $key,
400
+        array $default = [],
401
+        bool $lazy = false,
402
+    ): array {
403
+        try {
404
+            $defaultJson = json_encode($default, JSON_THROW_ON_ERROR);
405
+            $value = json_decode($this->getTypedValue($app, $key, $defaultJson, $lazy, self::VALUE_ARRAY), true, flags: JSON_THROW_ON_ERROR);
406
+
407
+            return is_array($value) ? $value : [];
408
+        } catch (JsonException) {
409
+            return [];
410
+        }
411
+    }
412
+
413
+    /**
414
+     * @param string $app id of the app
415
+     * @param string $key config key
416
+     * @param string $default default value
417
+     * @param bool $lazy search within lazy loaded config
418
+     * @param int $type value type {@see VALUE_STRING} {@see VALUE_INT}{@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
419
+     *
420
+     * @return string
421
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
422
+     * @throws InvalidArgumentException
423
+     */
424
+    private function getTypedValue(
425
+        string $app,
426
+        string $key,
427
+        string $default,
428
+        bool $lazy,
429
+        int $type,
430
+    ): string {
431
+        $this->assertParams($app, $key, valueType: $type);
432
+        if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $default)) {
433
+            return $default; // returns default if strictness of lexicon is set to WARNING (block and report)
434
+        }
435
+        $this->loadConfig($app, $lazy);
436
+
437
+        /**
438
+         * We ignore check if mixed type is requested.
439
+         * If type of stored value is set as mixed, we don't filter.
440
+         * If type of stored value is defined, we compare with the one requested.
441
+         */
442
+        $knownType = $this->valueTypes[$app][$key] ?? 0;
443
+        if (!$this->isTyped(self::VALUE_MIXED, $type)
444
+            && $knownType > 0
445
+            && !$this->isTyped(self::VALUE_MIXED, $knownType)
446
+            && !$this->isTyped($type, $knownType)) {
447
+            $this->logger->warning('conflict with value type from database', ['app' => $app, 'key' => $key, 'type' => $type, 'knownType' => $knownType]);
448
+            throw new AppConfigTypeConflictException('conflict with value type from database');
449
+        }
450
+
451
+        /**
452
+         * - the pair $app/$key cannot exist in both array,
453
+         * - we should still return an existing non-lazy value even if current method
454
+         *   is called with $lazy is true
455
+         *
456
+         * This way, lazyCache will be empty until the load for lazy config value is requested.
457
+         */
458
+        if (isset($this->lazyCache[$app][$key])) {
459
+            $value = $this->lazyCache[$app][$key];
460
+        } elseif (isset($this->fastCache[$app][$key])) {
461
+            $value = $this->fastCache[$app][$key];
462
+        } else {
463
+            return $default;
464
+        }
465
+
466
+        $sensitive = $this->isTyped(self::VALUE_SENSITIVE, $knownType);
467
+        if ($sensitive && str_starts_with($value, self::ENCRYPTION_PREFIX)) {
468
+            // Only decrypt values that are stored encrypted
469
+            $value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
470
+        }
471
+
472
+        return $value;
473
+    }
474
+
475
+    /**
476
+     * @inheritDoc
477
+     *
478
+     * @param string $app id of the app
479
+     * @param string $key config key
480
+     *
481
+     * @return int type of the value
482
+     * @throws AppConfigUnknownKeyException if config key is not known
483
+     * @since 29.0.0
484
+     * @see VALUE_STRING
485
+     * @see VALUE_INT
486
+     * @see VALUE_FLOAT
487
+     * @see VALUE_BOOL
488
+     * @see VALUE_ARRAY
489
+     */
490
+    public function getValueType(string $app, string $key, ?bool $lazy = null): int {
491
+        $type = self::VALUE_MIXED;
492
+        $ignorable = $lazy ?? false;
493
+        $this->matchAndApplyLexiconDefinition($app, $key, $ignorable, $type);
494
+        if ($type !== self::VALUE_MIXED) {
495
+            // a modified $type means config key is set in Lexicon
496
+            return $type;
497
+        }
498
+
499
+        $this->assertParams($app, $key);
500
+        $this->loadConfig($app, $lazy);
501
+
502
+        if (!isset($this->valueTypes[$app][$key])) {
503
+            throw new AppConfigUnknownKeyException('unknown config key');
504
+        }
505
+
506
+        $type = $this->valueTypes[$app][$key];
507
+        $type &= ~self::VALUE_SENSITIVE;
508
+        return $type;
509
+    }
510
+
511
+
512
+    /**
513
+     * Store a config key and its value in database as VALUE_MIXED
514
+     *
515
+     * **WARNING:** Method is internal and **MUST** not be used as it is best to set a real value type
516
+     *
517
+     * @param string $app id of the app
518
+     * @param string $key config key
519
+     * @param string $value config value
520
+     * @param bool $lazy set config as lazy loaded
521
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
522
+     *
523
+     * @return bool TRUE if value was different, therefor updated in database
524
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED
525
+     * @internal
526
+     * @since 29.0.0
527
+     * @see IAppConfig for explanation about lazy loading
528
+     * @see setValueString()
529
+     * @see setValueInt()
530
+     * @see setValueFloat()
531
+     * @see setValueBool()
532
+     * @see setValueArray()
533
+     */
534
+    public function setValueMixed(
535
+        string $app,
536
+        string $key,
537
+        string $value,
538
+        bool $lazy = false,
539
+        bool $sensitive = false,
540
+    ): bool {
541
+        return $this->setTypedValue(
542
+            $app,
543
+            $key,
544
+            $value,
545
+            $lazy,
546
+            self::VALUE_MIXED | ($sensitive ? self::VALUE_SENSITIVE : 0)
547
+        );
548
+    }
549
+
550
+
551
+    /**
552
+     * @inheritDoc
553
+     *
554
+     * @param string $app id of the app
555
+     * @param string $key config key
556
+     * @param string $value config value
557
+     * @param bool $lazy set config as lazy loaded
558
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
559
+     *
560
+     * @return bool TRUE if value was different, therefor updated in database
561
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
562
+     * @since 29.0.0
563
+     * @see IAppConfig for explanation about lazy loading
564
+     */
565
+    public function setValueString(
566
+        string $app,
567
+        string $key,
568
+        string $value,
569
+        bool $lazy = false,
570
+        bool $sensitive = false,
571
+    ): bool {
572
+        return $this->setTypedValue(
573
+            $app,
574
+            $key,
575
+            $value,
576
+            $lazy,
577
+            self::VALUE_STRING | ($sensitive ? self::VALUE_SENSITIVE : 0)
578
+        );
579
+    }
580
+
581
+    /**
582
+     * @inheritDoc
583
+     *
584
+     * @param string $app id of the app
585
+     * @param string $key config key
586
+     * @param int $value config value
587
+     * @param bool $lazy set config as lazy loaded
588
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
589
+     *
590
+     * @return bool TRUE if value was different, therefor updated in database
591
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
592
+     * @since 29.0.0
593
+     * @see IAppConfig for explanation about lazy loading
594
+     */
595
+    public function setValueInt(
596
+        string $app,
597
+        string $key,
598
+        int $value,
599
+        bool $lazy = false,
600
+        bool $sensitive = false,
601
+    ): bool {
602
+        if ($value > 2000000000) {
603
+            $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.');
604
+        }
605
+
606
+        return $this->setTypedValue(
607
+            $app,
608
+            $key,
609
+            (string)$value,
610
+            $lazy,
611
+            self::VALUE_INT | ($sensitive ? self::VALUE_SENSITIVE : 0)
612
+        );
613
+    }
614
+
615
+    /**
616
+     * @inheritDoc
617
+     *
618
+     * @param string $app id of the app
619
+     * @param string $key config key
620
+     * @param float $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 setValueFloat(
630
+        string $app,
631
+        string $key,
632
+        float $value,
633
+        bool $lazy = false,
634
+        bool $sensitive = false,
635
+    ): bool {
636
+        return $this->setTypedValue(
637
+            $app,
638
+            $key,
639
+            (string)$value,
640
+            $lazy,
641
+            self::VALUE_FLOAT | ($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 bool $value config value
651
+     * @param bool $lazy set config as lazy loaded
652
+     *
653
+     * @return bool TRUE if value was different, therefor updated in database
654
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
655
+     * @since 29.0.0
656
+     * @see IAppConfig for explanation about lazy loading
657
+     */
658
+    public function setValueBool(
659
+        string $app,
660
+        string $key,
661
+        bool $value,
662
+        bool $lazy = false,
663
+    ): bool {
664
+        return $this->setTypedValue(
665
+            $app,
666
+            $key,
667
+            ($value) ? '1' : '0',
668
+            $lazy,
669
+            self::VALUE_BOOL
670
+        );
671
+    }
672
+
673
+    /**
674
+     * @inheritDoc
675
+     *
676
+     * @param string $app id of the app
677
+     * @param string $key config key
678
+     * @param array $value config value
679
+     * @param bool $lazy set config as lazy loaded
680
+     * @param bool $sensitive if TRUE value will be hidden when listing config values.
681
+     *
682
+     * @return bool TRUE if value was different, therefor updated in database
683
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
684
+     * @throws JsonException
685
+     * @since 29.0.0
686
+     * @see IAppConfig for explanation about lazy loading
687
+     */
688
+    public function setValueArray(
689
+        string $app,
690
+        string $key,
691
+        array $value,
692
+        bool $lazy = false,
693
+        bool $sensitive = false,
694
+    ): bool {
695
+        try {
696
+            return $this->setTypedValue(
697
+                $app,
698
+                $key,
699
+                json_encode($value, JSON_THROW_ON_ERROR),
700
+                $lazy,
701
+                self::VALUE_ARRAY | ($sensitive ? self::VALUE_SENSITIVE : 0)
702
+            );
703
+        } catch (JsonException $e) {
704
+            $this->logger->warning('could not setValueArray', ['app' => $app, 'key' => $key, 'exception' => $e]);
705
+            throw $e;
706
+        }
707
+    }
708
+
709
+    /**
710
+     * Store a config key and its value in database
711
+     *
712
+     * If config key is already known with the exact same config value and same sensitive/lazy status, the
713
+     * database is not updated. If config value was previously stored as sensitive, status will not be
714
+     * altered.
715
+     *
716
+     * @param string $app id of the app
717
+     * @param string $key config key
718
+     * @param string $value config value
719
+     * @param bool $lazy config set as lazy loaded
720
+     * @param int $type value type {@see VALUE_STRING} {@see VALUE_INT} {@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
721
+     *
722
+     * @return bool TRUE if value was updated in database
723
+     * @throws AppConfigTypeConflictException if type from database is not VALUE_MIXED and different from the requested one
724
+     * @see IAppConfig for explanation about lazy loading
725
+     */
726
+    private function setTypedValue(
727
+        string $app,
728
+        string $key,
729
+        string $value,
730
+        bool $lazy,
731
+        int $type,
732
+    ): bool {
733
+        $this->assertParams($app, $key);
734
+        if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type)) {
735
+            return false; // returns false as database is not updated
736
+        }
737
+        $this->loadConfig(null, $lazy);
738
+
739
+        $sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type);
740
+        $inserted = $refreshCache = false;
741
+
742
+        $origValue = $value;
743
+        if ($sensitive || ($this->hasKey($app, $key, $lazy) && $this->isSensitive($app, $key, $lazy))) {
744
+            $value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
745
+        }
746
+
747
+        if ($this->hasKey($app, $key, $lazy)) {
748
+            /**
749
+             * no update if key is already known with set lazy status and value is
750
+             * not different, unless sensitivity is switched from false to true.
751
+             */
752
+            if ($origValue === $this->getTypedValue($app, $key, $value, $lazy, $type)
753
+                && (!$sensitive || $this->isSensitive($app, $key, $lazy))) {
754
+                return false;
755
+            }
756
+        } else {
757
+            /**
758
+             * if key is not known yet, we try to insert.
759
+             * It might fail if the key exists with a different lazy flag.
760
+             */
761
+            try {
762
+                $insert = $this->connection->getQueryBuilder();
763
+                $insert->insert('appconfig')
764
+                    ->setValue('appid', $insert->createNamedParameter($app))
765
+                    ->setValue('lazy', $insert->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
766
+                    ->setValue('type', $insert->createNamedParameter($type, IQueryBuilder::PARAM_INT))
767
+                    ->setValue('configkey', $insert->createNamedParameter($key))
768
+                    ->setValue('configvalue', $insert->createNamedParameter($value));
769
+                $insert->executeStatement();
770
+                $inserted = true;
771
+            } catch (DBException $e) {
772
+                if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
773
+                    throw $e; // TODO: throw exception or just log and returns false !?
774
+                }
775
+            }
776
+        }
777
+
778
+        /**
779
+         * We cannot insert a new row, meaning we need to update an already existing one
780
+         */
781
+        if (!$inserted) {
782
+            $currType = $this->valueTypes[$app][$key] ?? 0;
783
+            if ($currType === 0) { // this might happen when switching lazy loading status
784
+                $this->loadConfigAll();
785
+                $currType = $this->valueTypes[$app][$key] ?? 0;
786
+            }
787
+
788
+            /**
789
+             * This should only happen during the upgrade process from 28 to 29.
790
+             * We only log a warning and set it to VALUE_MIXED.
791
+             */
792
+            if ($currType === 0) {
793
+                $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]);
794
+                $currType = self::VALUE_MIXED;
795
+            }
796
+
797
+            /**
798
+             * we only accept a different type from the one stored in database
799
+             * if the one stored in database is not-defined (VALUE_MIXED)
800
+             */
801
+            if (!$this->isTyped(self::VALUE_MIXED, $currType) &&
802
+                ($type | self::VALUE_SENSITIVE) !== ($currType | self::VALUE_SENSITIVE)) {
803
+                try {
804
+                    $currType = $this->convertTypeToString($currType);
805
+                    $type = $this->convertTypeToString($type);
806
+                } catch (AppConfigIncorrectTypeException) {
807
+                    // can be ignored, this was just needed for a better exception message.
808
+                }
809
+                throw new AppConfigTypeConflictException('conflict between new type (' . $type . ') and old type (' . $currType . ')');
810
+            }
811
+
812
+            // we fix $type if the stored value, or the new value as it might be changed, is set as sensitive
813
+            if ($sensitive || $this->isTyped(self::VALUE_SENSITIVE, $currType)) {
814
+                $type |= self::VALUE_SENSITIVE;
815
+            }
816
+
817
+            if ($lazy !== $this->isLazy($app, $key)) {
818
+                $refreshCache = true;
819
+            }
820
+
821
+            $update = $this->connection->getQueryBuilder();
822
+            $update->update('appconfig')
823
+                ->set('configvalue', $update->createNamedParameter($value))
824
+                ->set('lazy', $update->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
825
+                ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
826
+                ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
827
+                ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
828
+
829
+            $update->executeStatement();
830
+        }
831
+
832
+        if ($refreshCache) {
833
+            $this->clearCache();
834
+            return true;
835
+        }
836
+
837
+        // update local cache
838
+        if ($lazy) {
839
+            $this->lazyCache[$app][$key] = $value;
840
+        } else {
841
+            $this->fastCache[$app][$key] = $value;
842
+        }
843
+        $this->valueTypes[$app][$key] = $type;
844
+
845
+        return true;
846
+    }
847
+
848
+    /**
849
+     * Change the type of config value.
850
+     *
851
+     * **WARNING:** Method is internal and **MUST** not be used as it may break things.
852
+     *
853
+     * @param string $app id of the app
854
+     * @param string $key config key
855
+     * @param int $type value type {@see VALUE_STRING} {@see VALUE_INT} {@see VALUE_FLOAT} {@see VALUE_BOOL} {@see VALUE_ARRAY}
856
+     *
857
+     * @return bool TRUE if database update were necessary
858
+     * @throws AppConfigUnknownKeyException if $key is now known in database
859
+     * @throws AppConfigIncorrectTypeException if $type is not valid
860
+     * @internal
861
+     * @since 29.0.0
862
+     */
863
+    public function updateType(string $app, string $key, int $type = self::VALUE_MIXED): bool {
864
+        $this->assertParams($app, $key);
865
+        $this->loadConfigAll();
866
+        $lazy = $this->isLazy($app, $key);
867
+
868
+        // type can only be one type
869
+        if (!in_array($type, [self::VALUE_MIXED, self::VALUE_STRING, self::VALUE_INT, self::VALUE_FLOAT, self::VALUE_BOOL, self::VALUE_ARRAY])) {
870
+            throw new AppConfigIncorrectTypeException('Unknown value type');
871
+        }
872
+
873
+        $currType = $this->valueTypes[$app][$key];
874
+        if (($type | self::VALUE_SENSITIVE) === ($currType | self::VALUE_SENSITIVE)) {
875
+            return false;
876
+        }
877
+
878
+        // we complete with sensitive flag if the stored value is set as sensitive
879
+        if ($this->isTyped(self::VALUE_SENSITIVE, $currType)) {
880
+            $type = $type | self::VALUE_SENSITIVE;
881
+        }
882
+
883
+        $update = $this->connection->getQueryBuilder();
884
+        $update->update('appconfig')
885
+            ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
886
+            ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
887
+            ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
888
+        $update->executeStatement();
889
+        $this->valueTypes[$app][$key] = $type;
890
+
891
+        return true;
892
+    }
893
+
894
+
895
+    /**
896
+     * @inheritDoc
897
+     *
898
+     * @param string $app id of the app
899
+     * @param string $key config key
900
+     * @param bool $sensitive TRUE to set as sensitive, FALSE to unset
901
+     *
902
+     * @return bool TRUE if entry was found in database and an update was necessary
903
+     * @since 29.0.0
904
+     */
905
+    public function updateSensitive(string $app, string $key, bool $sensitive): bool {
906
+        $this->assertParams($app, $key);
907
+        $this->loadConfigAll();
908
+
909
+        try {
910
+            if ($sensitive === $this->isSensitive($app, $key, null)) {
911
+                return false;
912
+            }
913
+        } catch (AppConfigUnknownKeyException $e) {
914
+            return false;
915
+        }
916
+
917
+        $lazy = $this->isLazy($app, $key);
918
+        if ($lazy) {
919
+            $cache = $this->lazyCache;
920
+        } else {
921
+            $cache = $this->fastCache;
922
+        }
923
+
924
+        if (!isset($cache[$app][$key])) {
925
+            throw new AppConfigUnknownKeyException('unknown config key');
926
+        }
927
+
928
+        /**
929
+         * type returned by getValueType() is already cleaned from sensitive flag
930
+         * we just need to update it based on $sensitive and store it in database
931
+         */
932
+        $type = $this->getValueType($app, $key);
933
+        $value = $cache[$app][$key];
934
+        if ($sensitive) {
935
+            $type |= self::VALUE_SENSITIVE;
936
+            $value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
937
+        } else {
938
+            $value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
939
+        }
940
+
941
+        $update = $this->connection->getQueryBuilder();
942
+        $update->update('appconfig')
943
+            ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
944
+            ->set('configvalue', $update->createNamedParameter($value))
945
+            ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
946
+            ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
947
+        $update->executeStatement();
948
+
949
+        $this->valueTypes[$app][$key] = $type;
950
+
951
+        return true;
952
+    }
953
+
954
+    /**
955
+     * @inheritDoc
956
+     *
957
+     * @param string $app id of the app
958
+     * @param string $key config key
959
+     * @param bool $lazy TRUE to set as lazy loaded, FALSE to unset
960
+     *
961
+     * @return bool TRUE if entry was found in database and an update was necessary
962
+     * @since 29.0.0
963
+     */
964
+    public function updateLazy(string $app, string $key, bool $lazy): bool {
965
+        $this->assertParams($app, $key);
966
+        $this->loadConfigAll();
967
+
968
+        try {
969
+            if ($lazy === $this->isLazy($app, $key)) {
970
+                return false;
971
+            }
972
+        } catch (AppConfigUnknownKeyException $e) {
973
+            return false;
974
+        }
975
+
976
+        $update = $this->connection->getQueryBuilder();
977
+        $update->update('appconfig')
978
+            ->set('lazy', $update->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))
979
+            ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
980
+            ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
981
+        $update->executeStatement();
982
+
983
+        // At this point, it is a lot safer to clean cache
984
+        $this->clearCache();
985
+
986
+        return true;
987
+    }
988
+
989
+    /**
990
+     * @inheritDoc
991
+     *
992
+     * @param string $app id of the app
993
+     * @param string $key config key
994
+     *
995
+     * @return array
996
+     * @throws AppConfigUnknownKeyException if config key is not known in database
997
+     * @since 29.0.0
998
+     */
999
+    public function getDetails(string $app, string $key): array {
1000
+        $this->assertParams($app, $key);
1001
+        $this->loadConfigAll();
1002
+        $lazy = $this->isLazy($app, $key);
1003
+
1004
+        if ($lazy) {
1005
+            $cache = $this->lazyCache;
1006
+        } else {
1007
+            $cache = $this->fastCache;
1008
+        }
1009
+
1010
+        $type = $this->getValueType($app, $key);
1011
+        try {
1012
+            $typeString = $this->convertTypeToString($type);
1013
+        } catch (AppConfigIncorrectTypeException $e) {
1014
+            $this->logger->warning('type stored in database is not correct', ['exception' => $e, 'type' => $type]);
1015
+            $typeString = (string)$type;
1016
+        }
1017
+
1018
+        if (!isset($cache[$app][$key])) {
1019
+            throw new AppConfigUnknownKeyException('unknown config key');
1020
+        }
1021
+
1022
+        $value = $cache[$app][$key];
1023
+        $sensitive = $this->isSensitive($app, $key, null);
1024
+        if ($sensitive && str_starts_with($value, self::ENCRYPTION_PREFIX)) {
1025
+            $value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
1026
+        }
1027
+
1028
+        return [
1029
+            'app' => $app,
1030
+            'key' => $key,
1031
+            'value' => $value,
1032
+            'type' => $type,
1033
+            'lazy' => $lazy,
1034
+            'typeString' => $typeString,
1035
+            'sensitive' => $sensitive
1036
+        ];
1037
+    }
1038
+
1039
+    /**
1040
+     * @param string $type
1041
+     *
1042
+     * @return int
1043
+     * @throws AppConfigIncorrectTypeException
1044
+     * @since 29.0.0
1045
+     */
1046
+    public function convertTypeToInt(string $type): int {
1047
+        return match (strtolower($type)) {
1048
+            'mixed' => IAppConfig::VALUE_MIXED,
1049
+            'string' => IAppConfig::VALUE_STRING,
1050
+            'integer' => IAppConfig::VALUE_INT,
1051
+            'float' => IAppConfig::VALUE_FLOAT,
1052
+            'boolean' => IAppConfig::VALUE_BOOL,
1053
+            'array' => IAppConfig::VALUE_ARRAY,
1054
+            default => throw new AppConfigIncorrectTypeException('Unknown type ' . $type)
1055
+        };
1056
+    }
1057
+
1058
+    /**
1059
+     * @param int $type
1060
+     *
1061
+     * @return string
1062
+     * @throws AppConfigIncorrectTypeException
1063
+     * @since 29.0.0
1064
+     */
1065
+    public function convertTypeToString(int $type): string {
1066
+        $type &= ~self::VALUE_SENSITIVE;
1067
+
1068
+        return match ($type) {
1069
+            IAppConfig::VALUE_MIXED => 'mixed',
1070
+            IAppConfig::VALUE_STRING => 'string',
1071
+            IAppConfig::VALUE_INT => 'integer',
1072
+            IAppConfig::VALUE_FLOAT => 'float',
1073
+            IAppConfig::VALUE_BOOL => 'boolean',
1074
+            IAppConfig::VALUE_ARRAY => 'array',
1075
+            default => throw new AppConfigIncorrectTypeException('Unknown numeric type ' . $type)
1076
+        };
1077
+    }
1078
+
1079
+    /**
1080
+     * @inheritDoc
1081
+     *
1082
+     * @param string $app id of the app
1083
+     * @param string $key config key
1084
+     *
1085
+     * @since 29.0.0
1086
+     */
1087
+    public function deleteKey(string $app, string $key): void {
1088
+        $this->assertParams($app, $key);
1089
+        $qb = $this->connection->getQueryBuilder();
1090
+        $qb->delete('appconfig')
1091
+            ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
1092
+            ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
1093
+        $qb->executeStatement();
1094
+
1095
+        unset($this->lazyCache[$app][$key]);
1096
+        unset($this->fastCache[$app][$key]);
1097
+        unset($this->valueTypes[$app][$key]);
1098
+    }
1099
+
1100
+    /**
1101
+     * @inheritDoc
1102
+     *
1103
+     * @param string $app id of the app
1104
+     *
1105
+     * @since 29.0.0
1106
+     */
1107
+    public function deleteApp(string $app): void {
1108
+        $this->assertParams($app);
1109
+        $qb = $this->connection->getQueryBuilder();
1110
+        $qb->delete('appconfig')
1111
+            ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
1112
+        $qb->executeStatement();
1113
+
1114
+        $this->clearCache();
1115
+    }
1116
+
1117
+    /**
1118
+     * @inheritDoc
1119
+     *
1120
+     * @param bool $reload set to TRUE to refill cache instantly after clearing it
1121
+     *
1122
+     * @since 29.0.0
1123
+     */
1124
+    public function clearCache(bool $reload = false): void {
1125
+        $this->lazyLoaded = $this->fastLoaded = false;
1126
+        $this->lazyCache = $this->fastCache = $this->valueTypes = [];
1127
+
1128
+        if (!$reload) {
1129
+            return;
1130
+        }
1131
+
1132
+        $this->loadConfigAll();
1133
+    }
1134
+
1135
+
1136
+    /**
1137
+     * For debug purpose.
1138
+     * Returns the cached data.
1139
+     *
1140
+     * @return array
1141
+     * @since 29.0.0
1142
+     * @internal
1143
+     */
1144
+    public function statusCache(): array {
1145
+        return [
1146
+            'fastLoaded' => $this->fastLoaded,
1147
+            'fastCache' => $this->fastCache,
1148
+            'lazyLoaded' => $this->lazyLoaded,
1149
+            'lazyCache' => $this->lazyCache,
1150
+        ];
1151
+    }
1152
+
1153
+    /**
1154
+     * @param int $needle bitflag to search
1155
+     * @param int $type known value
1156
+     *
1157
+     * @return bool TRUE if bitflag $needle is set in $type
1158
+     */
1159
+    private function isTyped(int $needle, int $type): bool {
1160
+        return (($needle & $type) !== 0);
1161
+    }
1162
+
1163
+    /**
1164
+     * Confirm the string set for app and key fit the database description
1165
+     *
1166
+     * @param string $app assert $app fit in database
1167
+     * @param string $configKey assert config key fit in database
1168
+     * @param bool $allowEmptyApp $app can be empty string
1169
+     * @param int $valueType assert value type is only one type
1170
+     *
1171
+     * @throws InvalidArgumentException
1172
+     */
1173
+    private function assertParams(string $app = '', string $configKey = '', bool $allowEmptyApp = false, int $valueType = -1): void {
1174
+        if (!$allowEmptyApp && $app === '') {
1175
+            throw new InvalidArgumentException('app cannot be an empty string');
1176
+        }
1177
+        if (strlen($app) > self::APP_MAX_LENGTH) {
1178
+            throw new InvalidArgumentException(
1179
+                'Value (' . $app . ') for app is too long (' . self::APP_MAX_LENGTH . ')'
1180
+            );
1181
+        }
1182
+        if (strlen($configKey) > self::KEY_MAX_LENGTH) {
1183
+            throw new InvalidArgumentException('Value (' . $configKey . ') for key is too long (' . self::KEY_MAX_LENGTH . ')');
1184
+        }
1185
+        if ($valueType > -1) {
1186
+            $valueType &= ~self::VALUE_SENSITIVE;
1187
+            if (!in_array($valueType, [self::VALUE_MIXED, self::VALUE_STRING, self::VALUE_INT, self::VALUE_FLOAT, self::VALUE_BOOL, self::VALUE_ARRAY])) {
1188
+                throw new InvalidArgumentException('Unknown value type');
1189
+            }
1190
+        }
1191
+    }
1192
+
1193
+    private function loadConfigAll(?string $app = null): void {
1194
+        $this->loadConfig($app, null);
1195
+    }
1196
+
1197
+    /**
1198
+     * Load normal config or config set as lazy loaded
1199
+     *
1200
+     * @param bool|null $lazy set to TRUE to load config set as lazy loaded, set to NULL to load all config
1201
+     */
1202
+    private function loadConfig(?string $app = null, ?bool $lazy = false): void {
1203
+        if ($this->isLoaded($lazy)) {
1204
+            return;
1205
+        }
1206
+
1207
+        // if lazy is null or true, we debug log
1208
+        if (($lazy ?? true) !== false && $app !== null) {
1209
+            $exception = new \RuntimeException('The loading of lazy AppConfig values have been triggered by app "' . $app . '"');
1210
+            $this->logger->debug($exception->getMessage(), ['exception' => $exception, 'app' => $app]);
1211
+        }
1212
+
1213
+        $qb = $this->connection->getQueryBuilder();
1214
+        $qb->from('appconfig');
1215
+
1216
+        // we only need value from lazy when loadConfig does not specify it
1217
+        $qb->select('appid', 'configkey', 'configvalue', 'type');
1218
+
1219
+        if ($lazy !== null) {
1220
+            $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT)));
1221
+        } else {
1222
+            $qb->addSelect('lazy');
1223
+        }
1224
+
1225
+        $result = $qb->executeQuery();
1226
+        $rows = $result->fetchAll();
1227
+        foreach ($rows as $row) {
1228
+            // most of the time, 'lazy' is not in the select because its value is already known
1229
+            if (($row['lazy'] ?? ($lazy ?? 0) ? 1 : 0) === 1) {
1230
+                $this->lazyCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1231
+            } else {
1232
+                $this->fastCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
1233
+            }
1234
+            $this->valueTypes[$row['appid']][$row['configkey']] = (int)($row['type'] ?? 0);
1235
+        }
1236
+        $result->closeCursor();
1237
+        $this->setAsLoaded($lazy);
1238
+    }
1239
+
1240
+    /**
1241
+     * if $lazy is:
1242
+     *  - false: will returns true if fast config is loaded
1243
+     *  - true : will returns true if lazy config is loaded
1244
+     *  - null : will returns true if both config are loaded
1245
+     *
1246
+     * @param bool $lazy
1247
+     *
1248
+     * @return bool
1249
+     */
1250
+    private function isLoaded(?bool $lazy): bool {
1251
+        if ($lazy === null) {
1252
+            return $this->lazyLoaded && $this->fastLoaded;
1253
+        }
1254
+
1255
+        return $lazy ? $this->lazyLoaded : $this->fastLoaded;
1256
+    }
1257
+
1258
+    /**
1259
+     * if $lazy is:
1260
+     * - false: set fast config as loaded
1261
+     * - true : set lazy config as loaded
1262
+     * - null : set both config as loaded
1263
+     *
1264
+     * @param bool $lazy
1265
+     */
1266
+    private function setAsLoaded(?bool $lazy): void {
1267
+        if ($lazy === null) {
1268
+            $this->fastLoaded = true;
1269
+            $this->lazyLoaded = true;
1270
+
1271
+            return;
1272
+        }
1273
+
1274
+        if ($lazy) {
1275
+            $this->lazyLoaded = true;
1276
+        } else {
1277
+            $this->fastLoaded = true;
1278
+        }
1279
+    }
1280
+
1281
+    /**
1282
+     * Gets the config value
1283
+     *
1284
+     * @param string $app app
1285
+     * @param string $key key
1286
+     * @param string $default = null, default value if the key does not exist
1287
+     *
1288
+     * @return string the value or $default
1289
+     * @deprecated 29.0.0 use getValue*()
1290
+     *
1291
+     * This function gets a value from the appconfig table. If the key does
1292
+     * not exist the default value will be returned
1293
+     */
1294
+    public function getValue($app, $key, $default = null) {
1295
+        $this->loadConfig($app);
1296
+
1297
+        return $this->fastCache[$app][$key] ?? $default;
1298
+    }
1299
+
1300
+    /**
1301
+     * Sets a value. If the key did not exist before it will be created.
1302
+     *
1303
+     * @param string $app app
1304
+     * @param string $key key
1305
+     * @param string|float|int $value value
1306
+     *
1307
+     * @return bool True if the value was inserted or updated, false if the value was the same
1308
+     * @throws AppConfigTypeConflictException
1309
+     * @throws AppConfigUnknownKeyException
1310
+     * @deprecated 29.0.0
1311
+     */
1312
+    public function setValue($app, $key, $value) {
1313
+        /**
1314
+         * TODO: would it be overkill, or decently improve performance, to catch
1315
+         * call to this method with $key='enabled' and 'hide' config value related
1316
+         * to $app when the app is disabled (by modifying entry in database: lazy=lazy+2)
1317
+         * or enabled (lazy=lazy-2)
1318
+         *
1319
+         * this solution would remove the loading of config values from disabled app
1320
+         * unless calling the method {@see loadConfigAll()}
1321
+         */
1322
+        return $this->setTypedValue($app, $key, (string)$value, false, self::VALUE_MIXED);
1323
+    }
1324
+
1325
+
1326
+    /**
1327
+     * get multiple values, either the app or key can be used as wildcard by setting it to false
1328
+     *
1329
+     * @param string|false $app
1330
+     * @param string|false $key
1331
+     *
1332
+     * @return array|false
1333
+     * @deprecated 29.0.0 use {@see getAllValues()}
1334
+     */
1335
+    public function getValues($app, $key) {
1336
+        if (($app !== false) === ($key !== false)) {
1337
+            return false;
1338
+        }
1339
+
1340
+        $key = ($key === false) ? '' : $key;
1341
+        if (!$app) {
1342
+            return $this->searchValues($key, false, self::VALUE_MIXED);
1343
+        } else {
1344
+            return $this->getAllValues($app, $key);
1345
+        }
1346
+    }
1347
+
1348
+    /**
1349
+     * get all values of the app or and filters out sensitive data
1350
+     *
1351
+     * @param string $app
1352
+     *
1353
+     * @return array
1354
+     * @deprecated 29.0.0 use {@see getAllValues()}
1355
+     */
1356
+    public function getFilteredValues($app) {
1357
+        return $this->getAllValues($app, filtered: true);
1358
+    }
1359
+
1360
+
1361
+    /**
1362
+     * **Warning:** avoid default NULL value for $lazy as this will
1363
+     * load all lazy values from the database
1364
+     *
1365
+     * @param string $app
1366
+     * @param array<string, string> $values ['key' => 'value']
1367
+     * @param bool|null $lazy
1368
+     *
1369
+     * @return array<string, string|int|float|bool|array>
1370
+     */
1371
+    private function formatAppValues(string $app, array $values, ?bool $lazy = null): array {
1372
+        foreach ($values as $key => $value) {
1373
+            try {
1374
+                $type = $this->getValueType($app, $key, $lazy);
1375
+            } catch (AppConfigUnknownKeyException $e) {
1376
+                continue;
1377
+            }
1378
+
1379
+            $values[$key] = $this->convertTypedValue($value, $type);
1380
+        }
1381
+
1382
+        return $values;
1383
+    }
1384
+
1385
+    /**
1386
+     * convert string value to the expected type
1387
+     *
1388
+     * @param string $value
1389
+     * @param int $type
1390
+     *
1391
+     * @return string|int|float|bool|array
1392
+     */
1393
+    private function convertTypedValue(string $value, int $type): string|int|float|bool|array {
1394
+        switch ($type) {
1395
+            case self::VALUE_INT:
1396
+                return (int)$value;
1397
+            case self::VALUE_FLOAT:
1398
+                return (float)$value;
1399
+            case self::VALUE_BOOL:
1400
+                return in_array(strtolower($value), ['1', 'true', 'yes', 'on']);
1401
+            case self::VALUE_ARRAY:
1402
+                try {
1403
+                    return json_decode($value, true, flags: JSON_THROW_ON_ERROR);
1404
+                } catch (JsonException $e) {
1405
+                    // ignoreable
1406
+                }
1407
+                break;
1408
+        }
1409
+        return $value;
1410
+    }
1411
+
1412
+    /**
1413
+     * @param string $app
1414
+     *
1415
+     * @return string[]
1416
+     * @deprecated 29.0.0 data sensitivity should be set when calling setValue*()
1417
+     */
1418
+    private function getSensitiveKeys(string $app): array {
1419
+        $sensitiveValues = [
1420
+            'circles' => [
1421
+                '/^key_pairs$/',
1422
+                '/^local_gskey$/',
1423
+            ],
1424
+            'call_summary_bot' => [
1425
+                '/^secret_(.*)$/',
1426
+            ],
1427
+            'external' => [
1428
+                '/^sites$/',
1429
+                '/^jwt_token_privkey_(.*)$/',
1430
+            ],
1431
+            'globalsiteselector' => [
1432
+                '/^gss\.jwt\.key$/',
1433
+            ],
1434
+            'gpgmailer' => [
1435
+                '/^GpgServerKey$/',
1436
+            ],
1437
+            'integration_discourse' => [
1438
+                '/^private_key$/',
1439
+                '/^public_key$/',
1440
+            ],
1441
+            'integration_dropbox' => [
1442
+                '/^client_id$/',
1443
+                '/^client_secret$/',
1444
+            ],
1445
+            'integration_github' => [
1446
+                '/^client_id$/',
1447
+                '/^client_secret$/',
1448
+            ],
1449
+            'integration_gitlab' => [
1450
+                '/^client_id$/',
1451
+                '/^client_secret$/',
1452
+                '/^oauth_instance_url$/',
1453
+            ],
1454
+            'integration_google' => [
1455
+                '/^client_id$/',
1456
+                '/^client_secret$/',
1457
+            ],
1458
+            'integration_jira' => [
1459
+                '/^client_id$/',
1460
+                '/^client_secret$/',
1461
+                '/^forced_instance_url$/',
1462
+            ],
1463
+            'integration_onedrive' => [
1464
+                '/^client_id$/',
1465
+                '/^client_secret$/',
1466
+            ],
1467
+            'integration_openproject' => [
1468
+                '/^client_id$/',
1469
+                '/^client_secret$/',
1470
+                '/^oauth_instance_url$/',
1471
+            ],
1472
+            'integration_reddit' => [
1473
+                '/^client_id$/',
1474
+                '/^client_secret$/',
1475
+            ],
1476
+            'integration_suitecrm' => [
1477
+                '/^client_id$/',
1478
+                '/^client_secret$/',
1479
+                '/^oauth_instance_url$/',
1480
+            ],
1481
+            'integration_twitter' => [
1482
+                '/^consumer_key$/',
1483
+                '/^consumer_secret$/',
1484
+                '/^followed_user$/',
1485
+            ],
1486
+            'integration_zammad' => [
1487
+                '/^client_id$/',
1488
+                '/^client_secret$/',
1489
+                '/^oauth_instance_url$/',
1490
+            ],
1491
+            'maps' => [
1492
+                '/^mapboxAPIKEY$/',
1493
+            ],
1494
+            'notify_push' => [
1495
+                '/^cookie$/',
1496
+            ],
1497
+            'onlyoffice' => [
1498
+                '/^jwt_secret$/',
1499
+            ],
1500
+            'passwords' => [
1501
+                '/^SSEv1ServerKey$/',
1502
+            ],
1503
+            'serverinfo' => [
1504
+                '/^token$/',
1505
+            ],
1506
+            'spreed' => [
1507
+                '/^bridge_bot_password$/',
1508
+                '/^hosted-signaling-server-(.*)$/',
1509
+                '/^recording_servers$/',
1510
+                '/^signaling_servers$/',
1511
+                '/^signaling_ticket_secret$/',
1512
+                '/^signaling_token_privkey_(.*)$/',
1513
+                '/^signaling_token_pubkey_(.*)$/',
1514
+                '/^sip_bridge_dialin_info$/',
1515
+                '/^sip_bridge_shared_secret$/',
1516
+                '/^stun_servers$/',
1517
+                '/^turn_servers$/',
1518
+                '/^turn_server_secret$/',
1519
+            ],
1520
+            'support' => [
1521
+                '/^last_response$/',
1522
+                '/^potential_subscription_key$/',
1523
+                '/^subscription_key$/',
1524
+            ],
1525
+            'theming' => [
1526
+                '/^imprintUrl$/',
1527
+                '/^privacyUrl$/',
1528
+                '/^slogan$/',
1529
+                '/^url$/',
1530
+            ],
1531
+            'twofactor_gateway' => [
1532
+                '/^.*token$/',
1533
+            ],
1534
+            'user_ldap' => [
1535
+                '/^(s..)?ldap_agent_password$/',
1536
+            ],
1537
+            'user_saml' => [
1538
+                '/^idp-x509cert$/',
1539
+            ],
1540
+            'whiteboard' => [
1541
+                '/^jwt_secret_key$/',
1542
+            ],
1543
+        ];
1544
+
1545
+        return $sensitiveValues[$app] ?? [];
1546
+    }
1547
+
1548
+    /**
1549
+     * Clear all the cached app config values
1550
+     * New cache will be generated next time a config value is retrieved
1551
+     *
1552
+     * @deprecated 29.0.0 use {@see clearCache()}
1553
+     */
1554
+    public function clearCachedConfig(): void {
1555
+        $this->clearCache();
1556
+    }
1557
+
1558
+    /**
1559
+     * match and apply current use of config values with defined lexicon
1560
+     *
1561
+     * @throws AppConfigUnknownKeyException
1562
+     * @throws AppConfigTypeConflictException
1563
+     * @return bool TRUE if everything is fine compared to lexicon or lexicon does not exist
1564
+     */
1565
+    private function matchAndApplyLexiconDefinition(
1566
+        string $app,
1567
+        string $key,
1568
+        bool &$lazy,
1569
+        int &$type,
1570
+        string &$default = '',
1571
+    ): bool {
1572
+        if (in_array($key,
1573
+            [
1574
+                'enabled',
1575
+                'installed_version',
1576
+                'types',
1577
+            ])) {
1578
+            return true; // we don't break stuff for this list of config keys.
1579
+        }
1580
+        $configDetails = $this->getConfigDetailsFromLexicon($app);
1581
+        if (!array_key_exists($key, $configDetails['entries'])) {
1582
+            return $this->applyLexiconStrictness(
1583
+                $configDetails['strictness'],
1584
+                'The app config key ' . $app . '/' . $key . ' is not defined in the config lexicon'
1585
+            );
1586
+        }
1587
+
1588
+        /** @var ConfigLexiconEntry $configValue */
1589
+        $configValue = $configDetails['entries'][$key];
1590
+        $type &= ~self::VALUE_SENSITIVE;
1591
+
1592
+        $appConfigValueType = $configValue->getValueType()->toAppConfigFlag();
1593
+        if ($type === self::VALUE_MIXED) {
1594
+            $type = $appConfigValueType; // we overwrite if value was requested as mixed
1595
+        } elseif ($appConfigValueType !== $type) {
1596
+            throw new AppConfigTypeConflictException('The app config key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
1597
+        }
1598
+
1599
+        $lazy = $configValue->isLazy();
1600
+        $default = $configValue->getDefault() ?? $default; // default from Lexicon got priority
1601
+        if ($configValue->isFlagged(self::FLAG_SENSITIVE)) {
1602
+            $type |= self::VALUE_SENSITIVE;
1603
+        }
1604
+        if ($configValue->isDeprecated()) {
1605
+            $this->logger->notice('App config key ' . $app . '/' . $key . ' is set as deprecated.');
1606
+        }
1607
+
1608
+        return true;
1609
+    }
1610
+
1611
+    /**
1612
+     * manage ConfigLexicon behavior based on strictness set in IConfigLexicon
1613
+     *
1614
+     * @param ConfigLexiconStrictness|null $strictness
1615
+     * @param string $line
1616
+     *
1617
+     * @return bool TRUE if conflict can be fully ignored, FALSE if action should be not performed
1618
+     * @throws AppConfigUnknownKeyException if strictness implies exception
1619
+     * @see IConfigLexicon::getStrictness()
1620
+     */
1621
+    private function applyLexiconStrictness(
1622
+        ?ConfigLexiconStrictness $strictness,
1623
+        string $line = '',
1624
+    ): bool {
1625
+        if ($strictness === null) {
1626
+            return true;
1627
+        }
1628
+
1629
+        switch ($strictness) {
1630
+            case ConfigLexiconStrictness::IGNORE:
1631
+                return true;
1632
+            case ConfigLexiconStrictness::NOTICE:
1633
+                $this->logger->notice($line);
1634
+                return true;
1635
+            case ConfigLexiconStrictness::WARNING:
1636
+                $this->logger->warning($line);
1637
+                return false;
1638
+        }
1639
+
1640
+        throw new AppConfigUnknownKeyException($line);
1641
+    }
1642
+
1643
+    /**
1644
+     * extract details from registered $appId's config lexicon
1645
+     *
1646
+     * @param string $appId
1647
+     *
1648
+     * @return array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}
1649
+     */
1650
+    private function getConfigDetailsFromLexicon(string $appId): array {
1651
+        if (!array_key_exists($appId, $this->configLexiconDetails)) {
1652
+            $entries = [];
1653
+            $bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
1654
+            $configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
1655
+            foreach ($configLexicon?->getAppConfigs() ?? [] as $configEntry) {
1656
+                $entries[$configEntry->getKey()] = $configEntry;
1657
+            }
1658
+
1659
+            $this->configLexiconDetails[$appId] = [
1660
+                'entries' => $entries,
1661
+                'strictness' => $configLexicon?->getStrictness() ?? ConfigLexiconStrictness::IGNORE
1662
+            ];
1663
+        }
1664
+
1665
+        return $this->configLexiconDetails[$appId];
1666
+    }
1667
+
1668
+    /**
1669
+     * Returns the installed versions of all apps
1670
+     *
1671
+     * @return array<string, string>
1672
+     */
1673
+    public function getAppInstalledVersions(): array {
1674
+        if ($this->appVersionsCache === null) {
1675
+            /** @var array<string, string> */
1676
+            $this->appVersionsCache = $this->searchValues('installed_version', false, IAppConfig::VALUE_STRING);
1677
+        }
1678
+        return $this->appVersionsCache;
1679
+    }
1680 1680
 }
Please login to merge, or discard this patch.