1
0

UserPreferences.php 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910
  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\IUserPreferences;
  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 preferences in the
  28. * database.
  29. * Supports **lazy loading**
  30. *
  31. * ### What is lazy loading ?
  32. * In order to avoid loading useless user preferences 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 UserPreferences implements IUserPreferences {
  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 = '$UserPreferencesEncryption$';
  49. private const ENCRYPTION_PREFIX_LENGTH = 27; // 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 preference keys
  52. /** @var array<string, array<string, array<string, mixed>>> ['user_id' => ['app_id' => ['key' => 'value']]] */
  53. private array $lazyCache = []; // cache for lazy preference 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 preference values
  56. /** @var array<string, array<string, array<string, ValueType>>> ['user_id' => ['app_id' => ['key' => bitflag]]] */
  57. private array $valueTypes = []; // type for all preference values
  58. /** @var array<string, array<string, array<string, int>>> ['user_id' => ['app_id' => ['key' => bitflag]]] */
  59. private array $valueFlags = []; // type for all preference 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->loadPreferencesAll($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 preference keys
  117. * @since 31.0.0
  118. */
  119. public function getKeys(string $userId, string $app): array {
  120. $this->assertParams($userId, $app);
  121. $this->loadPreferencesAll($userId);
  122. // array_merge() will remove numeric keys (here preference 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 preference key
  133. * @param bool|null $lazy TRUE to search within lazy loaded preferences, NULL to search within all preferences
  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->loadPreferences($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 preference key
  156. * @param bool|null $lazy TRUE to search within lazy loaded preferences, NULL to search within all preferences
  157. *
  158. * @return bool
  159. * @throws UnknownKeyException if preference 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->loadPreferences($userId, $lazy);
  165. if (!isset($this->valueDetails[$userId][$app][$key])) {
  166. throw new UnknownKeyException('unknown preference 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 preference key
  176. * @param bool|null $lazy TRUE to search within lazy loaded preferences, NULL to search within all preferences
  177. *
  178. * @return bool
  179. * @throws UnknownKeyException if preference 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->loadPreferences($userId, $lazy);
  185. if (!isset($this->valueDetails[$userId][$app][$key])) {
  186. throw new UnknownKeyException('unknown preference 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 preference key
  196. *
  197. * @return bool TRUE if preference is lazy loaded
  198. * @throws UnknownKeyException if preference key is not known
  199. * @see IUserPreferences 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 preferences 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 preferences
  209. if ($this->hasKey($userId, $app, $key, true)) {
  210. return true;
  211. }
  212. throw new UnknownKeyException('unknown preference 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 preference keys prefix to search
  220. * @param bool $filtered TRUE to hide sensitive preference 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->loadPreferencesAll($userId);
  234. // array_merge() will remove numeric keys (here preference 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 preference 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->loadPreferencesAll($userId);
  255. $result = [];
  256. foreach ($this->getApps($userId) as $app) {
  257. // array_merge() will remove numeric keys (here preference 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 preference key
  268. * @param bool $lazy search within lazy loaded preferences
  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->loadPreferences($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 preference 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 preference key
  355. * @param string $value preference 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 preference key
  369. * @param int $value preference 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 preference key
  382. * @param array $values list of preference 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 preference key
  395. * @param bool $value preference 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 preference 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 preference 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 preference key
  472. * @param string $default preference value
  473. * @param null|bool $lazy get preference 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 IUserPreferences 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 preference key
  513. * @param string $default default value
  514. * @param bool $lazy search within lazy loaded preferences
  515. *
  516. * @return string stored preference 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 IUserPreferences 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 preference key
  537. * @param int $default default value
  538. * @param bool $lazy search within lazy loaded preferences
  539. *
  540. * @return int stored preference 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 IUserPreferences 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 preference key
  561. * @param float $default default value
  562. * @param bool $lazy search within lazy loaded preferences
  563. *
  564. * @return float stored preference 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 IUserPreferences 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 preference key
  585. * @param bool $default default value
  586. * @param bool $lazy search within lazy loaded preferences
  587. *
  588. * @return bool stored preference 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 IUserPreferences 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 preference key
  610. * @param array $default default value
  611. * @param bool $lazy search within lazy loaded preferences
  612. *
  613. * @return array stored preference 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 IUserPreferences 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 preference key
  638. * @param string $default default value
  639. * @param bool $lazy search within lazy loaded preferences
  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->compareRegisteredConfigValues($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->loadPreferences($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 preferences 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 preference key
  694. *
  695. * @return ValueType type of the value
  696. * @throws UnknownKeyException if preference key is not known
  697. * @throws IncorrectTypeException if preferences 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->loadPreferences($userId, $lazy);
  703. if (!isset($this->valueDetails[$userId][$app][$key]['type'])) {
  704. throw new UnknownKeyException('unknown preference 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 preference key
  714. * @param bool $lazy lazy loading
  715. *
  716. * @return int flags applied to value
  717. * @throws UnknownKeyException if preference key is not known
  718. * @throws IncorrectTypeException if preferences 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->loadPreferences($userId, $lazy);
  724. if (!isset($this->valueDetails[$userId][$app][$key])) {
  725. throw new UnknownKeyException('unknown preference key');
  726. }
  727. return $this->valueDetails[$userId][$app][$key]['flags'];
  728. }
  729. /**
  730. * Store a preference 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 preference key
  737. * @param string $value preference value
  738. * @param bool $lazy set preference as lazy loaded
  739. * @param bool $sensitive if TRUE value will be hidden when listing preference 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 IUserPreferences 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 preference key
  776. * @param string $value preference value
  777. * @param bool $lazy set preference as lazy loaded
  778. * @param bool $sensitive if TRUE value will be hidden when listing preference 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 IUserPreferences 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 preference key
  809. * @param int $value preference value
  810. * @param bool $lazy set preference as lazy loaded
  811. * @param bool $sensitive if TRUE value will be hidden when listing preference 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 IUserPreferences 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 preference key
  845. * @param float $value preference value
  846. * @param bool $lazy set preference as lazy loaded
  847. * @param bool $sensitive if TRUE value will be hidden when listing preference 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 IUserPreferences 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 preference key
  878. * @param bool $value preference value
  879. * @param bool $lazy set preference 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 IUserPreferences 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 preference key
  910. * @param array $value preference value
  911. * @param bool $lazy set preference as lazy loaded
  912. * @param bool $sensitive if TRUE value will be hidden when listing preference 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 IUserPreferences 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 preference key and its value in database
  945. *
  946. * If preference key is already known with the exact same preference value and same sensitive/lazy status, the
  947. * database is not updated. If preference 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 preference key
  953. * @param string $value preference value
  954. * @param bool $lazy preferences 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 IUserPreferences 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->compareRegisteredConfigValues($app, $key, $lazy, $type, $flags)) {
  972. return false; // returns false as database is not updated
  973. }
  974. $this->loadPreferences($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->loadPreferencesAll($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 preference 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 preference 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->loadPreferencesAll($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 preference 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->loadPreferencesAll($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 preference 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->loadPreferencesAll($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 preference 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 preference 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->loadPreferencesAll($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 preference 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 preference key
  1317. *
  1318. * @return array
  1319. * @throws UnknownKeyException if preference 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->loadPreferencesAll($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 preference 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 preference key
  1361. *
  1362. * @since 31.0.0
  1363. */
  1364. public function deletePreference(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 preference 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 deleteAllPreferences(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->loadPreferencesAll($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 preference 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 loadPreferencesAll(string $userId): void {
  1501. $this->loadPreferences($userId, null);
  1502. }
  1503. /**
  1504. * Load normal preferences or preferences set as lazy loaded
  1505. *
  1506. * @param bool|null $lazy set to TRUE to load preferences set as lazy loaded, set to NULL to load all preferences
  1507. */
  1508. private function loadPreferences(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 UserPreferences 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 loadPreferences 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 preferences are loaded
  1541. * - true : will returns true if lazy preferences are loaded
  1542. * - null : will returns true if both preferences 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 preferences as loaded
  1558. * - true : set lazy preferences as loaded
  1559. * - null : set both preferences 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 preference 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. * verify and compare current use of config values with defined lexicon
  1656. *
  1657. * @throws UnknownKeyException
  1658. * @throws TypeConflictException
  1659. */
  1660. private function compareRegisteredConfigValues(
  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 preference 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()->value; // we overwrite if value was requested as mixed
  1676. } elseif ($configValue->getValueType()->value !== $type) {
  1677. throw new TypeConflictException('The user preference 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 preference 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. }
  1711. throw new UnknownKeyException($line);
  1712. }
  1713. /**
  1714. * extract details from registered $appId's config lexicon
  1715. *
  1716. * @param string $appId
  1717. *
  1718. * @return array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}
  1719. */
  1720. private function getConfigDetailsFromLexicon(string $appId): array {
  1721. if (!array_key_exists($appId, $this->configLexiconDetails)) {
  1722. $entries = [];
  1723. $bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
  1724. $configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
  1725. foreach ($configLexicon?->getUserPreferences() ?? [] as $configEntry) {
  1726. $entries[$configEntry->getKey()] = $configEntry;
  1727. }
  1728. $this->configLexiconDetails[$appId] = [
  1729. 'entries' => $entries,
  1730. 'strictness' => $configLexicon?->getStrictness() ?? ConfigLexiconStrictness::IGNORE
  1731. ];
  1732. }
  1733. return $this->configLexiconDetails[$appId];
  1734. }
  1735. }