UserConfig.php 58 KB

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