12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910 |
- <?php
- declare(strict_types=1);
- /**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
- namespace OC\Config;
- use Generator;
- use InvalidArgumentException;
- use JsonException;
- use NCU\Config\Exceptions\IncorrectTypeException;
- use NCU\Config\Exceptions\TypeConflictException;
- use NCU\Config\Exceptions\UnknownKeyException;
- use NCU\Config\IUserPreferences;
- use NCU\Config\Lexicon\ConfigLexiconEntry;
- use NCU\Config\Lexicon\ConfigLexiconStrictness;
- use NCU\Config\ValueType;
- use OC\AppFramework\Bootstrap\Coordinator;
- use OCP\DB\Exception as DBException;
- use OCP\DB\IResult;
- use OCP\DB\QueryBuilder\IQueryBuilder;
- use OCP\IConfig;
- use OCP\IDBConnection;
- use OCP\Security\ICrypto;
- use Psr\Log\LoggerInterface;
- /**
- * This class provides an easy way for apps to store user preferences in the
- * database.
- * Supports **lazy loading**
- *
- * ### What is lazy loading ?
- * In order to avoid loading useless user preferences into memory for each request,
- * only non-lazy values are now loaded.
- *
- * Once a value that is lazy is requested, all lazy values will be loaded.
- *
- * Similarly, some methods from this class are marked with a warning about ignoring
- * lazy loading. Use them wisely and only on parts of the code that are called
- * during specific requests or actions to avoid loading the lazy values all the time.
- *
- * @since 31.0.0
- */
- class UserPreferences implements IUserPreferences {
- private const USER_MAX_LENGTH = 64;
- private const APP_MAX_LENGTH = 32;
- private const KEY_MAX_LENGTH = 64;
- private const INDEX_MAX_LENGTH = 64;
- private const ENCRYPTION_PREFIX = '$UserPreferencesEncryption$';
- private const ENCRYPTION_PREFIX_LENGTH = 27; // strlen(self::ENCRYPTION_PREFIX)
- /** @var array<string, array<string, array<string, mixed>>> [ass'user_id' => ['app_id' => ['key' => 'value']]] */
- private array $fastCache = []; // cache for normal preference keys
- /** @var array<string, array<string, array<string, mixed>>> ['user_id' => ['app_id' => ['key' => 'value']]] */
- private array $lazyCache = []; // cache for lazy preference keys
- /** @var array<string, array<string, array<string, array<string, mixed>>>> ['user_id' => ['app_id' => ['key' => ['type' => ValueType, 'flags' => bitflag]]]] */
- private array $valueDetails = []; // type for all preference values
- /** @var array<string, array<string, array<string, ValueType>>> ['user_id' => ['app_id' => ['key' => bitflag]]] */
- private array $valueTypes = []; // type for all preference values
- /** @var array<string, array<string, array<string, int>>> ['user_id' => ['app_id' => ['key' => bitflag]]] */
- private array $valueFlags = []; // type for all preference values
- /** @var array<string, boolean> ['user_id' => bool] */
- private array $fastLoaded = [];
- /** @var array<string, boolean> ['user_id' => bool] */
- private array $lazyLoaded = [];
- /** @var array<array-key, array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
- private array $configLexiconDetails = [];
- public function __construct(
- protected IDBConnection $connection,
- protected LoggerInterface $logger,
- protected ICrypto $crypto,
- ) {
- }
- /**
- * @inheritDoc
- *
- * @param string $appId optional id of app
- *
- * @return list<string> list of userIds
- * @since 31.0.0
- */
- public function getUserIds(string $appId = ''): array {
- $this->assertParams(app: $appId, allowEmptyUser: true, allowEmptyApp: true);
- $qb = $this->connection->getQueryBuilder();
- $qb->from('preferences');
- $qb->select('userid');
- $qb->groupBy('userid');
- if ($appId !== '') {
- $qb->where($qb->expr()->eq('appid', $qb->createNamedParameter($appId)));
- }
- $result = $qb->executeQuery();
- $rows = $result->fetchAll();
- $userIds = [];
- foreach ($rows as $row) {
- $userIds[] = $row['userid'];
- }
- return $userIds;
- }
- /**
- * @inheritDoc
- *
- * @return list<string> list of app ids
- * @since 31.0.0
- */
- public function getApps(string $userId): array {
- $this->assertParams($userId, allowEmptyApp: true);
- $this->loadPreferencesAll($userId);
- $apps = array_merge(array_keys($this->fastCache[$userId] ?? []), array_keys($this->lazyCache[$userId] ?? []));
- sort($apps);
- return array_values(array_unique($apps));
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- *
- * @return list<string> list of stored preference keys
- * @since 31.0.0
- */
- public function getKeys(string $userId, string $app): array {
- $this->assertParams($userId, $app);
- $this->loadPreferencesAll($userId);
- // array_merge() will remove numeric keys (here preference keys), so addition arrays instead
- $keys = array_map('strval', array_keys(($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? [])));
- sort($keys);
- return array_values(array_unique($keys));
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param bool|null $lazy TRUE to search within lazy loaded preferences, NULL to search within all preferences
- *
- * @return bool TRUE if key exists
- * @since 31.0.0
- */
- public function hasKey(string $userId, string $app, string $key, ?bool $lazy = false): bool {
- $this->assertParams($userId, $app, $key);
- $this->loadPreferences($userId, $lazy);
- if ($lazy === null) {
- $appCache = $this->getValues($userId, $app);
- return isset($appCache[$key]);
- }
- if ($lazy) {
- return isset($this->lazyCache[$userId][$app][$key]);
- }
- return isset($this->fastCache[$userId][$app][$key]);
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param bool|null $lazy TRUE to search within lazy loaded preferences, NULL to search within all preferences
- *
- * @return bool
- * @throws UnknownKeyException if preference key is not known
- * @since 31.0.0
- */
- public function isSensitive(string $userId, string $app, string $key, ?bool $lazy = false): bool {
- $this->assertParams($userId, $app, $key);
- $this->loadPreferences($userId, $lazy);
- if (!isset($this->valueDetails[$userId][$app][$key])) {
- throw new UnknownKeyException('unknown preference key');
- }
- return $this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags']);
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param bool|null $lazy TRUE to search within lazy loaded preferences, NULL to search within all preferences
- *
- * @return bool
- * @throws UnknownKeyException if preference key is not known
- * @since 31.0.0
- */
- public function isIndexed(string $userId, string $app, string $key, ?bool $lazy = false): bool {
- $this->assertParams($userId, $app, $key);
- $this->loadPreferences($userId, $lazy);
- if (!isset($this->valueDetails[$userId][$app][$key])) {
- throw new UnknownKeyException('unknown preference key');
- }
- return $this->isFlagged(self::FLAG_INDEXED, $this->valueDetails[$userId][$app][$key]['flags']);
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app if of the app
- * @param string $key preference key
- *
- * @return bool TRUE if preference is lazy loaded
- * @throws UnknownKeyException if preference key is not known
- * @see IUserPreferences for details about lazy loading
- * @since 31.0.0
- */
- public function isLazy(string $userId, string $app, string $key): bool {
- // there is a huge probability the non-lazy preferences are already loaded
- // meaning that we can start by only checking if a current non-lazy key exists
- if ($this->hasKey($userId, $app, $key, false)) {
- return false; // meaning key is not lazy.
- }
- // as key is not found as non-lazy, we load and search in the lazy preferences
- if ($this->hasKey($userId, $app, $key, true)) {
- return true;
- }
- throw new UnknownKeyException('unknown preference key');
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $prefix preference keys prefix to search
- * @param bool $filtered TRUE to hide sensitive preference values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
- *
- * @return array<string, string|int|float|bool|array> [key => value]
- * @since 31.0.0
- */
- public function getValues(
- string $userId,
- string $app,
- string $prefix = '',
- bool $filtered = false,
- ): array {
- $this->assertParams($userId, $app, $prefix);
- // if we want to filter values, we need to get sensitivity
- $this->loadPreferencesAll($userId);
- // array_merge() will remove numeric keys (here preference keys), so addition arrays instead
- $values = array_filter(
- $this->formatAppValues($userId, $app, ($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? []), $filtered),
- function (string $key) use ($prefix): bool {
- return str_starts_with($key, $prefix); // filter values based on $prefix
- }, ARRAY_FILTER_USE_KEY
- );
- return $values;
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param bool $filtered TRUE to hide sensitive preference values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
- *
- * @return array<string, array<string, string|int|float|bool|array>> [appId => [key => value]]
- * @since 31.0.0
- */
- public function getAllValues(string $userId, bool $filtered = false): array {
- $this->assertParams($userId, allowEmptyApp: true);
- $this->loadPreferencesAll($userId);
- $result = [];
- foreach ($this->getApps($userId) as $app) {
- // array_merge() will remove numeric keys (here preference keys), so addition arrays instead
- $cached = ($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? []);
- $result[$app] = $this->formatAppValues($userId, $app, $cached, $filtered);
- }
- return $result;
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $key preference key
- * @param bool $lazy search within lazy loaded preferences
- * @param ValueType|null $typedAs enforce type for the returned values
- *
- * @return array<string, string|int|float|bool|array> [appId => value]
- * @since 31.0.0
- */
- public function getValuesByApps(string $userId, string $key, bool $lazy = false, ?ValueType $typedAs = null): array {
- $this->assertParams($userId, '', $key, allowEmptyApp: true);
- $this->loadPreferences($userId, $lazy);
- /** @var array<array-key, array<array-key, mixed>> $cache */
- if ($lazy) {
- $cache = $this->lazyCache[$userId];
- } else {
- $cache = $this->fastCache[$userId];
- }
- $values = [];
- foreach (array_keys($cache) as $app) {
- if (isset($cache[$app][$key])) {
- $value = $cache[$app][$key];
- try {
- $this->decryptSensitiveValue($userId, $app, $key, $value);
- $value = $this->convertTypedValue($value, $typedAs ?? $this->getValueType($userId, $app, $key, $lazy));
- } catch (IncorrectTypeException|UnknownKeyException) {
- }
- $values[$app] = $value;
- }
- }
- return $values;
- }
- /**
- * @inheritDoc
- *
- * @param string $app id of the app
- * @param string $key preference key
- * @param ValueType|null $typedAs enforce type for the returned values
- * @param array|null $userIds limit to a list of user ids
- *
- * @return array<string, string|int|float|bool|array> [userId => value]
- * @since 31.0.0
- */
- public function getValuesByUsers(
- string $app,
- string $key,
- ?ValueType $typedAs = null,
- ?array $userIds = null,
- ): array {
- $this->assertParams('', $app, $key, allowEmptyUser: true);
- $qb = $this->connection->getQueryBuilder();
- $qb->select('userid', 'configvalue', 'type')
- ->from('preferences')
- ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
- ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
- $values = [];
- // this nested function will execute current Query and store result within $values.
- $executeAndStoreValue = function (IQueryBuilder $qb) use (&$values, $typedAs): IResult {
- $result = $qb->executeQuery();
- while ($row = $result->fetch()) {
- $value = $row['configvalue'];
- try {
- $value = $this->convertTypedValue($value, $typedAs ?? ValueType::from((int)$row['type']));
- } catch (IncorrectTypeException) {
- }
- $values[$row['userid']] = $value;
- }
- return $result;
- };
- // if no userIds to filter, we execute query as it is and returns all values ...
- if ($userIds === null) {
- $result = $executeAndStoreValue($qb);
- $result->closeCursor();
- return $values;
- }
- // if userIds to filter, we chunk the list and execute the same query multiple times until we get all values
- $result = null;
- $qb->andWhere($qb->expr()->in('userid', $qb->createParameter('userIds')));
- foreach (array_chunk($userIds, 50, true) as $chunk) {
- $qb->setParameter('userIds', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
- $result = $executeAndStoreValue($qb);
- }
- $result?->closeCursor();
- return $values;
- }
- /**
- * @inheritDoc
- *
- * @param string $app id of the app
- * @param string $key preference key
- * @param string $value preference value
- * @param bool $caseInsensitive non-case-sensitive search, only works if $value is a string
- *
- * @return Generator<string>
- * @since 31.0.0
- */
- public function searchUsersByValueString(string $app, string $key, string $value, bool $caseInsensitive = false): Generator {
- return $this->searchUsersByTypedValue($app, $key, $value, $caseInsensitive);
- }
- /**
- * @inheritDoc
- *
- * @param string $app id of the app
- * @param string $key preference key
- * @param int $value preference value
- *
- * @return Generator<string>
- * @since 31.0.0
- */
- public function searchUsersByValueInt(string $app, string $key, int $value): Generator {
- return $this->searchUsersByValueString($app, $key, (string)$value);
- }
- /**
- * @inheritDoc
- *
- * @param string $app id of the app
- * @param string $key preference key
- * @param array $values list of preference values
- *
- * @return Generator<string>
- * @since 31.0.0
- */
- public function searchUsersByValues(string $app, string $key, array $values): Generator {
- return $this->searchUsersByTypedValue($app, $key, $values);
- }
- /**
- * @inheritDoc
- *
- * @param string $app id of the app
- * @param string $key preference key
- * @param bool $value preference value
- *
- * @return Generator<string>
- * @since 31.0.0
- */
- public function searchUsersByValueBool(string $app, string $key, bool $value): Generator {
- $values = ['0', 'off', 'false', 'no'];
- if ($value) {
- $values = ['1', 'on', 'true', 'yes'];
- }
- return $this->searchUsersByValues($app, $key, $values);
- }
- /**
- * returns a list of users with preference key set to a specific value, or within the list of
- * possible values
- *
- * @param string $app
- * @param string $key
- * @param string|array $value
- * @param bool $caseInsensitive
- *
- * @return Generator<string>
- */
- private function searchUsersByTypedValue(string $app, string $key, string|array $value, bool $caseInsensitive = false): Generator {
- $this->assertParams('', $app, $key, allowEmptyUser: true);
- $qb = $this->connection->getQueryBuilder();
- $qb->from('preferences');
- $qb->select('userid');
- $qb->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
- $qb->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
- // search within 'indexed' OR 'configvalue' only if 'flags' is set as not indexed
- // TODO: when implementing config lexicon remove the searches on 'configvalue' if value is set as indexed
- $configValueColumn = ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) ? $qb->expr()->castColumn('configvalue', IQueryBuilder::PARAM_STR) : 'configvalue';
- if (is_array($value)) {
- $where = $qb->expr()->orX(
- $qb->expr()->in('indexed', $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR_ARRAY)),
- $qb->expr()->andX(
- $qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)),
- $qb->expr()->in($configValueColumn, $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR_ARRAY))
- )
- );
- } else {
- if ($caseInsensitive) {
- $where = $qb->expr()->orX(
- $qb->expr()->eq($qb->func()->lower('indexed'), $qb->createNamedParameter(strtolower($value))),
- $qb->expr()->andX(
- $qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)),
- $qb->expr()->eq($qb->func()->lower($configValueColumn), $qb->createNamedParameter(strtolower($value)))
- )
- );
- } else {
- $where = $qb->expr()->orX(
- $qb->expr()->eq('indexed', $qb->createNamedParameter($value)),
- $qb->expr()->andX(
- $qb->expr()->neq($qb->expr()->bitwiseAnd('flags', self::FLAG_INDEXED), $qb->createNamedParameter(self::FLAG_INDEXED, IQueryBuilder::PARAM_INT)),
- $qb->expr()->eq($configValueColumn, $qb->createNamedParameter($value))
- )
- );
- }
- }
- $qb->andWhere($where);
- $result = $qb->executeQuery();
- while ($row = $result->fetch()) {
- yield $row['userid'];
- }
- }
- /**
- * Get the preference value as string.
- * If the value does not exist the given default will be returned.
- *
- * Set lazy to `null` to ignore it and get the value from either source.
- *
- * **WARNING:** Method is internal and **SHOULD** not be used, as it is better to get the value with a type.
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param string $default preference value
- * @param null|bool $lazy get preference as lazy loaded or not. can be NULL
- *
- * @return string the value or $default
- * @throws TypeConflictException
- * @internal
- * @since 31.0.0
- * @see IUserPreferences for explanation about lazy loading
- * @see getValueString()
- * @see getValueInt()
- * @see getValueFloat()
- * @see getValueBool()
- * @see getValueArray()
- */
- public function getValueMixed(
- string $userId,
- string $app,
- string $key,
- string $default = '',
- ?bool $lazy = false,
- ): string {
- try {
- $lazy ??= $this->isLazy($userId, $app, $key);
- } catch (UnknownKeyException) {
- return $default;
- }
- return $this->getTypedValue(
- $userId,
- $app,
- $key,
- $default,
- $lazy,
- ValueType::MIXED
- );
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param string $default default value
- * @param bool $lazy search within lazy loaded preferences
- *
- * @return string stored preference value or $default if not set in database
- * @throws InvalidArgumentException if one of the argument format is invalid
- * @throws TypeConflictException in case of conflict with the value type set in database
- * @since 31.0.0
- * @see IUserPreferences for explanation about lazy loading
- */
- public function getValueString(
- string $userId,
- string $app,
- string $key,
- string $default = '',
- bool $lazy = false,
- ): string {
- return $this->getTypedValue($userId, $app, $key, $default, $lazy, ValueType::STRING);
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param int $default default value
- * @param bool $lazy search within lazy loaded preferences
- *
- * @return int stored preference value or $default if not set in database
- * @throws InvalidArgumentException if one of the argument format is invalid
- * @throws TypeConflictException in case of conflict with the value type set in database
- * @since 31.0.0
- * @see IUserPreferences for explanation about lazy loading
- */
- public function getValueInt(
- string $userId,
- string $app,
- string $key,
- int $default = 0,
- bool $lazy = false,
- ): int {
- return (int)$this->getTypedValue($userId, $app, $key, (string)$default, $lazy, ValueType::INT);
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param float $default default value
- * @param bool $lazy search within lazy loaded preferences
- *
- * @return float stored preference value or $default if not set in database
- * @throws InvalidArgumentException if one of the argument format is invalid
- * @throws TypeConflictException in case of conflict with the value type set in database
- * @since 31.0.0
- * @see IUserPreferences for explanation about lazy loading
- */
- public function getValueFloat(
- string $userId,
- string $app,
- string $key,
- float $default = 0,
- bool $lazy = false,
- ): float {
- return (float)$this->getTypedValue($userId, $app, $key, (string)$default, $lazy, ValueType::FLOAT);
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param bool $default default value
- * @param bool $lazy search within lazy loaded preferences
- *
- * @return bool stored preference value or $default if not set in database
- * @throws InvalidArgumentException if one of the argument format is invalid
- * @throws TypeConflictException in case of conflict with the value type set in database
- * @since 31.0.0
- * @see IUserPreferences for explanation about lazy loading
- */
- public function getValueBool(
- string $userId,
- string $app,
- string $key,
- bool $default = false,
- bool $lazy = false,
- ): bool {
- $b = strtolower($this->getTypedValue($userId, $app, $key, $default ? 'true' : 'false', $lazy, ValueType::BOOL));
- return in_array($b, ['1', 'true', 'yes', 'on']);
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param array $default default value
- * @param bool $lazy search within lazy loaded preferences
- *
- * @return array stored preference value or $default if not set in database
- * @throws InvalidArgumentException if one of the argument format is invalid
- * @throws TypeConflictException in case of conflict with the value type set in database
- * @since 31.0.0
- * @see IUserPreferences for explanation about lazy loading
- */
- public function getValueArray(
- string $userId,
- string $app,
- string $key,
- array $default = [],
- bool $lazy = false,
- ): array {
- try {
- $defaultJson = json_encode($default, JSON_THROW_ON_ERROR);
- $value = json_decode($this->getTypedValue($userId, $app, $key, $defaultJson, $lazy, ValueType::ARRAY), true, flags: JSON_THROW_ON_ERROR);
- return is_array($value) ? $value : [];
- } catch (JsonException) {
- return [];
- }
- }
- /**
- * @param string $userId
- * @param string $app id of the app
- * @param string $key preference key
- * @param string $default default value
- * @param bool $lazy search within lazy loaded preferences
- * @param ValueType $type value type
- *
- * @return string
- * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
- */
- private function getTypedValue(
- string $userId,
- string $app,
- string $key,
- string $default,
- bool $lazy,
- ValueType $type,
- ): string {
- $this->assertParams($userId, $app, $key);
- if (!$this->compareRegisteredConfigValues($app, $key, $lazy, $type, default: $default)) {
- return $default; // returns default if strictness of lexicon is set to WARNING (block and report)
- }
- $this->loadPreferences($userId, $lazy);
- /**
- * We ignore check if mixed type is requested.
- * If type of stored value is set as mixed, we don't filter.
- * If type of stored value is defined, we compare with the one requested.
- */
- $knownType = $this->valueDetails[$userId][$app][$key]['type'] ?? null;
- if ($type !== ValueType::MIXED
- && $knownType !== null
- && $knownType !== ValueType::MIXED
- && $type !== $knownType) {
- $this->logger->warning('conflict with value type from database', ['app' => $app, 'key' => $key, 'type' => $type, 'knownType' => $knownType]);
- throw new TypeConflictException('conflict with value type from database');
- }
- /**
- * - the pair $app/$key cannot exist in both array,
- * - we should still return an existing non-lazy value even if current method
- * is called with $lazy is true
- *
- * This way, lazyCache will be empty until the load for lazy preferences value is requested.
- */
- if (isset($this->lazyCache[$userId][$app][$key])) {
- $value = $this->lazyCache[$userId][$app][$key];
- } elseif (isset($this->fastCache[$userId][$app][$key])) {
- $value = $this->fastCache[$userId][$app][$key];
- } else {
- return $default;
- }
- $this->decryptSensitiveValue($userId, $app, $key, $value);
- return $value;
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- *
- * @return ValueType type of the value
- * @throws UnknownKeyException if preference key is not known
- * @throws IncorrectTypeException if preferences value type is not known
- * @since 31.0.0
- */
- public function getValueType(string $userId, string $app, string $key, ?bool $lazy = null): ValueType {
- $this->assertParams($userId, $app, $key);
- $this->loadPreferences($userId, $lazy);
- if (!isset($this->valueDetails[$userId][$app][$key]['type'])) {
- throw new UnknownKeyException('unknown preference key');
- }
- return $this->valueDetails[$userId][$app][$key]['type'];
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param bool $lazy lazy loading
- *
- * @return int flags applied to value
- * @throws UnknownKeyException if preference key is not known
- * @throws IncorrectTypeException if preferences value type is not known
- * @since 31.0.0
- */
- public function getValueFlags(string $userId, string $app, string $key, bool $lazy = false): int {
- $this->assertParams($userId, $app, $key);
- $this->loadPreferences($userId, $lazy);
- if (!isset($this->valueDetails[$userId][$app][$key])) {
- throw new UnknownKeyException('unknown preference key');
- }
- return $this->valueDetails[$userId][$app][$key]['flags'];
- }
- /**
- * Store a preference key and its value in database as VALUE_MIXED
- *
- * **WARNING:** Method is internal and **MUST** not be used as it is best to set a real value type
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param string $value preference value
- * @param bool $lazy set preference as lazy loaded
- * @param bool $sensitive if TRUE value will be hidden when listing preference values.
- *
- * @return bool TRUE if value was different, therefor updated in database
- * @throws TypeConflictException if type from database is not VALUE_MIXED
- * @internal
- * @since 31.0.0
- * @see IUserPreferences for explanation about lazy loading
- * @see setValueString()
- * @see setValueInt()
- * @see setValueFloat()
- * @see setValueBool()
- * @see setValueArray()
- */
- public function setValueMixed(
- string $userId,
- string $app,
- string $key,
- string $value,
- bool $lazy = false,
- int $flags = 0,
- ): bool {
- return $this->setTypedValue(
- $userId,
- $app,
- $key,
- $value,
- $lazy,
- $flags,
- ValueType::MIXED
- );
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param string $value preference value
- * @param bool $lazy set preference as lazy loaded
- * @param bool $sensitive if TRUE value will be hidden when listing preference values.
- *
- * @return bool TRUE if value was different, therefor updated in database
- * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
- * @since 31.0.0
- * @see IUserPreferences for explanation about lazy loading
- */
- public function setValueString(
- string $userId,
- string $app,
- string $key,
- string $value,
- bool $lazy = false,
- int $flags = 0,
- ): bool {
- return $this->setTypedValue(
- $userId,
- $app,
- $key,
- $value,
- $lazy,
- $flags,
- ValueType::STRING
- );
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param int $value preference value
- * @param bool $lazy set preference as lazy loaded
- * @param bool $sensitive if TRUE value will be hidden when listing preference values.
- *
- * @return bool TRUE if value was different, therefor updated in database
- * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
- * @since 31.0.0
- * @see IUserPreferences for explanation about lazy loading
- */
- public function setValueInt(
- string $userId,
- string $app,
- string $key,
- int $value,
- bool $lazy = false,
- int $flags = 0,
- ): bool {
- if ($value > 2000000000) {
- $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.');
- }
- return $this->setTypedValue(
- $userId,
- $app,
- $key,
- (string)$value,
- $lazy,
- $flags,
- ValueType::INT
- );
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param float $value preference value
- * @param bool $lazy set preference as lazy loaded
- * @param bool $sensitive if TRUE value will be hidden when listing preference values.
- *
- * @return bool TRUE if value was different, therefor updated in database
- * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
- * @since 31.0.0
- * @see IUserPreferences for explanation about lazy loading
- */
- public function setValueFloat(
- string $userId,
- string $app,
- string $key,
- float $value,
- bool $lazy = false,
- int $flags = 0,
- ): bool {
- return $this->setTypedValue(
- $userId,
- $app,
- $key,
- (string)$value,
- $lazy,
- $flags,
- ValueType::FLOAT
- );
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param bool $value preference value
- * @param bool $lazy set preference as lazy loaded
- *
- * @return bool TRUE if value was different, therefor updated in database
- * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
- * @since 31.0.0
- * @see IUserPreferences for explanation about lazy loading
- */
- public function setValueBool(
- string $userId,
- string $app,
- string $key,
- bool $value,
- bool $lazy = false,
- int $flags = 0,
- ): bool {
- return $this->setTypedValue(
- $userId,
- $app,
- $key,
- ($value) ? '1' : '0',
- $lazy,
- $flags,
- ValueType::BOOL
- );
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param array $value preference value
- * @param bool $lazy set preference as lazy loaded
- * @param bool $sensitive if TRUE value will be hidden when listing preference values.
- *
- * @return bool TRUE if value was different, therefor updated in database
- * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
- * @throws JsonException
- * @since 31.0.0
- * @see IUserPreferences for explanation about lazy loading
- */
- public function setValueArray(
- string $userId,
- string $app,
- string $key,
- array $value,
- bool $lazy = false,
- int $flags = 0,
- ): bool {
- try {
- return $this->setTypedValue(
- $userId,
- $app,
- $key,
- json_encode($value, JSON_THROW_ON_ERROR),
- $lazy,
- $flags,
- ValueType::ARRAY
- );
- } catch (JsonException $e) {
- $this->logger->warning('could not setValueArray', ['app' => $app, 'key' => $key, 'exception' => $e]);
- throw $e;
- }
- }
- /**
- * Store a preference key and its value in database
- *
- * If preference key is already known with the exact same preference value and same sensitive/lazy status, the
- * database is not updated. If preference value was previously stored as sensitive, status will not be
- * altered.
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param string $value preference value
- * @param bool $lazy preferences set as lazy loaded
- * @param ValueType $type value type
- *
- * @return bool TRUE if value was updated in database
- * @throws TypeConflictException if type from database is not VALUE_MIXED and different from the requested one
- * @see IUserPreferences for explanation about lazy loading
- */
- private function setTypedValue(
- string $userId,
- string $app,
- string $key,
- string $value,
- bool $lazy,
- int $flags,
- ValueType $type,
- ): bool {
- $this->assertParams($userId, $app, $key);
- if (!$this->compareRegisteredConfigValues($app, $key, $lazy, $type, $flags)) {
- return false; // returns false as database is not updated
- }
- $this->loadPreferences($userId, $lazy);
- $inserted = $refreshCache = false;
- $origValue = $value;
- $sensitive = $this->isFlagged(self::FLAG_SENSITIVE, $flags);
- if ($sensitive || ($this->hasKey($userId, $app, $key, $lazy) && $this->isSensitive($userId, $app, $key, $lazy))) {
- $value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
- $flags |= self::FLAG_SENSITIVE;
- }
- // if requested, we fill the 'indexed' field with current value
- $indexed = '';
- if ($type !== ValueType::ARRAY && $this->isFlagged(self::FLAG_INDEXED, $flags)) {
- if ($this->isFlagged(self::FLAG_SENSITIVE, $flags)) {
- $this->logger->warning('sensitive value are not to be indexed');
- } elseif (strlen($value) > self::USER_MAX_LENGTH) {
- $this->logger->warning('value is too lengthy to be indexed');
- } else {
- $indexed = $value;
- }
- }
- if ($this->hasKey($userId, $app, $key, $lazy)) {
- /**
- * no update if key is already known with set lazy status and value is
- * not different, unless sensitivity is switched from false to true.
- */
- if ($origValue === $this->getTypedValue($userId, $app, $key, $value, $lazy, $type)
- && (!$sensitive || $this->isSensitive($userId, $app, $key, $lazy))) {
- return false;
- }
- } else {
- /**
- * if key is not known yet, we try to insert.
- * It might fail if the key exists with a different lazy flag.
- */
- try {
- $insert = $this->connection->getQueryBuilder();
- $insert->insert('preferences')
- ->setValue('userid', $insert->createNamedParameter($userId))
- ->setValue('appid', $insert->createNamedParameter($app))
- ->setValue('lazy', $insert->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
- ->setValue('type', $insert->createNamedParameter($type->value, IQueryBuilder::PARAM_INT))
- ->setValue('flags', $insert->createNamedParameter($flags, IQueryBuilder::PARAM_INT))
- ->setValue('indexed', $insert->createNamedParameter($indexed))
- ->setValue('configkey', $insert->createNamedParameter($key))
- ->setValue('configvalue', $insert->createNamedParameter($value));
- $insert->executeStatement();
- $inserted = true;
- } catch (DBException $e) {
- if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
- throw $e; // TODO: throw exception or just log and returns false !?
- }
- }
- }
- /**
- * We cannot insert a new row, meaning we need to update an already existing one
- */
- if (!$inserted) {
- $currType = $this->valueDetails[$userId][$app][$key]['type'] ?? null;
- if ($currType === null) { // this might happen when switching lazy loading status
- $this->loadPreferencesAll($userId);
- $currType = $this->valueDetails[$userId][$app][$key]['type'];
- }
- /**
- * We only log a warning and set it to VALUE_MIXED.
- */
- if ($currType === null) {
- $this->logger->warning('Value type is set to zero (0) in database. This is not supposed to happens', ['app' => $app, 'key' => $key]);
- $currType = ValueType::MIXED;
- }
- /**
- * we only accept a different type from the one stored in database
- * if the one stored in database is not-defined (VALUE_MIXED)
- */
- if ($currType !== ValueType::MIXED &&
- $currType !== $type) {
- try {
- $currTypeDef = $currType->getDefinition();
- $typeDef = $type->getDefinition();
- } catch (IncorrectTypeException) {
- $currTypeDef = $currType->value;
- $typeDef = $type->value;
- }
- throw new TypeConflictException('conflict between new type (' . $typeDef . ') and old type (' . $currTypeDef . ')');
- }
- if ($lazy !== $this->isLazy($userId, $app, $key)) {
- $refreshCache = true;
- }
- $update = $this->connection->getQueryBuilder();
- $update->update('preferences')
- ->set('configvalue', $update->createNamedParameter($value))
- ->set('lazy', $update->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
- ->set('type', $update->createNamedParameter($type->value, IQueryBuilder::PARAM_INT))
- ->set('flags', $update->createNamedParameter($flags, IQueryBuilder::PARAM_INT))
- ->set('indexed', $update->createNamedParameter($indexed))
- ->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
- ->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
- ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
- $update->executeStatement();
- }
- if ($refreshCache) {
- $this->clearCache($userId);
- return true;
- }
- // update local cache
- if ($lazy) {
- $this->lazyCache[$userId][$app][$key] = $value;
- } else {
- $this->fastCache[$userId][$app][$key] = $value;
- }
- $this->valueDetails[$userId][$app][$key] = [
- 'type' => $type,
- 'flags' => $flags
- ];
- return true;
- }
- /**
- * Change the type of preference value.
- *
- * **WARNING:** Method is internal and **MUST** not be used as it may break things.
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param ValueType $type value type
- *
- * @return bool TRUE if database update were necessary
- * @throws UnknownKeyException if $key is now known in database
- * @throws IncorrectTypeException if $type is not valid
- * @internal
- * @since 31.0.0
- */
- public function updateType(string $userId, string $app, string $key, ValueType $type = ValueType::MIXED): bool {
- $this->assertParams($userId, $app, $key);
- $this->loadPreferencesAll($userId);
- $this->isLazy($userId, $app, $key); // confirm key exists
- $update = $this->connection->getQueryBuilder();
- $update->update('preferences')
- ->set('type', $update->createNamedParameter($type->value, IQueryBuilder::PARAM_INT))
- ->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
- ->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
- ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
- $update->executeStatement();
- $this->valueDetails[$userId][$app][$key]['type'] = $type;
- return true;
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param bool $sensitive TRUE to set as sensitive, FALSE to unset
- *
- * @return bool TRUE if entry was found in database and an update was necessary
- * @since 31.0.0
- */
- public function updateSensitive(string $userId, string $app, string $key, bool $sensitive): bool {
- $this->assertParams($userId, $app, $key);
- $this->loadPreferencesAll($userId);
- try {
- if ($sensitive === $this->isSensitive($userId, $app, $key, null)) {
- return false;
- }
- } catch (UnknownKeyException) {
- return false;
- }
- $lazy = $this->isLazy($userId, $app, $key);
- if ($lazy) {
- $cache = $this->lazyCache;
- } else {
- $cache = $this->fastCache;
- }
- if (!isset($cache[$userId][$app][$key])) {
- throw new UnknownKeyException('unknown preference key');
- }
- $value = $cache[$userId][$app][$key];
- $flags = $this->getValueFlags($userId, $app, $key);
- if ($sensitive) {
- $flags |= self::FLAG_SENSITIVE;
- $value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
- } else {
- $flags &= ~self::FLAG_SENSITIVE;
- $this->decryptSensitiveValue($userId, $app, $key, $value);
- }
- $update = $this->connection->getQueryBuilder();
- $update->update('preferences')
- ->set('flags', $update->createNamedParameter($flags, IQueryBuilder::PARAM_INT))
- ->set('configvalue', $update->createNamedParameter($value))
- ->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
- ->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
- ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
- $update->executeStatement();
- $this->valueDetails[$userId][$app][$key]['flags'] = $flags;
- return true;
- }
- /**
- * @inheritDoc
- *
- * @param string $app
- * @param string $key
- * @param bool $sensitive
- *
- * @since 31.0.0
- */
- public function updateGlobalSensitive(string $app, string $key, bool $sensitive): void {
- $this->assertParams('', $app, $key, allowEmptyUser: true);
- foreach (array_keys($this->getValuesByUsers($app, $key)) as $userId) {
- try {
- $this->updateSensitive($userId, $app, $key, $sensitive);
- } catch (UnknownKeyException) {
- // should not happen and can be ignored
- }
- }
- $this->clearCacheAll(); // we clear all cache
- }
- /**
- * @inheritDoc
- *
- * @param string $userId
- * @param string $app
- * @param string $key
- * @param bool $indexed
- *
- * @return bool
- * @throws DBException
- * @throws IncorrectTypeException
- * @throws UnknownKeyException
- * @since 31.0.0
- */
- public function updateIndexed(string $userId, string $app, string $key, bool $indexed): bool {
- $this->assertParams($userId, $app, $key);
- $this->loadPreferencesAll($userId);
- try {
- if ($indexed === $this->isIndexed($userId, $app, $key, null)) {
- return false;
- }
- } catch (UnknownKeyException) {
- return false;
- }
- $lazy = $this->isLazy($userId, $app, $key);
- if ($lazy) {
- $cache = $this->lazyCache;
- } else {
- $cache = $this->fastCache;
- }
- if (!isset($cache[$userId][$app][$key])) {
- throw new UnknownKeyException('unknown preference key');
- }
- $value = $cache[$userId][$app][$key];
- $flags = $this->getValueFlags($userId, $app, $key);
- if ($indexed) {
- $indexed = $value;
- } else {
- $flags &= ~self::FLAG_INDEXED;
- $indexed = '';
- }
- $update = $this->connection->getQueryBuilder();
- $update->update('preferences')
- ->set('flags', $update->createNamedParameter($flags, IQueryBuilder::PARAM_INT))
- ->set('indexed', $update->createNamedParameter($indexed))
- ->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
- ->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
- ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
- $update->executeStatement();
- $this->valueDetails[$userId][$app][$key]['flags'] = $flags;
- return true;
- }
- /**
- * @inheritDoc
- *
- * @param string $app
- * @param string $key
- * @param bool $indexed
- *
- * @since 31.0.0
- */
- public function updateGlobalIndexed(string $app, string $key, bool $indexed): void {
- $this->assertParams('', $app, $key, allowEmptyUser: true);
- foreach (array_keys($this->getValuesByUsers($app, $key)) as $userId) {
- try {
- $this->updateIndexed($userId, $app, $key, $indexed);
- } catch (UnknownKeyException) {
- // should not happen and can be ignored
- }
- }
- $this->clearCacheAll(); // we clear all cache
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- * @param bool $lazy TRUE to set as lazy loaded, FALSE to unset
- *
- * @return bool TRUE if entry was found in database and an update was necessary
- * @since 31.0.0
- */
- public function updateLazy(string $userId, string $app, string $key, bool $lazy): bool {
- $this->assertParams($userId, $app, $key);
- $this->loadPreferencesAll($userId);
- try {
- if ($lazy === $this->isLazy($userId, $app, $key)) {
- return false;
- }
- } catch (UnknownKeyException) {
- return false;
- }
- $update = $this->connection->getQueryBuilder();
- $update->update('preferences')
- ->set('lazy', $update->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))
- ->where($update->expr()->eq('userid', $update->createNamedParameter($userId)))
- ->andWhere($update->expr()->eq('appid', $update->createNamedParameter($app)))
- ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
- $update->executeStatement();
- // At this point, it is a lot safer to clean cache
- $this->clearCache($userId);
- return true;
- }
- /**
- * @inheritDoc
- *
- * @param string $app id of the app
- * @param string $key preference key
- * @param bool $lazy TRUE to set as lazy loaded, FALSE to unset
- *
- * @since 31.0.0
- */
- public function updateGlobalLazy(string $app, string $key, bool $lazy): void {
- $this->assertParams('', $app, $key, allowEmptyUser: true);
- $update = $this->connection->getQueryBuilder();
- $update->update('preferences')
- ->set('lazy', $update->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))
- ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
- ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
- $update->executeStatement();
- $this->clearCacheAll();
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- *
- * @return array
- * @throws UnknownKeyException if preference key is not known in database
- * @since 31.0.0
- */
- public function getDetails(string $userId, string $app, string $key): array {
- $this->assertParams($userId, $app, $key);
- $this->loadPreferencesAll($userId);
- $lazy = $this->isLazy($userId, $app, $key);
- if ($lazy) {
- $cache = $this->lazyCache[$userId];
- } else {
- $cache = $this->fastCache[$userId];
- }
- $type = $this->getValueType($userId, $app, $key);
- try {
- $typeString = $type->getDefinition();
- } catch (IncorrectTypeException $e) {
- $this->logger->warning('type stored in database is not correct', ['exception' => $e, 'type' => $type]);
- $typeString = (string)$type->value;
- }
- if (!isset($cache[$app][$key])) {
- throw new UnknownKeyException('unknown preference key');
- }
- $value = $cache[$app][$key];
- $sensitive = $this->isSensitive($userId, $app, $key, null);
- $this->decryptSensitiveValue($userId, $app, $key, $value);
- return [
- 'userId' => $userId,
- 'app' => $app,
- 'key' => $key,
- 'value' => $value,
- 'type' => $type->value,
- 'lazy' => $lazy,
- 'typeString' => $typeString,
- 'sensitive' => $sensitive
- ];
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $key preference key
- *
- * @since 31.0.0
- */
- public function deletePreference(string $userId, string $app, string $key): void {
- $this->assertParams($userId, $app, $key);
- $qb = $this->connection->getQueryBuilder();
- $qb->delete('preferences')
- ->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId)))
- ->andWhere($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
- ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
- $qb->executeStatement();
- unset($this->lazyCache[$userId][$app][$key]);
- unset($this->fastCache[$userId][$app][$key]);
- }
- /**
- * @inheritDoc
- *
- * @param string $app id of the app
- * @param string $key preference key
- *
- * @since 31.0.0
- */
- public function deleteKey(string $app, string $key): void {
- $this->assertParams('', $app, $key, allowEmptyUser: true);
- $qb = $this->connection->getQueryBuilder();
- $qb->delete('preferences')
- ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
- ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
- $qb->executeStatement();
- $this->clearCacheAll();
- }
- /**
- * @inheritDoc
- *
- * @param string $app id of the app
- *
- * @since 31.0.0
- */
- public function deleteApp(string $app): void {
- $this->assertParams('', $app, allowEmptyUser: true);
- $qb = $this->connection->getQueryBuilder();
- $qb->delete('preferences')
- ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
- $qb->executeStatement();
- $this->clearCacheAll();
- }
- public function deleteAllPreferences(string $userId): void {
- $this->assertParams($userId, '', allowEmptyApp: true);
- $qb = $this->connection->getQueryBuilder();
- $qb->delete('preferences')
- ->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId)));
- $qb->executeStatement();
- $this->clearCache($userId);
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param bool $reload set to TRUE to refill cache instantly after clearing it.
- *
- * @since 31.0.0
- */
- public function clearCache(string $userId, bool $reload = false): void {
- $this->assertParams($userId, allowEmptyApp: true);
- $this->lazyLoaded[$userId] = $this->fastLoaded[$userId] = false;
- $this->lazyCache[$userId] = $this->fastCache[$userId] = $this->valueDetails[$userId] = [];
- if (!$reload) {
- return;
- }
- $this->loadPreferencesAll($userId);
- }
- /**
- * @inheritDoc
- *
- * @since 31.0.0
- */
- public function clearCacheAll(): void {
- $this->lazyLoaded = $this->fastLoaded = [];
- $this->lazyCache = $this->fastCache = $this->valueDetails = [];
- }
- /**
- * For debug purpose.
- * Returns the cached data.
- *
- * @return array
- * @since 31.0.0
- * @internal
- */
- public function statusCache(): array {
- return [
- 'fastLoaded' => $this->fastLoaded,
- 'fastCache' => $this->fastCache,
- 'lazyLoaded' => $this->lazyLoaded,
- 'lazyCache' => $this->lazyCache,
- 'valueDetails' => $this->valueDetails,
- ];
- }
- /**
- * @param int $needle bitflag to search
- * @param int $flags all flags
- *
- * @return bool TRUE if bitflag $needle is set in $flags
- */
- private function isFlagged(int $needle, int $flags): bool {
- return (($needle & $flags) !== 0);
- }
- /**
- * Confirm the string set for app and key fit the database description
- *
- * @param string $userId
- * @param string $app assert $app fit in database
- * @param string $prefKey assert preference key fit in database
- * @param bool $allowEmptyUser
- * @param bool $allowEmptyApp $app can be empty string
- * @param ValueType|null $valueType assert value type is only one type
- */
- private function assertParams(
- string $userId = '',
- string $app = '',
- string $prefKey = '',
- bool $allowEmptyUser = false,
- bool $allowEmptyApp = false,
- ): void {
- if (!$allowEmptyUser && $userId === '') {
- throw new InvalidArgumentException('userId cannot be an empty string');
- }
- if (!$allowEmptyApp && $app === '') {
- throw new InvalidArgumentException('app cannot be an empty string');
- }
- if (strlen($userId) > self::USER_MAX_LENGTH) {
- throw new InvalidArgumentException('Value (' . $userId . ') for userId is too long (' . self::USER_MAX_LENGTH . ')');
- }
- if (strlen($app) > self::APP_MAX_LENGTH) {
- throw new InvalidArgumentException('Value (' . $app . ') for app is too long (' . self::APP_MAX_LENGTH . ')');
- }
- if (strlen($prefKey) > self::KEY_MAX_LENGTH) {
- throw new InvalidArgumentException('Value (' . $prefKey . ') for key is too long (' . self::KEY_MAX_LENGTH . ')');
- }
- }
- private function loadPreferencesAll(string $userId): void {
- $this->loadPreferences($userId, null);
- }
- /**
- * Load normal preferences or preferences set as lazy loaded
- *
- * @param bool|null $lazy set to TRUE to load preferences set as lazy loaded, set to NULL to load all preferences
- */
- private function loadPreferences(string $userId, ?bool $lazy = false): void {
- if ($this->isLoaded($userId, $lazy)) {
- return;
- }
- if (($lazy ?? true) !== false) { // if lazy is null or true, we debug log
- $this->logger->debug('The loading of lazy UserPreferences values have been requested', ['exception' => new \RuntimeException('ignorable exception')]);
- }
- $qb = $this->connection->getQueryBuilder();
- $qb->from('preferences');
- $qb->select('appid', 'configkey', 'configvalue', 'type', 'flags');
- $qb->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId)));
- // we only need value from lazy when loadPreferences does not specify it
- if ($lazy !== null) {
- $qb->andWhere($qb->expr()->eq('lazy', $qb->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT)));
- } else {
- $qb->addSelect('lazy');
- }
- $result = $qb->executeQuery();
- $rows = $result->fetchAll();
- foreach ($rows as $row) {
- if (($row['lazy'] ?? ($lazy ?? 0) ? 1 : 0) === 1) {
- $this->lazyCache[$userId][$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
- } else {
- $this->fastCache[$userId][$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
- }
- $this->valueDetails[$userId][$row['appid']][$row['configkey']] = ['type' => ValueType::from((int)($row['type'] ?? 0)), 'flags' => (int)$row['flags']];
- }
- $result->closeCursor();
- $this->setAsLoaded($userId, $lazy);
- }
- /**
- * if $lazy is:
- * - false: will returns true if fast preferences are loaded
- * - true : will returns true if lazy preferences are loaded
- * - null : will returns true if both preferences are loaded
- *
- * @param string $userId
- * @param bool $lazy
- *
- * @return bool
- */
- private function isLoaded(string $userId, ?bool $lazy): bool {
- if ($lazy === null) {
- return ($this->lazyLoaded[$userId] ?? false) && ($this->fastLoaded[$userId] ?? false);
- }
- return $lazy ? $this->lazyLoaded[$userId] ?? false : $this->fastLoaded[$userId] ?? false;
- }
- /**
- * if $lazy is:
- * - false: set fast preferences as loaded
- * - true : set lazy preferences as loaded
- * - null : set both preferences as loaded
- *
- * @param string $userId
- * @param bool $lazy
- */
- private function setAsLoaded(string $userId, ?bool $lazy): void {
- if ($lazy === null) {
- $this->fastLoaded[$userId] = $this->lazyLoaded[$userId] = true;
- return;
- }
- // We also create empty entry to keep both fastLoaded/lazyLoaded synced
- if ($lazy) {
- $this->lazyLoaded[$userId] = true;
- $this->fastLoaded[$userId] = $this->fastLoaded[$userId] ?? false;
- $this->fastCache[$userId] = $this->fastCache[$userId] ?? [];
- } else {
- $this->fastLoaded[$userId] = true;
- $this->lazyLoaded[$userId] = $this->lazyLoaded[$userId] ?? false;
- $this->lazyCache[$userId] = $this->lazyCache[$userId] ?? [];
- }
- }
- /**
- * **Warning:** this will load all lazy values from the database
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param bool $filtered TRUE to hide sensitive preference values. Value are replaced by {@see IConfig::SENSITIVE_VALUE}
- *
- * @return array<string, string|int|float|bool|array>
- */
- private function formatAppValues(string $userId, string $app, array $values, bool $filtered = false): array {
- foreach ($values as $key => $value) {
- //$key = (string)$key;
- try {
- $type = $this->getValueType($userId, $app, (string)$key);
- } catch (UnknownKeyException) {
- continue;
- }
- if ($this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags'] ?? 0)) {
- if ($filtered) {
- $value = IConfig::SENSITIVE_VALUE;
- $type = ValueType::STRING;
- } else {
- $this->decryptSensitiveValue($userId, $app, (string)$key, $value);
- }
- }
- $values[$key] = $this->convertTypedValue($value, $type);
- }
- return $values;
- }
- /**
- * convert string value to the expected type
- *
- * @param string $value
- * @param ValueType $type
- *
- * @return string|int|float|bool|array
- */
- private function convertTypedValue(string $value, ValueType $type): string|int|float|bool|array {
- switch ($type) {
- case ValueType::INT:
- return (int)$value;
- case ValueType::FLOAT:
- return (float)$value;
- case ValueType::BOOL:
- return in_array(strtolower($value), ['1', 'true', 'yes', 'on']);
- case ValueType::ARRAY:
- try {
- return json_decode($value, true, flags: JSON_THROW_ON_ERROR);
- } catch (JsonException) {
- // ignoreable
- }
- break;
- }
- return $value;
- }
- private function decryptSensitiveValue(string $userId, string $app, string $key, string &$value): void {
- if (!$this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags'] ?? 0)) {
- return;
- }
- if (!str_starts_with($value, self::ENCRYPTION_PREFIX)) {
- return;
- }
- try {
- $value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
- } catch (\Exception $e) {
- $this->logger->warning('could not decrypt sensitive value', [
- 'userId' => $userId,
- 'app' => $app,
- 'key' => $key,
- 'value' => $value,
- 'exception' => $e
- ]);
- }
- }
- /**
- * verify and compare current use of config values with defined lexicon
- *
- * @throws UnknownKeyException
- * @throws TypeConflictException
- */
- private function compareRegisteredConfigValues(
- string $app,
- string $key,
- bool &$lazy,
- ValueType &$type,
- int &$flags = 0,
- string &$default = '',
- ): bool {
- $configDetails = $this->getConfigDetailsFromLexicon($app);
- if (!array_key_exists($key, $configDetails['entries'])) {
- return $this->applyLexiconStrictness($configDetails['strictness'], 'The user preference key ' . $app . '/' . $key . ' is not defined in the config lexicon');
- }
- /** @var ConfigLexiconEntry $configValue */
- $configValue = $configDetails['entries'][$key];
- if ($type === ValueType::MIXED) {
- $type = $configValue->getValueType()->value; // we overwrite if value was requested as mixed
- } elseif ($configValue->getValueType()->value !== $type) {
- throw new TypeConflictException('The user preference key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
- }
- $lazy = $configValue->isLazy();
- $default = $configValue->getDefault() ?? $default; // default from Lexicon got priority
- $flags = $configValue->getFlags();
- if ($configValue->isDeprecated()) {
- $this->logger->notice('User preference key ' . $app . '/' . $key . ' is set as deprecated.');
- }
- return true;
- }
- /**
- * manage ConfigLexicon behavior based on strictness set in IConfigLexicon
- *
- * @see IConfigLexicon::getStrictness()
- * @param ConfigLexiconStrictness|null $strictness
- * @param string $line
- *
- * @return bool TRUE if conflict can be fully ignored
- * @throws UnknownKeyException
- */
- private function applyLexiconStrictness(?ConfigLexiconStrictness $strictness, string $line = ''): bool {
- if ($strictness === null) {
- return true;
- }
- switch ($strictness) {
- case ConfigLexiconStrictness::IGNORE:
- return true;
- case ConfigLexiconStrictness::NOTICE:
- $this->logger->notice($line);
- return true;
- case ConfigLexiconStrictness::WARNING:
- $this->logger->warning($line);
- return false;
- }
- throw new UnknownKeyException($line);
- }
- /**
- * extract details from registered $appId's config lexicon
- *
- * @param string $appId
- *
- * @return array{entries: array<array-key, ConfigLexiconEntry>, strictness: ConfigLexiconStrictness}
- */
- private function getConfigDetailsFromLexicon(string $appId): array {
- if (!array_key_exists($appId, $this->configLexiconDetails)) {
- $entries = [];
- $bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
- $configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
- foreach ($configLexicon?->getUserPreferences() ?? [] as $configEntry) {
- $entries[$configEntry->getKey()] = $configEntry;
- }
- $this->configLexiconDetails[$appId] = [
- 'entries' => $entries,
- 'strictness' => $configLexicon?->getStrictness() ?? ConfigLexiconStrictness::IGNORE
- ];
- }
- return $this->configLexiconDetails[$appId];
- }
- }
|