2024-10-03 09:52:26 -04:00
< ? php
declare ( strict_types = 1 );
/**
* SPDX - FileCopyrightText : 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
*/
2024-10-21 15:31:14 -04:00
namespace OC\Config ;
2024-10-03 09:52:26 -04:00
2024-10-21 15:31:14 -04:00
use Generator ;
2024-10-03 09:52:26 -04:00
use InvalidArgumentException ;
use JsonException ;
2024-12-06 05:35:00 -05:00
use OC\AppFramework\Bootstrap\Coordinator ;
2025-07-16 09:46:45 -04:00
use OCP\Config\Exceptions\IncorrectTypeException ;
use OCP\Config\Exceptions\TypeConflictException ;
use OCP\Config\Exceptions\UnknownKeyException ;
use OCP\Config\IUserConfig ;
use OCP\Config\Lexicon\Entry ;
use OCP\Config\Lexicon\ILexicon ;
use OCP\Config\Lexicon\Strictness ;
use OCP\Config\ValueType ;
2024-10-03 09:52:26 -04:00
use OCP\DB\Exception as DBException ;
use OCP\DB\IResult ;
use OCP\DB\QueryBuilder\IQueryBuilder ;
2023-12-05 10:57:50 -05:00
use OCP\EventDispatcher\IEventDispatcher ;
2024-10-03 09:52:26 -04:00
use OCP\IConfig ;
use OCP\IDBConnection ;
use OCP\Security\ICrypto ;
2023-12-05 10:57:50 -05:00
use OCP\User\Events\UserConfigChangedEvent ;
2024-10-03 09:52:26 -04:00
use Psr\Log\LoggerInterface ;
/**
2024-11-18 16:09:45 -05:00
* This class provides an easy way for apps to store user config in the
2024-10-03 09:52:26 -04:00
* database .
* Supports ** lazy loading **
*
* ### What is lazy loading ?
2024-11-18 16:09:45 -05:00
* In order to avoid loading useless user config into memory for each request ,
2024-10-03 09:52:26 -04:00
* 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
*/
2024-11-18 16:09:45 -05:00
class UserConfig implements IUserConfig {
2024-10-03 09:52:26 -04:00
private const USER_MAX_LENGTH = 64 ;
private const APP_MAX_LENGTH = 32 ;
private const KEY_MAX_LENGTH = 64 ;
2024-10-17 05:48:09 -04:00
private const INDEX_MAX_LENGTH = 64 ;
2024-11-18 16:09:45 -05:00
private const ENCRYPTION_PREFIX = '$UserConfigEncryption$' ;
private const ENCRYPTION_PREFIX_LENGTH = 22 ; // strlen(self::ENCRYPTION_PREFIX)
2024-10-03 09:52:26 -04:00
2024-10-17 05:48:09 -04:00
/** @var array<string, array<string, array<string, mixed>>> [ass'user_id' => ['app_id' => ['key' => 'value']]] */
2024-11-18 16:09:45 -05:00
private array $fastCache = []; // cache for normal config keys
2024-10-03 09:52:26 -04:00
/** @var array<string, array<string, array<string, mixed>>> ['user_id' => ['app_id' => ['key' => 'value']]] */
2024-11-18 16:09:45 -05:00
private array $lazyCache = []; // cache for lazy config keys
2024-10-21 15:31:14 -04:00
/** @var array<string, array<string, array<string, array<string, mixed>>>> ['user_id' => ['app_id' => ['key' => ['type' => ValueType, 'flags' => bitflag]]]] */
2024-11-18 16:09:45 -05:00
private array $valueDetails = []; // type for all config values
2024-10-03 09:52:26 -04:00
/** @var array<string, boolean> ['user_id' => bool] */
private array $fastLoaded = [];
/** @var array<string, boolean> ['user_id' => bool] */
private array $lazyLoaded = [];
2025-07-16 09:46:45 -04:00
/** @var array<string, array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
2024-12-06 05:35:00 -05:00
private array $configLexiconDetails = [];
2025-05-14 14:10:34 -04:00
private bool $ignoreLexiconAliases = false ;
2025-08-29 08:41:08 -04:00
private array $strictnessApplied = [];
2024-10-03 09:52:26 -04:00
public function __construct (
protected IDBConnection $connection ,
2025-01-09 05:42:53 -05:00
protected IConfig $config ,
2025-07-29 10:47:23 -04:00
private readonly ConfigManager $configManager ,
private readonly PresetManager $presetManager ,
2024-10-03 09:52:26 -04:00
protected LoggerInterface $logger ,
protected ICrypto $crypto ,
2023-12-05 10:57:50 -05:00
protected IEventDispatcher $dispatcher ,
2024-10-03 09:52:26 -04:00
) {
}
/**
* @ 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 );
2024-11-18 16:09:45 -05:00
$this -> loadConfigAll ( $userId );
2024-10-03 09:52:26 -04:00
$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
*
2024-11-18 16:09:45 -05:00
* @ return list < string > list of stored config keys
2024-10-03 09:52:26 -04:00
* @ since 31.0 . 0
*/
public function getKeys ( string $userId , string $app ) : array {
$this -> assertParams ( $userId , $app );
2024-11-18 16:09:45 -05:00
$this -> loadConfigAll ( $userId );
// array_merge() will remove numeric keys (here config keys), so addition arrays instead
2024-10-03 09:52:26 -04:00
$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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
* @ param bool | null $lazy TRUE to search within lazy loaded config , NULL to search within all config
2024-10-03 09:52:26 -04:00
*
* @ 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 );
2024-11-18 16:09:45 -05:00
$this -> loadConfig ( $userId , $lazy );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
2024-10-03 09:52:26 -04:00
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 ]);
}
/**
2024-10-17 05:48:09 -04:00
* @ inheritDoc
*
2024-10-03 09:52:26 -04:00
* @ param string $userId id of the user
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ param string $key config key
* @ param bool | null $lazy TRUE to search within lazy loaded config , NULL to search within all config
2024-10-03 09:52:26 -04:00
*
* @ return bool
2024-11-18 16:09:45 -05:00
* @ throws UnknownKeyException if config key is not known
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
2024-10-03 09:52:26 -04:00
*/
public function isSensitive ( string $userId , string $app , string $key , ? bool $lazy = false ) : bool {
$this -> assertParams ( $userId , $app , $key );
2024-11-18 16:09:45 -05:00
$this -> loadConfig ( $userId , $lazy );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
2024-10-03 09:52:26 -04:00
2024-10-17 05:48:09 -04:00
if ( ! isset ( $this -> valueDetails [ $userId ][ $app ][ $key ])) {
2024-11-18 16:09:45 -05:00
throw new UnknownKeyException ( 'unknown config key' );
2024-10-03 09:52:26 -04:00
}
2024-10-17 05:48:09 -04:00
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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
* @ param bool | null $lazy TRUE to search within lazy loaded config , NULL to search within all config
2024-10-17 05:48:09 -04:00
*
* @ return bool
2024-11-18 16:09:45 -05:00
* @ throws UnknownKeyException if config key is not known
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
*/
public function isIndexed ( string $userId , string $app , string $key , ? bool $lazy = false ) : bool {
$this -> assertParams ( $userId , $app , $key );
2024-11-18 16:09:45 -05:00
$this -> loadConfig ( $userId , $lazy );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
2024-10-17 05:48:09 -04:00
if ( ! isset ( $this -> valueDetails [ $userId ][ $app ][ $key ])) {
2024-11-18 16:09:45 -05:00
throw new UnknownKeyException ( 'unknown config key' );
2024-10-17 05:48:09 -04:00
}
return $this -> isFlagged ( self :: FLAG_INDEXED , $this -> valueDetails [ $userId ][ $app ][ $key ][ 'flags' ]);
2024-10-03 09:52:26 -04:00
}
/**
* @ inheritDoc
*
* @ param string $userId id of the user
* @ param string $app if of the app
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
*
2024-11-18 16:09:45 -05:00
* @ return bool TRUE if config is lazy loaded
* @ throws UnknownKeyException if config key is not known
* @ see IUserConfig for details about lazy loading
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
2024-10-03 09:52:26 -04:00
*/
public function isLazy ( string $userId , string $app , string $key ) : bool {
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
2024-11-18 16:09:45 -05:00
// there is a huge probability the non-lazy config are already loaded
2024-10-17 05:48:09 -04:00
// meaning that we can start by only checking if a current non-lazy key exists
2024-10-03 09:52:26 -04:00
if ( $this -> hasKey ( $userId , $app , $key , false )) {
2025-01-14 06:38:08 -05:00
// meaning key is not lazy.
return false ;
2024-10-03 09:52:26 -04:00
}
2024-11-18 16:09:45 -05:00
// as key is not found as non-lazy, we load and search in the lazy config
2024-10-03 09:52:26 -04:00
if ( $this -> hasKey ( $userId , $app , $key , true )) {
return true ;
}
2024-11-18 16:09:45 -05:00
throw new UnknownKeyException ( 'unknown config key' );
2024-10-03 09:52:26 -04:00
}
/**
* @ inheritDoc
*
* @ param string $userId id of the user
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ 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 }
2024-10-03 09:52:26 -04:00
*
* @ 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
2024-11-18 16:09:45 -05:00
$this -> loadConfigAll ( $userId );
// array_merge() will remove numeric keys (here config keys), so addition arrays instead
2024-10-03 09:52:26 -04:00
$values = array_filter (
$this -> formatAppValues ( $userId , $app , ( $this -> fastCache [ $userId ][ $app ] ? ? []) + ( $this -> lazyCache [ $userId ][ $app ] ? ? []), $filtered ),
function ( string $key ) use ( $prefix ) : bool {
2025-01-14 06:38:08 -05:00
// filter values based on $prefix
return str_starts_with ( $key , $prefix );
2024-10-03 09:52:26 -04:00
}, ARRAY_FILTER_USE_KEY
);
return $values ;
}
/**
* @ inheritDoc
*
* @ param string $userId id of the user
2024-11-18 16:09:45 -05:00
* @ param bool $filtered TRUE to hide sensitive config values . Value are replaced by { @ see IConfig :: SENSITIVE_VALUE }
2024-10-03 09:52:26 -04:00
*
* @ 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 );
2024-11-18 16:09:45 -05:00
$this -> loadConfigAll ( $userId );
2024-10-03 09:52:26 -04:00
$result = [];
foreach ( $this -> getApps ( $userId ) as $app ) {
2024-11-18 16:09:45 -05:00
// array_merge() will remove numeric keys (here config keys), so addition arrays instead
2024-10-03 09:52:26 -04:00
$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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
* @ param bool $lazy search within lazy loaded config
2024-10-03 09:52:26 -04:00
* @ param ValueType | null $typedAs enforce type for the returned values
*
* @ return array < string , string | int | float | bool | array > [ appId => value ]
* @ since 31.0 . 0
*/
2024-10-17 05:48:09 -04:00
public function getValuesByApps ( string $userId , string $key , bool $lazy = false , ? ValueType $typedAs = null ) : array {
2024-10-03 09:52:26 -04:00
$this -> assertParams ( $userId , '' , $key , allowEmptyApp : true );
2024-11-18 16:09:45 -05:00
$this -> loadConfig ( $userId , $lazy );
2024-10-03 09:52:26 -04:00
/** @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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
* @ 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
*/
2024-10-17 05:48:09 -04:00
public function getValuesByUsers (
2024-10-03 09:52:26 -04:00
string $app ,
string $key ,
? ValueType $typedAs = null ,
? array $userIds = null ,
) : array {
$this -> assertParams ( '' , $app , $key , allowEmptyUser : true );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( '' , $app , $key );
2024-10-03 09:52:26 -04:00
$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 {
2024-10-21 15:31:14 -04:00
$value = $this -> convertTypedValue ( $value , $typedAs ? ? ValueType :: from (( int ) $row [ 'type' ]));
2024-10-03 09:52:26 -04:00
} 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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
* @ param string $value config value
2024-10-03 09:52:26 -04:00
* @ param bool $caseInsensitive non - case - sensitive search , only works if $value is a string
*
2024-10-21 15:31:14 -04:00
* @ return Generator < string >
2024-10-03 09:52:26 -04:00
* @ since 31.0 . 0
*/
2024-10-21 15:31:14 -04:00
public function searchUsersByValueString ( string $app , string $key , string $value , bool $caseInsensitive = false ) : Generator {
2024-10-03 09:52:26 -04:00
return $this -> searchUsersByTypedValue ( $app , $key , $value , $caseInsensitive );
}
/**
* @ inheritDoc
*
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ param string $key config key
* @ param int $value config value
2024-10-03 09:52:26 -04:00
*
2024-10-21 15:31:14 -04:00
* @ return Generator < string >
2024-10-03 09:52:26 -04:00
* @ since 31.0 . 0
*/
2024-10-21 15:31:14 -04:00
public function searchUsersByValueInt ( string $app , string $key , int $value ) : Generator {
2024-10-03 09:52:26 -04:00
return $this -> searchUsersByValueString ( $app , $key , ( string ) $value );
}
/**
* @ inheritDoc
*
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ param string $key config key
* @ param array $values list of config values
2024-10-03 09:52:26 -04:00
*
2024-10-21 15:31:14 -04:00
* @ return Generator < string >
2024-10-03 09:52:26 -04:00
* @ since 31.0 . 0
*/
2024-10-21 15:31:14 -04:00
public function searchUsersByValues ( string $app , string $key , array $values ) : Generator {
2024-10-03 09:52:26 -04:00
return $this -> searchUsersByTypedValue ( $app , $key , $values );
}
/**
* @ inheritDoc
*
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ param string $key config key
* @ param bool $value config value
2024-10-03 09:52:26 -04:00
*
2024-10-21 15:31:14 -04:00
* @ return Generator < string >
2024-10-03 09:52:26 -04:00
* @ since 31.0 . 0
*/
2024-10-21 15:31:14 -04:00
public function searchUsersByValueBool ( string $app , string $key , bool $value ) : Generator {
2024-10-03 09:52:26 -04:00
$values = [ '0' , 'off' , 'false' , 'no' ];
if ( $value ) {
$values = [ '1' , 'on' , 'true' , 'yes' ];
}
return $this -> searchUsersByValues ( $app , $key , $values );
}
/**
2024-11-18 16:09:45 -05:00
* returns a list of users with config key set to a specific value , or within the list of
2024-10-03 09:52:26 -04:00
* possible values
*
* @ param string $app
* @ param string $key
* @ param string | array $value
* @ param bool $caseInsensitive
*
2024-10-21 15:31:14 -04:00
* @ return Generator < string >
2024-10-03 09:52:26 -04:00
*/
2024-10-21 15:31:14 -04:00
private function searchUsersByTypedValue ( string $app , string $key , string | array $value , bool $caseInsensitive = false ) : Generator {
2024-10-03 09:52:26 -04:00
$this -> assertParams ( '' , $app , $key , allowEmptyUser : true );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( '' , $app , $key );
2024-10-03 09:52:26 -04:00
2025-09-16 07:46:45 -04:00
$lexiconEntry = $this -> getLexiconEntry ( $app , $key );
if ( $lexiconEntry ? -> isFlagged ( self :: FLAG_INDEXED ) === false ) {
2025-09-16 09:52:27 -04:00
$this -> logger -> notice ( 'UserConfig+Lexicon: using searchUsersByTypedValue on config key ' . $app . '/' . $key . ' which is not set as indexed' );
2025-09-16 07:46:45 -04:00
}
2024-10-03 09:52:26 -04:00
$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 )));
$configValueColumn = ( $this -> connection -> getDatabaseProvider () === IDBConnection :: PLATFORM_ORACLE ) ? $qb -> expr () -> castColumn ( 'configvalue' , IQueryBuilder :: PARAM_STR ) : 'configvalue' ;
if ( is_array ( $value )) {
2025-09-16 07:46:45 -04:00
$where = $qb -> expr () -> in ( 'indexed' , $qb -> createNamedParameter ( $value , IQueryBuilder :: PARAM_STR_ARRAY ));
// in case lexicon does not exist for this key - or is not set as indexed - we keep searching for non-index entries if 'flags' is set as not indexed
if ( $lexiconEntry ? -> isFlagged ( self :: FLAG_INDEXED ) !== true ) {
2024-10-17 05:48:09 -04:00
$where = $qb -> expr () -> orX (
2025-09-16 07:46:45 -04:00
$where ,
2024-10-17 05:48:09 -04:00
$qb -> expr () -> andX (
2024-10-21 15:31:14 -04:00
$qb -> expr () -> neq ( $qb -> expr () -> bitwiseAnd ( 'flags' , self :: FLAG_INDEXED ), $qb -> createNamedParameter ( self :: FLAG_INDEXED , IQueryBuilder :: PARAM_INT )),
2025-09-16 07:46:45 -04:00
$qb -> expr () -> in ( $configValueColumn , $qb -> createNamedParameter ( $value , IQueryBuilder :: PARAM_STR_ARRAY ))
2024-10-17 05:48:09 -04:00
)
2024-10-03 09:52:26 -04:00
);
2025-09-16 07:46:45 -04:00
}
} else {
if ( $caseInsensitive ) {
$where = $qb -> expr () -> eq ( $qb -> func () -> lower ( 'indexed' ), $qb -> createNamedParameter ( strtolower ( $value )));
// in case lexicon does not exist for this key - or is not set as indexed - we keep searching for non-index entries if 'flags' is set as not indexed
if ( $lexiconEntry ? -> isFlagged ( self :: FLAG_INDEXED ) !== true ) {
$where = $qb -> expr () -> orX (
$where ,
$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 )))
)
);
}
2024-10-03 09:52:26 -04:00
} else {
2025-09-16 07:46:45 -04:00
$where = $qb -> expr () -> eq ( 'indexed' , $qb -> createNamedParameter ( $value ));
// in case lexicon does not exist for this key - or is not set as indexed - we keep searching for non-index entries if 'flags' is set as not indexed
if ( $lexiconEntry ? -> isFlagged ( self :: FLAG_INDEXED ) !== true ) {
$where = $qb -> expr () -> orX (
$where ,
$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 ))
)
);
}
2024-10-03 09:52:26 -04:00
}
}
2024-10-17 05:48:09 -04:00
$qb -> andWhere ( $where );
2024-10-03 09:52:26 -04:00
$result = $qb -> executeQuery ();
2024-10-21 15:31:14 -04:00
while ( $row = $result -> fetch ()) {
yield $row [ 'userid' ];
2024-10-03 09:52:26 -04:00
}
}
/**
2024-11-18 16:09:45 -05:00
* Get the config value as string .
2024-10-03 09:52:26 -04:00
* 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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
* @ param string $default config value
* @ param null | bool $lazy get config as lazy loaded or not . can be NULL
2024-10-03 09:52:26 -04:00
*
* @ return string the value or $default
* @ throws TypeConflictException
* @ internal
* @ since 31.0 . 0
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
* @ 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 {
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
2024-10-03 09:52:26 -04:00
try {
2024-10-17 05:48:09 -04:00
$lazy ? ? = $this -> isLazy ( $userId , $app , $key );
2024-10-03 09:52:26 -04:00
} 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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
* @ param string $default default value
2024-11-18 16:09:45 -05:00
* @ param bool $lazy search within lazy loaded config
2024-10-03 09:52:26 -04:00
*
2024-11-18 16:09:45 -05:00
* @ return string stored config value or $default if not set in database
2024-10-03 09:52:26 -04:00
* @ 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
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
*/
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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
* @ param int $default default value
2024-11-18 16:09:45 -05:00
* @ param bool $lazy search within lazy loaded config
2024-10-03 09:52:26 -04:00
*
2024-11-18 16:09:45 -05:00
* @ return int stored config value or $default if not set in database
2024-10-03 09:52:26 -04:00
* @ 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
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
*/
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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
* @ param float $default default value
2024-11-18 16:09:45 -05:00
* @ param bool $lazy search within lazy loaded config
2024-10-03 09:52:26 -04:00
*
2024-11-18 16:09:45 -05:00
* @ return float stored config value or $default if not set in database
2024-10-03 09:52:26 -04:00
* @ throws InvalidArgumentException if one of the argument format is invalid
* @ throws TypeConflictException in case of conflict with the value type set in database
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
*/
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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
* @ param bool $default default value
2024-11-18 16:09:45 -05:00
* @ param bool $lazy search within lazy loaded config
2024-10-03 09:52:26 -04:00
*
2024-11-18 16:09:45 -05:00
* @ return bool stored config value or $default if not set in database
2024-10-03 09:52:26 -04:00
* @ throws InvalidArgumentException if one of the argument format is invalid
* @ throws TypeConflictException in case of conflict with the value type set in database
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
*/
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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
* @ param array $default default value
2024-11-18 16:09:45 -05:00
* @ param bool $lazy search within lazy loaded config
2024-10-03 09:52:26 -04:00
*
2024-11-18 16:09:45 -05:00
* @ return array stored config value or $default if not set in database
2024-10-03 09:52:26 -04:00
* @ throws InvalidArgumentException if one of the argument format is invalid
* @ throws TypeConflictException in case of conflict with the value type set in database
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
*/
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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
* @ param string $default default value
2024-11-18 16:09:45 -05:00
* @ param bool $lazy search within lazy loaded config
2024-10-03 09:52:26 -04:00
* @ 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 {
2024-10-21 15:31:14 -04:00
$this -> assertParams ( $userId , $app , $key );
2025-05-14 14:10:34 -04:00
$origKey = $key ;
2025-06-11 14:18:44 -04:00
$matched = $this -> matchAndApplyLexiconDefinition ( $userId , $app , $key , $lazy , $type , default : $default );
if ( $default === null ) {
// there is no logical reason for it to be null
throw new \Exception ( 'default cannot be null' );
}
// returns default if strictness of lexicon is set to WARNING (block and report)
if ( ! $matched ) {
2025-01-14 06:38:08 -05:00
return $default ;
2024-12-06 05:35:00 -05:00
}
2025-06-11 14:18:44 -04:00
2024-11-18 16:09:45 -05:00
$this -> loadConfig ( $userId , $lazy );
2024-10-03 09:52:26 -04:00
/**
* 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 .
*/
2024-10-17 05:48:09 -04:00
$knownType = $this -> valueDetails [ $userId ][ $app ][ $key ][ 'type' ] ? ? null ;
if ( $type !== ValueType :: MIXED
&& $knownType !== null
&& $knownType !== ValueType :: MIXED
&& $type !== $knownType ) {
2024-10-03 09:52:26 -04:00
$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
*
2024-11-18 16:09:45 -05:00
* This way , lazyCache will be empty until the load for lazy config value is requested .
2024-10-03 09:52:26 -04:00
*/
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 );
2025-05-14 14:10:34 -04:00
// in case the key was modified while running matchAndApplyLexiconDefinition() we are
// interested to check options in case a modification of the value is needed
2025-06-17 13:02:08 -04:00
// ie inverting value from previous key when using lexicon option RENAME_INVERT_BOOLEAN
2025-06-04 09:23:36 -04:00
if ( $origKey !== $key && $type === ValueType :: BOOL ) {
2025-07-29 10:47:23 -04:00
$value = ( $this -> configManager -> convertToBool ( $value , $this -> getLexiconEntry ( $app , $key ))) ? '1' : '0' ;
2025-05-14 14:10:34 -04:00
}
2024-10-03 09:52:26 -04:00
return $value ;
}
/**
* @ inheritDoc
*
* @ param string $userId id of the user
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
*
* @ return ValueType type of the value
2024-11-18 16:09:45 -05:00
* @ throws UnknownKeyException if config key is not known
* @ throws IncorrectTypeException if config value type is not known
2024-10-03 09:52:26 -04:00
* @ since 31.0 . 0
*/
public function getValueType ( string $userId , string $app , string $key , ? bool $lazy = null ) : ValueType {
$this -> assertParams ( $userId , $app , $key );
2024-11-18 16:09:45 -05:00
$this -> loadConfig ( $userId , $lazy );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
2024-10-03 09:52:26 -04:00
2024-10-17 05:48:09 -04:00
if ( ! isset ( $this -> valueDetails [ $userId ][ $app ][ $key ][ 'type' ])) {
2024-11-18 16:09:45 -05:00
throw new UnknownKeyException ( 'unknown config key' );
2024-10-03 09:52:26 -04:00
}
2024-10-17 05:48:09 -04:00
return $this -> valueDetails [ $userId ][ $app ][ $key ][ 'type' ];
2024-10-03 09:52:26 -04:00
}
/**
2024-10-17 05:48:09 -04:00
* @ inheritDoc
2024-10-03 09:52:26 -04:00
*
2024-10-17 05:48:09 -04:00
* @ param string $userId id of the user
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-17 05:48:09 -04:00
* @ param bool $lazy lazy loading
2024-10-03 09:52:26 -04:00
*
2024-10-21 15:31:14 -04:00
* @ return int flags applied to value
2024-11-18 16:09:45 -05:00
* @ throws UnknownKeyException if config key is not known
* @ throws IncorrectTypeException if config value type is not known
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
2024-10-03 09:52:26 -04:00
*/
2024-10-17 05:48:09 -04:00
public function getValueFlags ( string $userId , string $app , string $key , bool $lazy = false ) : int {
$this -> assertParams ( $userId , $app , $key );
2024-11-18 16:09:45 -05:00
$this -> loadConfig ( $userId , $lazy );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
2024-10-03 09:52:26 -04:00
2024-10-17 05:48:09 -04:00
if ( ! isset ( $this -> valueDetails [ $userId ][ $app ][ $key ])) {
2024-11-18 16:09:45 -05:00
throw new UnknownKeyException ( 'unknown config key' );
2024-10-03 09:52:26 -04:00
}
2024-10-17 05:48:09 -04:00
return $this -> valueDetails [ $userId ][ $app ][ $key ][ 'flags' ];
2024-10-03 09:52:26 -04:00
}
/**
2024-11-18 16:09:45 -05:00
* Store a config key and its value in database as VALUE_MIXED
2024-10-03 09:52:26 -04:00
*
* ** 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
2024-11-18 16:09:45 -05:00
* @ 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 .
2024-10-03 09:52:26 -04:00
*
* @ return bool TRUE if value was different , therefor updated in database
* @ throws TypeConflictException if type from database is not VALUE_MIXED
* @ internal
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
* @ see setValueString ()
* @ see setValueInt ()
* @ see setValueFloat ()
* @ see setValueBool ()
* @ see setValueArray ()
*/
public function setValueMixed (
string $userId ,
string $app ,
string $key ,
string $value ,
bool $lazy = false ,
2024-10-17 05:48:09 -04:00
int $flags = 0 ,
2024-10-03 09:52:26 -04:00
) : bool {
return $this -> setTypedValue (
$userId ,
$app ,
$key ,
$value ,
$lazy ,
2024-10-17 05:48:09 -04:00
$flags ,
2024-10-03 09:52:26 -04:00
ValueType :: MIXED
);
}
/**
* @ inheritDoc
*
* @ param string $userId id of the user
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ 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 .
2024-10-03 09:52:26 -04:00
*
* @ 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
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
*/
public function setValueString (
string $userId ,
string $app ,
string $key ,
string $value ,
bool $lazy = false ,
2024-10-17 05:48:09 -04:00
int $flags = 0 ,
2024-10-03 09:52:26 -04:00
) : bool {
return $this -> setTypedValue (
$userId ,
$app ,
$key ,
$value ,
$lazy ,
2024-10-17 05:48:09 -04:00
$flags ,
2024-10-03 09:52:26 -04:00
ValueType :: STRING
);
}
/**
* @ inheritDoc
*
* @ param string $userId id of the user
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ 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 .
2024-10-03 09:52:26 -04:00
*
* @ 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
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
*/
public function setValueInt (
string $userId ,
string $app ,
string $key ,
int $value ,
bool $lazy = false ,
2024-10-17 05:48:09 -04:00
int $flags = 0 ,
2024-10-03 09:52:26 -04:00
) : 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 ,
2024-10-17 05:48:09 -04:00
$flags ,
2024-10-03 09:52:26 -04:00
ValueType :: INT
);
}
/**
* @ inheritDoc
*
* @ param string $userId id of the user
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ 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 .
2024-10-03 09:52:26 -04:00
*
* @ 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
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
*/
public function setValueFloat (
string $userId ,
string $app ,
string $key ,
float $value ,
bool $lazy = false ,
2024-10-17 05:48:09 -04:00
int $flags = 0 ,
2024-10-03 09:52:26 -04:00
) : bool {
return $this -> setTypedValue (
$userId ,
$app ,
$key ,
( string ) $value ,
$lazy ,
2024-10-17 05:48:09 -04:00
$flags ,
2024-10-03 09:52:26 -04:00
ValueType :: FLOAT
);
}
/**
* @ inheritDoc
*
* @ param string $userId id of the user
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ param string $key config key
* @ param bool $value config value
* @ param bool $lazy set config as lazy loaded
2024-10-03 09:52:26 -04:00
*
* @ 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
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
*/
public function setValueBool (
string $userId ,
string $app ,
string $key ,
bool $value ,
bool $lazy = false ,
2024-10-21 15:31:14 -04:00
int $flags = 0 ,
2024-10-03 09:52:26 -04:00
) : bool {
return $this -> setTypedValue (
$userId ,
$app ,
$key ,
( $value ) ? '1' : '0' ,
$lazy ,
2024-10-17 05:48:09 -04:00
$flags ,
2024-10-03 09:52:26 -04:00
ValueType :: BOOL
);
}
/**
* @ inheritDoc
*
* @ param string $userId id of the user
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ 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 .
2024-10-03 09:52:26 -04:00
*
* @ 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
2024-10-17 05:48:09 -04:00
* @ since 31.0 . 0
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
*/
public function setValueArray (
string $userId ,
string $app ,
string $key ,
array $value ,
bool $lazy = false ,
2024-10-17 05:48:09 -04:00
int $flags = 0 ,
2024-10-03 09:52:26 -04:00
) : bool {
try {
return $this -> setTypedValue (
$userId ,
$app ,
$key ,
json_encode ( $value , JSON_THROW_ON_ERROR ),
$lazy ,
2024-10-17 05:48:09 -04:00
$flags ,
2024-10-03 09:52:26 -04:00
ValueType :: ARRAY
);
} catch ( JsonException $e ) {
$this -> logger -> warning ( 'could not setValueArray' , [ 'app' => $app , 'key' => $key , 'exception' => $e ]);
throw $e ;
}
}
/**
2024-11-18 16:09:45 -05:00
* Store a config key and its value in database
2024-10-03 09:52:26 -04:00
*
2024-11-18 16:09:45 -05:00
* 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
2024-10-03 09:52:26 -04:00
* altered .
*
* @ param string $userId id of the user
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ param string $key config key
* @ param string $value config value
* @ param bool $lazy config set as lazy loaded
2024-10-03 09:52:26 -04:00
* @ 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
2024-11-18 16:09:45 -05:00
* @ see IUserConfig for explanation about lazy loading
2024-10-03 09:52:26 -04:00
*/
private function setTypedValue (
string $userId ,
string $app ,
string $key ,
string $value ,
bool $lazy ,
2024-10-17 05:48:09 -04:00
int $flags ,
2024-10-03 09:52:26 -04:00
ValueType $type ,
) : bool {
2025-06-20 04:13:56 -04:00
// Primary email addresses are always(!) expected to be lowercase
if ( $app === 'settings' && $key === 'email' ) {
$value = strtolower ( $value );
}
2024-10-21 15:31:14 -04:00
$this -> assertParams ( $userId , $app , $key );
2025-01-09 05:42:53 -05:00
if ( ! $this -> matchAndApplyLexiconDefinition ( $userId , $app , $key , $lazy , $type , $flags )) {
2025-01-14 06:38:08 -05:00
// returns false as database is not updated
return false ;
2024-12-06 05:35:00 -05:00
}
2024-11-18 16:09:45 -05:00
$this -> loadConfig ( $userId , $lazy );
2024-10-03 09:52:26 -04:00
$inserted = $refreshCache = false ;
$origValue = $value ;
2024-10-17 05:48:09 -04:00
$sensitive = $this -> isFlagged ( self :: FLAG_SENSITIVE , $flags );
2024-10-03 09:52:26 -04:00
if ( $sensitive || ( $this -> hasKey ( $userId , $app , $key , $lazy ) && $this -> isSensitive ( $userId , $app , $key , $lazy ))) {
$value = self :: ENCRYPTION_PREFIX . $this -> crypto -> encrypt ( $value );
2024-12-06 05:35:00 -05:00
$flags |= self :: FLAG_SENSITIVE ;
2024-10-17 05:48:09 -04:00
}
// 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' );
2024-10-21 15:31:14 -04:00
} elseif ( strlen ( $value ) > self :: USER_MAX_LENGTH ) {
2024-10-17 05:48:09 -04:00
$this -> logger -> warning ( 'value is too lengthy to be indexed' );
} else {
$indexed = $value ;
}
2024-10-03 09:52:26 -04:00
}
2023-12-05 10:57:50 -05:00
$oldValue = null ;
2024-10-03 09:52:26 -04:00
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 .
*/
2023-12-05 10:57:50 -05:00
$oldValue = $this -> getTypedValue ( $userId , $app , $key , $value , $lazy , $type );
if ( $origValue === $oldValue
2024-10-03 09:52:26 -04:00
&& ( ! $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 ))
2024-10-17 05:48:09 -04:00
-> setValue ( 'type' , $insert -> createNamedParameter ( $type -> value , IQueryBuilder :: PARAM_INT ))
-> setValue ( 'flags' , $insert -> createNamedParameter ( $flags , IQueryBuilder :: PARAM_INT ))
-> setValue ( 'indexed' , $insert -> createNamedParameter ( $indexed ))
2024-10-03 09:52:26 -04:00
-> 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 ) {
2025-01-14 06:38:08 -05:00
// TODO: throw exception or just log and returns false !?
throw $e ;
2024-10-03 09:52:26 -04:00
}
}
}
/**
* We cannot insert a new row , meaning we need to update an already existing one
*/
if ( ! $inserted ) {
2024-10-17 05:48:09 -04:00
$currType = $this -> valueDetails [ $userId ][ $app ][ $key ][ 'type' ] ? ? null ;
if ( $currType === null ) { // this might happen when switching lazy loading status
2024-11-18 16:09:45 -05:00
$this -> loadConfigAll ( $userId );
2024-10-17 05:48:09 -04:00
$currType = $this -> valueDetails [ $userId ][ $app ][ $key ][ 'type' ];
2024-10-03 09:52:26 -04:00
}
/**
* We only log a warning and set it to VALUE_MIXED .
*/
2024-10-17 05:48:09 -04:00
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 ;
2024-10-03 09:52:26 -04:00
}
/**
* we only accept a different type from the one stored in database
* if the one stored in database is not - defined ( VALUE_MIXED )
*/
2025-06-30 09:04:05 -04:00
if ( $currType !== ValueType :: MIXED
&& $currType !== $type ) {
2024-10-03 09:52:26 -04:00
try {
2024-10-17 05:48:09 -04:00
$currTypeDef = $currType -> getDefinition ();
$typeDef = $type -> getDefinition ();
2024-10-03 09:52:26 -04:00
} catch ( IncorrectTypeException ) {
2024-10-17 05:48:09 -04:00
$currTypeDef = $currType -> value ;
$typeDef = $type -> value ;
2024-10-03 09:52:26 -04:00
}
2024-10-17 05:48:09 -04:00
throw new TypeConflictException ( 'conflict between new type (' . $typeDef . ') and old type (' . $currTypeDef . ')' );
2024-10-03 09:52:26 -04:00
}
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 ))
2024-10-17 05:48:09 -04:00
-> set ( 'type' , $update -> createNamedParameter ( $type -> value , IQueryBuilder :: PARAM_INT ))
-> set ( 'flags' , $update -> createNamedParameter ( $flags , IQueryBuilder :: PARAM_INT ))
-> set ( 'indexed' , $update -> createNamedParameter ( $indexed ))
2024-10-03 09:52:26 -04:00
-> 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 ();
}
2023-12-05 10:57:50 -05:00
$this -> dispatcher -> dispatchTyped ( new UserConfigChangedEvent ( $userId , $app , $key , $value , $oldValue ));
2024-10-03 09:52:26 -04:00
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 ;
}
2024-10-17 05:48:09 -04:00
$this -> valueDetails [ $userId ][ $app ][ $key ] = [
'type' => $type ,
'flags' => $flags
];
2024-10-03 09:52:26 -04:00
return true ;
}
/**
2024-11-18 16:09:45 -05:00
* Change the type of config value .
2024-10-03 09:52:26 -04:00
*
* ** 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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
* @ 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 {
2024-10-21 15:31:14 -04:00
$this -> assertParams ( $userId , $app , $key );
2024-11-18 16:09:45 -05:00
$this -> loadConfigAll ( $userId );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
$this -> isLazy ( $userId , $app , $key ); // confirm key exists
2024-10-03 09:52:26 -04:00
$update = $this -> connection -> getQueryBuilder ();
$update -> update ( 'preferences' )
2024-10-17 05:48:09 -04:00
-> set ( 'type' , $update -> createNamedParameter ( $type -> value , IQueryBuilder :: PARAM_INT ))
2024-10-03 09:52:26 -04:00
-> 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 ();
2024-10-17 05:48:09 -04:00
$this -> valueDetails [ $userId ][ $app ][ $key ][ 'type' ] = $type ;
2024-10-03 09:52:26 -04:00
return true ;
}
/**
* @ inheritDoc
*
* @ param string $userId id of the user
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
* @ 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 );
2024-11-18 16:09:45 -05:00
$this -> loadConfigAll ( $userId );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
2024-10-03 09:52:26 -04:00
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 ])) {
2024-11-18 16:09:45 -05:00
throw new UnknownKeyException ( 'unknown config key' );
2024-10-03 09:52:26 -04:00
}
$value = $cache [ $userId ][ $app ][ $key ];
2024-10-17 05:48:09 -04:00
$flags = $this -> getValueFlags ( $userId , $app , $key );
2024-10-03 09:52:26 -04:00
if ( $sensitive ) {
2024-10-17 05:48:09 -04:00
$flags |= self :: FLAG_SENSITIVE ;
2024-10-03 09:52:26 -04:00
$value = self :: ENCRYPTION_PREFIX . $this -> crypto -> encrypt ( $value );
} else {
2024-10-17 05:48:09 -04:00
$flags &= ~ self :: FLAG_SENSITIVE ;
2024-10-03 09:52:26 -04:00
$this -> decryptSensitiveValue ( $userId , $app , $key , $value );
}
$update = $this -> connection -> getQueryBuilder ();
$update -> update ( 'preferences' )
2024-10-17 05:48:09 -04:00
-> set ( 'flags' , $update -> createNamedParameter ( $flags , IQueryBuilder :: PARAM_INT ))
2024-10-03 09:52:26 -04:00
-> 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 ();
2024-10-17 05:48:09 -04:00
$this -> valueDetails [ $userId ][ $app ][ $key ][ 'flags' ] = $flags ;
2024-10-03 09:52:26 -04:00
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 );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( '' , $app , $key );
2024-10-17 05:48:09 -04:00
foreach ( array_keys ( $this -> getValuesByUsers ( $app , $key )) as $userId ) {
2024-10-03 09:52:26 -04:00
try {
$this -> updateSensitive ( $userId , $app , $key , $sensitive );
} catch ( UnknownKeyException ) {
// should not happen and can be ignored
}
}
2025-01-14 06:38:08 -05:00
// we clear all cache
$this -> clearCacheAll ();
2024-10-03 09:52:26 -04:00
}
2024-10-17 05:48:09 -04:00
/**
* @ 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 );
2024-11-18 16:09:45 -05:00
$this -> loadConfigAll ( $userId );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
2024-10-17 05:48:09 -04:00
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 ])) {
2024-11-18 16:09:45 -05:00
throw new UnknownKeyException ( 'unknown config key' );
2024-10-17 05:48:09 -04:00
}
$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' )
2024-10-21 15:31:14 -04:00
-> 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 )));
2024-10-17 05:48:09 -04:00
$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 );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( '' , $app , $key );
2025-09-16 07:46:45 -04:00
$update = $this -> connection -> getQueryBuilder ();
$update -> update ( 'preferences' )
-> where (
$update -> expr () -> eq ( 'appid' , $update -> createNamedParameter ( $app )),
$update -> expr () -> eq ( 'configkey' , $update -> createNamedParameter ( $key ))
);
2025-09-16 09:52:27 -04:00
// switching flags 'indexed' on and off is about adding/removing the bit value on the correct entries
if ( $indexed ) {
2025-09-17 09:33:04 -04:00
$update -> set ( 'indexed' , $update -> func () -> substring ( 'configvalue' , $update -> createNamedParameter ( 1 , IQueryBuilder :: PARAM_INT ), $update -> createNamedParameter ( 64 , IQueryBuilder :: PARAM_INT )));
2025-09-16 09:52:27 -04:00
$update -> set ( 'flags' , $update -> func () -> add ( 'flags' , $update -> createNamedParameter ( self :: FLAG_INDEXED , IQueryBuilder :: PARAM_INT )));
$update -> andWhere (
$update -> expr () -> neq ( $update -> expr () -> castColumn (
$update -> expr () -> bitwiseAnd ( 'flags' , self :: FLAG_INDEXED ), IQueryBuilder :: PARAM_INT ), $update -> createNamedParameter ( self :: FLAG_INDEXED , IQueryBuilder :: PARAM_INT )
));
} else {
2025-09-17 09:33:04 -04:00
// emptying field 'indexed' if key is not set as indexed anymore
$update -> set ( 'indexed' , $update -> createNamedParameter ( '' ));
2025-09-16 09:52:27 -04:00
$update -> set ( 'flags' , $update -> func () -> subtract ( 'flags' , $update -> createNamedParameter ( self :: FLAG_INDEXED , IQueryBuilder :: PARAM_INT )));
$update -> andWhere (
$update -> expr () -> eq ( $update -> expr () -> castColumn (
$update -> expr () -> bitwiseAnd ( 'flags' , self :: FLAG_INDEXED ), IQueryBuilder :: PARAM_INT ), $update -> createNamedParameter ( self :: FLAG_INDEXED , IQueryBuilder :: PARAM_INT )
));
}
2025-09-16 07:46:45 -04:00
$update -> executeStatement ();
2024-10-17 05:48:09 -04:00
2025-01-14 06:38:08 -05:00
// we clear all cache
$this -> clearCacheAll ();
2024-10-17 05:48:09 -04:00
}
2024-10-03 09:52:26 -04:00
/**
* @ inheritDoc
*
* @ param string $userId id of the user
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
* @ 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 );
2024-11-18 16:09:45 -05:00
$this -> loadConfigAll ( $userId );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
2024-10-03 09:52:26 -04:00
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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
* @ 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 );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( '' , $app , $key );
2024-10-03 09:52:26 -04:00
$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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
*
* @ return array
2024-11-18 16:09:45 -05:00
* @ throws UnknownKeyException if config key is not known in database
2024-10-03 09:52:26 -04:00
* @ since 31.0 . 0
*/
public function getDetails ( string $userId , string $app , string $key ) : array {
$this -> assertParams ( $userId , $app , $key );
2024-11-18 16:09:45 -05:00
$this -> loadConfigAll ( $userId );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
2024-10-03 09:52:26 -04:00
$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 ])) {
2024-11-18 16:09:45 -05:00
throw new UnknownKeyException ( 'unknown config key' );
2024-10-03 09:52:26 -04:00
}
$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
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
*
* @ since 31.0 . 0
*/
2024-11-18 16:09:45 -05:00
public function deleteUserConfig ( string $userId , string $app , string $key ) : void {
2024-10-03 09:52:26 -04:00
$this -> assertParams ( $userId , $app , $key );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( $userId , $app , $key );
2024-10-03 09:52:26 -04:00
$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 ]);
2024-12-04 08:25:49 -05:00
unset ( $this -> valueDetails [ $userId ][ $app ][ $key ]);
2024-10-03 09:52:26 -04:00
}
/**
* @ inheritDoc
*
* @ param string $app id of the app
2024-11-18 16:09:45 -05:00
* @ param string $key config key
2024-10-03 09:52:26 -04:00
*
* @ since 31.0 . 0
*/
public function deleteKey ( string $app , string $key ) : void {
$this -> assertParams ( '' , $app , $key , allowEmptyUser : true );
2025-05-14 14:10:34 -04:00
$this -> matchAndApplyLexiconDefinition ( '' , $app , $key );
2024-10-03 09:52:26 -04:00
$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 );
2025-05-14 14:10:34 -04:00
2024-10-03 09:52:26 -04:00
$qb = $this -> connection -> getQueryBuilder ();
$qb -> delete ( 'preferences' )
-> where ( $qb -> expr () -> eq ( 'appid' , $qb -> createNamedParameter ( $app )));
$qb -> executeStatement ();
$this -> clearCacheAll ();
}
2024-11-18 16:09:45 -05:00
public function deleteAllUserConfig ( string $userId ) : void {
2024-10-03 09:52:26 -04:00
$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 ;
2024-10-17 05:48:09 -04:00
$this -> lazyCache [ $userId ] = $this -> fastCache [ $userId ] = $this -> valueDetails [ $userId ] = [];
2024-10-03 09:52:26 -04:00
if ( ! $reload ) {
return ;
}
2024-11-18 16:09:45 -05:00
$this -> loadConfigAll ( $userId );
2024-10-03 09:52:26 -04:00
}
/**
* @ inheritDoc
*
* @ since 31.0 . 0
*/
public function clearCacheAll () : void {
$this -> lazyLoaded = $this -> fastLoaded = [];
2025-06-11 14:18:44 -04:00
$this -> lazyCache = $this -> fastCache = $this -> valueDetails = $this -> configLexiconDetails = [];
2024-10-03 09:52:26 -04:00
}
/**
* 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 ,
2024-10-17 05:48:09 -04:00
'valueDetails' => $this -> valueDetails ,
2024-10-03 09:52:26 -04:00
];
}
/**
2024-10-17 05:48:09 -04:00
* @ param int $needle bitflag to search
* @ param int $flags all flags
2024-10-03 09:52:26 -04:00
*
2024-10-17 05:48:09 -04:00
* @ return bool TRUE if bitflag $needle is set in $flags
2024-10-03 09:52:26 -04:00
*/
2024-10-17 05:48:09 -04:00
private function isFlagged ( int $needle , int $flags ) : bool {
return (( $needle & $flags ) !== 0 );
2024-10-03 09:52:26 -04:00
}
/**
* Confirm the string set for app and key fit the database description
*
* @ param string $userId
* @ param string $app assert $app fit in database
2024-11-18 16:09:45 -05:00
* @ param string $prefKey assert config key fit in database
2024-10-03 09:52:26 -04:00
* @ param bool $allowEmptyUser
* @ param bool $allowEmptyApp $app can be empty string
* @ param ValueType | null $valueType assert value type is only one type
2026-01-27 09:21:23 -05:00
* @ throws InvalidArgumentException if userId , app , or prefKey is invalid ( too long , or empty string )
2024-10-03 09:52:26 -04:00
*/
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 . ')' );
}
}
2024-11-18 16:09:45 -05:00
private function loadConfigAll ( string $userId ) : void {
$this -> loadConfig ( $userId , null );
2024-10-03 09:52:26 -04:00
}
/**
2024-11-18 16:09:45 -05:00
* Load normal config or config set as lazy loaded
2024-10-03 09:52:26 -04:00
*
2024-11-18 16:09:45 -05:00
* @ param bool | null $lazy set to TRUE to load config set as lazy loaded , set to NULL to load all config
2024-10-03 09:52:26 -04:00
*/
2024-11-18 16:09:45 -05:00
private function loadConfig ( string $userId , ? bool $lazy = false ) : void {
2024-10-03 09:52:26 -04:00
if ( $this -> isLoaded ( $userId , $lazy )) {
return ;
}
if (( $lazy ? ? true ) !== false ) { // if lazy is null or true, we debug log
2024-11-18 16:09:45 -05:00
$this -> logger -> debug ( 'The loading of lazy UserConfig values have been requested' , [ 'exception' => new \RuntimeException ( 'ignorable exception' )]);
2024-10-03 09:52:26 -04:00
}
$qb = $this -> connection -> getQueryBuilder ();
$qb -> from ( 'preferences' );
2024-10-17 05:48:09 -04:00
$qb -> select ( 'appid' , 'configkey' , 'configvalue' , 'type' , 'flags' );
2024-10-03 09:52:26 -04:00
$qb -> where ( $qb -> expr () -> eq ( 'userid' , $qb -> createNamedParameter ( $userId )));
2024-11-18 16:09:45 -05:00
// we only need value from lazy when loadConfig does not specify it
2024-10-03 09:52:26 -04:00
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 ) {
2024-10-17 05:48:09 -04:00
$this -> lazyCache [ $userId ][ $row [ 'appid' ]][ $row [ 'configkey' ]] = $row [ 'configvalue' ] ? ? '' ;
2024-10-03 09:52:26 -04:00
} else {
2024-10-17 05:48:09 -04:00
$this -> fastCache [ $userId ][ $row [ 'appid' ]][ $row [ 'configkey' ]] = $row [ 'configvalue' ] ? ? '' ;
2024-10-03 09:52:26 -04:00
}
2024-10-21 15:31:14 -04:00
$this -> valueDetails [ $userId ][ $row [ 'appid' ]][ $row [ 'configkey' ]] = [ 'type' => ValueType :: from (( int )( $row [ 'type' ] ? ? 0 )), 'flags' => ( int ) $row [ 'flags' ]];
2024-10-03 09:52:26 -04:00
}
$result -> closeCursor ();
$this -> setAsLoaded ( $userId , $lazy );
}
/**
* if $lazy is :
2024-11-18 16:09:45 -05:00
* - 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
2024-10-03 09:52:26 -04:00
*
* @ 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 :
2024-11-18 16:09:45 -05:00
* - false : set fast config as loaded
* - true : set lazy config as loaded
* - null : set both config as loaded
2024-10-03 09:52:26 -04:00
*
* @ 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
2024-11-18 16:09:45 -05:00
* @ param bool $filtered TRUE to hide sensitive config values . Value are replaced by { @ see IConfig :: SENSITIVE_VALUE }
2024-10-03 09:52:26 -04:00
*
* @ 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 ;
}
2024-10-17 05:48:09 -04:00
if ( $this -> isFlagged ( self :: FLAG_SENSITIVE , $this -> valueDetails [ $userId ][ $app ][ $key ][ 'flags' ] ? ? 0 )) {
2024-10-03 09:52:26 -04:00
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 ;
}
2025-01-14 06:38:08 -05:00
/**
* will change referenced $value with the decrypted value in case of encrypted ( sensitive value )
*
* @ param string $userId
* @ param string $app
* @ param string $key
* @ param string $value
*/
2024-10-03 09:52:26 -04:00
private function decryptSensitiveValue ( string $userId , string $app , string $key , string & $value ) : void {
2024-10-17 05:48:09 -04:00
if ( ! $this -> isFlagged ( self :: FLAG_SENSITIVE , $this -> valueDetails [ $userId ][ $app ][ $key ][ 'flags' ] ? ? 0 )) {
2024-10-03 09:52:26 -04:00
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
]);
}
}
2024-12-06 05:35:00 -05:00
/**
2025-05-14 14:10:34 -04:00
* Match and apply current use of config values with defined lexicon .
* Set $lazy to NULL only if only interested into checking that $key is alias .
2024-12-06 05:35:00 -05:00
*
* @ throws UnknownKeyException
* @ throws TypeConflictException
2025-01-14 06:38:08 -05:00
* @ return bool FALSE if conflict with defined lexicon were observed in the process
2024-12-06 05:35:00 -05:00
*/
2024-12-12 13:37:14 -05:00
private function matchAndApplyLexiconDefinition (
2025-01-09 05:42:53 -05:00
string $userId ,
2024-12-06 05:35:00 -05:00
string $app ,
2025-05-14 14:10:34 -04:00
string & $key ,
? bool & $lazy = null ,
ValueType & $type = ValueType :: MIXED ,
2024-12-06 05:35:00 -05:00
int & $flags = 0 ,
2025-06-11 14:18:44 -04:00
? string & $default = null ,
2024-12-06 05:35:00 -05:00
) : bool {
$configDetails = $this -> getConfigDetailsFromLexicon ( $app );
2025-05-14 14:10:34 -04:00
if ( array_key_exists ( $key , $configDetails [ 'aliases' ]) && ! $this -> ignoreLexiconAliases ) {
// in case '$rename' is set in ConfigLexiconEntry, we use the new config key
$key = $configDetails [ 'aliases' ][ $key ];
}
2024-12-06 05:35:00 -05:00
if ( ! array_key_exists ( $key , $configDetails [ 'entries' ])) {
2025-08-29 08:41:08 -04:00
return $this -> applyLexiconStrictness ( $configDetails [ 'strictness' ], $app . '/' . $key );
2024-12-06 05:35:00 -05:00
}
2025-05-14 14:10:34 -04:00
// if lazy is NULL, we ignore all check on the type/lazyness/default from Lexicon
if ( $lazy === null ) {
return true ;
}
2025-07-16 09:46:45 -04:00
/** @var Entry $configValue */
2024-12-06 05:35:00 -05:00
$configValue = $configDetails [ 'entries' ][ $key ];
if ( $type === ValueType :: MIXED ) {
2025-01-09 05:42:53 -05:00
// we overwrite if value was requested as mixed
$type = $configValue -> getValueType ();
2024-12-06 05:35:00 -05:00
} 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 ();
$flags = $configValue -> getFlags ();
if ( $configValue -> isDeprecated ()) {
$this -> logger -> notice ( 'User config key ' . $app . '/' . $key . ' is set as deprecated.' );
}
2025-01-09 05:42:53 -05:00
$enforcedValue = $this -> config -> getSystemValue ( 'lexicon.default.userconfig.enforced' , [])[ $app ][ $key ] ? ? false ;
if ( ! $enforcedValue && $this -> hasKey ( $userId , $app , $key , $lazy )) {
// if key exists there should be no need to extract default
return true ;
}
2025-06-11 14:18:44 -04:00
// only look for default if needed, default from Lexicon got priority if not overwritten by admin
if ( $default !== null ) {
2025-07-29 10:47:23 -04:00
$default = $this -> getSystemDefault ( $app , $configValue ) ? ? $configValue -> getDefault ( $this -> presetManager -> getLexiconPreset ()) ? ? $default ;
2025-06-11 14:18:44 -04:00
}
2025-01-09 05:42:53 -05:00
// returning false will make get() returning $default and set() not changing value in database
return ! $enforcedValue ;
}
/**
* get default value set in config / config . php if stored in key :
*
* 'lexicon.default.userconfig' => [
* < appId > => [
* < configKey > => 'my value' ,
* ]
* ],
*
* The entry is converted to string to fit the expected type when managing default value
*/
2025-07-16 09:46:45 -04:00
private function getSystemDefault ( string $appId , Entry $configValue ) : ? string {
2025-01-09 05:42:53 -05:00
$default = $this -> config -> getSystemValue ( 'lexicon.default.userconfig' , [])[ $appId ][ $configValue -> getKey ()] ? ? null ;
if ( $default === null ) {
2025-01-14 06:38:08 -05:00
// no system default, using default default.
return null ;
2025-01-09 05:42:53 -05:00
}
return $configValue -> convertToString ( $default );
2024-12-06 05:35:00 -05:00
}
/**
* manage ConfigLexicon behavior based on strictness set in IConfigLexicon
*
2025-07-16 09:46:45 -04:00
* @ param Strictness | null $strictness
2024-12-06 05:35:00 -05:00
* @ param string $line
*
* @ return bool TRUE if conflict can be fully ignored
* @ throws UnknownKeyException
2025-08-29 08:41:08 -04:00
* @ see ILexicon :: getStrictness ()
2024-12-06 05:35:00 -05:00
*/
2025-08-29 08:41:08 -04:00
private function applyLexiconStrictness ( ? Strictness $strictness , string $configAppKey ) : bool {
2024-12-06 05:35:00 -05:00
if ( $strictness === null ) {
return true ;
}
2025-08-29 08:41:08 -04:00
$line = 'The user config key ' . $configAppKey . ' is not defined in the config lexicon' ;
2024-12-06 05:35:00 -05:00
switch ( $strictness ) {
2025-07-16 09:46:45 -04:00
case Strictness :: IGNORE :
2024-12-06 05:35:00 -05:00
return true ;
2025-07-16 09:46:45 -04:00
case Strictness :: NOTICE :
2025-08-29 08:41:08 -04:00
if ( ! in_array ( $configAppKey , $this -> strictnessApplied , true )) {
$this -> strictnessApplied [] = $configAppKey ;
$this -> logger -> notice ( $line );
}
2024-12-06 05:35:00 -05:00
return true ;
2025-07-16 09:46:45 -04:00
case Strictness :: WARNING :
2025-08-29 08:41:08 -04:00
if ( ! in_array ( $configAppKey , $this -> strictnessApplied , true )) {
$this -> strictnessApplied [] = $configAppKey ;
$this -> logger -> warning ( $line );
}
2024-12-06 05:35:00 -05:00
return false ;
2025-07-16 09:46:45 -04:00
case Strictness :: EXCEPTION :
2024-12-06 05:35:00 -05:00
throw new UnknownKeyException ( $line );
}
throw new UnknownKeyException ( $line );
}
/**
* extract details from registered $appId ' s config lexicon
*
* @ param string $appId
*
2025-07-16 09:46:45 -04:00
* @ return array { entries : array < string , Entry > , aliases : array < string , string > , strictness : Strictness }
2025-08-19 11:32:37 -04:00
* @ internal
2024-12-06 05:35:00 -05:00
*/
2025-05-14 14:10:34 -04:00
public function getConfigDetailsFromLexicon ( string $appId ) : array {
2024-12-06 05:35:00 -05:00
if ( ! array_key_exists ( $appId , $this -> configLexiconDetails )) {
2025-05-14 14:10:34 -04:00
$entries = $aliases = [];
2024-12-06 05:35:00 -05:00
$bootstrapCoordinator = \OCP\Server :: get ( Coordinator :: class );
$configLexicon = $bootstrapCoordinator -> getRegistrationContext () ? -> getConfigLexicon ( $appId );
foreach ( $configLexicon ? -> getUserConfigs () ? ? [] as $configEntry ) {
$entries [ $configEntry -> getKey ()] = $configEntry ;
2025-05-14 14:10:34 -04:00
if ( $configEntry -> getRename () !== null ) {
$aliases [ $configEntry -> getRename ()] = $configEntry -> getKey ();
}
2024-12-06 05:35:00 -05:00
}
$this -> configLexiconDetails [ $appId ] = [
'entries' => $entries ,
2025-05-14 14:10:34 -04:00
'aliases' => $aliases ,
2025-07-16 09:46:45 -04:00
'strictness' => $configLexicon ? -> getStrictness () ? ? Strictness :: IGNORE
2024-12-06 05:35:00 -05:00
];
}
return $this -> configLexiconDetails [ $appId ];
}
2025-05-14 14:10:34 -04:00
2025-08-19 11:32:37 -04:00
/**
* get Lexicon Entry using appId and config key entry
*
* @ return Entry | null NULL if entry does not exist in user ' s Lexicon
* @ internal
*/
public function getLexiconEntry ( string $appId , string $key ) : ? Entry {
2025-05-14 14:10:34 -04:00
return $this -> getConfigDetailsFromLexicon ( $appId )[ 'entries' ][ $key ] ? ? null ;
}
/**
* if set to TRUE , ignore aliases defined in Config Lexicon during the use of the methods of this class
*
* @ internal
*/
public function ignoreLexiconAliases ( bool $ignore ) : void {
$this -> ignoreLexiconAliases = $ignore ;
}
2024-10-03 09:52:26 -04:00
}