AppConfig.php 45 KB

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