AppConfig.php 49 KB

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