1
0

AppConfig.php 45 KB

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