123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911 |
- <?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\IUserConfig;
- 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 config in the
- * database.
- * Supports **lazy loading**
- *
- * ### What is lazy loading ?
- * In order to avoid loading useless user config 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 UserConfig implements IUserConfig {
- 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 = '$UserConfigEncryption$';
- private const ENCRYPTION_PREFIX_LENGTH = 22; // 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 config keys
- /** @var array<string, array<string, array<string, mixed>>> ['user_id' => ['app_id' => ['key' => 'value']]] */
- private array $lazyCache = []; // cache for lazy config 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 config values
- /** @var array<string, array<string, array<string, ValueType>>> ['user_id' => ['app_id' => ['key' => bitflag]]] */
- private array $valueTypes = []; // type for all config values
- /** @var array<string, array<string, array<string, int>>> ['user_id' => ['app_id' => ['key' => bitflag]]] */
- private array $valueFlags = []; // type for all config 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->loadConfigAll($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 config keys
- * @since 31.0.0
- */
- public function getKeys(string $userId, string $app): array {
- $this->assertParams($userId, $app);
- $this->loadConfigAll($userId);
- // array_merge() will remove numeric keys (here config 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 config key
- * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
- *
- * @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->loadConfig($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 config key
- * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
- *
- * @return bool
- * @throws UnknownKeyException if config 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->loadConfig($userId, $lazy);
- if (!isset($this->valueDetails[$userId][$app][$key])) {
- throw new UnknownKeyException('unknown config 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 config key
- * @param bool|null $lazy TRUE to search within lazy loaded config, NULL to search within all config
- *
- * @return bool
- * @throws UnknownKeyException if config 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->loadConfig($userId, $lazy);
- if (!isset($this->valueDetails[$userId][$app][$key])) {
- throw new UnknownKeyException('unknown config 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 config key
- *
- * @return bool TRUE if config is lazy loaded
- * @throws UnknownKeyException if config key is not known
- * @see IUserConfig 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 config 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 config
- if ($this->hasKey($userId, $app, $key, true)) {
- return true;
- }
- throw new UnknownKeyException('unknown config key');
- }
- /**
- * @inheritDoc
- *
- * @param string $userId id of the user
- * @param string $app id of the app
- * @param string $prefix config keys prefix to search
- * @param bool $filtered TRUE to hide sensitive config 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->loadConfigAll($userId);
- // array_merge() will remove numeric keys (here config 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 config 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->loadConfigAll($userId);
- $result = [];
- foreach ($this->getApps($userId) as $app) {
- // array_merge() will remove numeric keys (here config 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 config key
- * @param bool $lazy search within lazy loaded config
- * @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->loadConfig($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 config 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 config key
- * @param string $value config 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 config key
- * @param int $value config 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 config key
- * @param array $values list of config 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 config key
- * @param bool $value config 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 config 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 config 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 config key
- * @param string $default config value
- * @param null|bool $lazy get config as lazy loaded or not. can be NULL
- *
- * @return string the value or $default
- * @throws TypeConflictException
- * @internal
- * @since 31.0.0
- * @see IUserConfig 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 config key
- * @param string $default default value
- * @param bool $lazy search within lazy loaded config
- *
- * @return string stored config 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 IUserConfig 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 config key
- * @param int $default default value
- * @param bool $lazy search within lazy loaded config
- *
- * @return int stored config 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 IUserConfig 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 config key
- * @param float $default default value
- * @param bool $lazy search within lazy loaded config
- *
- * @return float stored config 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 IUserConfig 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 config key
- * @param bool $default default value
- * @param bool $lazy search within lazy loaded config
- *
- * @return bool stored config 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 IUserConfig 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 config key
- * @param array $default default value
- * @param bool $lazy search within lazy loaded config
- *
- * @return array stored config 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 IUserConfig 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 config key
- * @param string $default default value
- * @param bool $lazy search within lazy loaded config
- * @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->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, default: $default)) {
- return $default; // returns default if strictness of lexicon is set to WARNING (block and report)
- }
- $this->loadConfig($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 config 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 config key
- *
- * @return ValueType type of the value
- * @throws UnknownKeyException if config key is not known
- * @throws IncorrectTypeException if config 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->loadConfig($userId, $lazy);
- if (!isset($this->valueDetails[$userId][$app][$key]['type'])) {
- throw new UnknownKeyException('unknown config 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 config key
- * @param bool $lazy lazy loading
- *
- * @return int flags applied to value
- * @throws UnknownKeyException if config key is not known
- * @throws IncorrectTypeException if config 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->loadConfig($userId, $lazy);
- if (!isset($this->valueDetails[$userId][$app][$key])) {
- throw new UnknownKeyException('unknown config key');
- }
- return $this->valueDetails[$userId][$app][$key]['flags'];
- }
- /**
- * Store a config 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 config key
- * @param string $value config value
- * @param bool $lazy set config as lazy loaded
- * @param bool $sensitive if TRUE value will be hidden when listing config 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 IUserConfig 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 config key
- * @param string $value config value
- * @param bool $lazy set config as lazy loaded
- * @param bool $sensitive if TRUE value will be hidden when listing config 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 IUserConfig 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 config key
- * @param int $value config value
- * @param bool $lazy set config as lazy loaded
- * @param bool $sensitive if TRUE value will be hidden when listing config 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 IUserConfig 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 config key
- * @param float $value config value
- * @param bool $lazy set config as lazy loaded
- * @param bool $sensitive if TRUE value will be hidden when listing config 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 IUserConfig 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 config key
- * @param bool $value config value
- * @param bool $lazy set config 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 IUserConfig 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 config key
- * @param array $value config value
- * @param bool $lazy set config as lazy loaded
- * @param bool $sensitive if TRUE value will be hidden when listing config 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 IUserConfig 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 config key and its value in database
- *
- * If config key is already known with the exact same config value and same sensitive/lazy status, the
- * database is not updated. If config 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 config key
- * @param string $value config value
- * @param bool $lazy config 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 IUserConfig 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->matchAndApplyLexiconDefinition($app, $key, $lazy, $type, $flags)) {
- return false; // returns false as database is not updated
- }
- $this->loadConfig($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->loadConfigAll($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 config 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 config 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->loadConfigAll($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 config 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->loadConfigAll($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 config 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->loadConfigAll($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 config 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 config 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->loadConfigAll($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 config 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 config key
- *
- * @return array
- * @throws UnknownKeyException if config 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->loadConfigAll($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 config 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 config key
- *
- * @since 31.0.0
- */
- public function deleteUserConfig(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 config 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 deleteAllUserConfig(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->loadConfigAll($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 config 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 loadConfigAll(string $userId): void {
- $this->loadConfig($userId, null);
- }
- /**
- * Load normal config or config set as lazy loaded
- *
- * @param bool|null $lazy set to TRUE to load config set as lazy loaded, set to NULL to load all config
- */
- private function loadConfig(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 UserConfig 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 loadConfig 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 config are loaded
- * - true : will returns true if lazy config are loaded
- * - null : will returns true if both config 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 config as loaded
- * - true : set lazy config as loaded
- * - null : set both config 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 config 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
- ]);
- }
- }
- /**
- * match and apply current use of config values with defined lexicon
- *
- * @throws UnknownKeyException
- * @throws TypeConflictException
- */
- private function matchAndApplyLexiconDefinition(
- 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 config key ' . $app . '/' . $key . ' is not defined in the config lexicon');
- }
- /** @var ConfigLexiconEntry $configValue */
- $configValue = $configDetails['entries'][$key];
- if ($type === ValueType::MIXED) {
- $type = $configValue->getValueType(); // we overwrite if value was requested as mixed
- } elseif ($configValue->getValueType() !== $type) {
- throw new TypeConflictException('The user config 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 config 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;
- case ConfigLexiconStrictness::EXCEPTION:
- throw new UnknownKeyException($line);
- }
- 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?->getUserConfigs() ?? [] as $configEntry) {
- $entries[$configEntry->getKey()] = $configEntry;
- }
- $this->configLexiconDetails[$appId] = [
- 'entries' => $entries,
- 'strictness' => $configLexicon?->getStrictness() ?? ConfigLexiconStrictness::IGNORE
- ];
- }
- return $this->configLexiconDetails[$appId];
- }
- }
|