aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/base.php7
-rw-r--r--lib/composer/composer/autoload_classmap.php8
-rw-r--r--lib/composer/composer/autoload_static.php8
-rw-r--r--lib/l10n/lv.js3
-rw-r--r--lib/l10n/lv.json3
-rw-r--r--lib/l10n/pl.js62
-rw-r--r--lib/l10n/pl.json62
-rw-r--r--lib/l10n/sk.js82
-rw-r--r--lib/l10n/sk.json82
-rw-r--r--lib/private/AllConfig.php261
-rw-r--r--lib/private/Config/UserConfig.php1806
-rw-r--r--lib/private/Files/Cache/Cache.php4
-rw-r--r--lib/private/Files/Cache/CacheQueryBuilder.php2
-rw-r--r--lib/private/Files/Storage/Common.php51
-rw-r--r--lib/private/Files/Storage/Local.php25
-rw-r--r--lib/private/Files/Storage/Wrapper/Availability.php253
-rw-r--r--lib/private/Files/Storage/Wrapper/Encryption.php2
-rw-r--r--lib/private/Mail/Mailer.php10
-rw-r--r--lib/private/OCM/OCMDiscoveryService.php10
-rw-r--r--lib/private/Server.php2
-rw-r--r--lib/private/Share20/DefaultShareProvider.php4
-rw-r--r--lib/private/Share20/Manager.php74
-rw-r--r--lib/public/Files/Mount/IShareOwnerlessMount.php18
-rw-r--r--lib/public/Share/IManager.php9
-rw-r--r--lib/unstable/Config/Exceptions/IncorrectTypeException.php18
-rw-r--r--lib/unstable/Config/Exceptions/TypeConflictException.php18
-rw-r--r--lib/unstable/Config/Exceptions/UnknownKeyException.php18
-rw-r--r--lib/unstable/Config/IUserConfig.php694
-rw-r--r--lib/unstable/Config/ValueType.php79
29 files changed, 3202 insertions, 473 deletions
diff --git a/lib/base.php b/lib/base.php
index 53231999e81..b0334ecd729 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -363,6 +363,13 @@ class OC {
public static function initSession(): void {
$request = Server::get(IRequest::class);
+ // Do not initialize sessions for 'status.php' requests
+ // Monitoring endpoints can quickly flood session handlers
+ // and 'status.php' doesn't require sessions anyway
+ if (str_ends_with($request->getScriptName(), '/status.php')) {
+ return;
+ }
+
// TODO: Temporary disabled again to solve issues with CalDAV/CardDAV clients like DAVx5 that use cookies
// TODO: See https://github.com/nextcloud/server/issues/37277#issuecomment-1476366147 and the other comments
// TODO: for further information.
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 13fb25ab303..5834bfd47b5 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -7,6 +7,11 @@ $baseDir = dirname(dirname($vendorDir));
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
+ 'NCU\\Config\\Exceptions\\IncorrectTypeException' => $baseDir . '/lib/unstable/Config/Exceptions/IncorrectTypeException.php',
+ 'NCU\\Config\\Exceptions\\TypeConflictException' => $baseDir . '/lib/unstable/Config/Exceptions/TypeConflictException.php',
+ 'NCU\\Config\\Exceptions\\UnknownKeyException' => $baseDir . '/lib/unstable/Config/Exceptions/UnknownKeyException.php',
+ 'NCU\\Config\\IUserConfig' => $baseDir . '/lib/unstable/Config/IUserConfig.php',
+ 'NCU\\Config\\ValueType' => $baseDir . '/lib/unstable/Config/ValueType.php',
'OCP\\Accounts\\IAccount' => $baseDir . '/lib/public/Accounts/IAccount.php',
'OCP\\Accounts\\IAccountManager' => $baseDir . '/lib/public/Accounts/IAccountManager.php',
'OCP\\Accounts\\IAccountProperty' => $baseDir . '/lib/public/Accounts/IAccountProperty.php',
@@ -404,6 +409,7 @@ return array(
'OCP\\Files\\Mount\\IMountManager' => $baseDir . '/lib/public/Files/Mount/IMountManager.php',
'OCP\\Files\\Mount\\IMountPoint' => $baseDir . '/lib/public/Files/Mount/IMountPoint.php',
'OCP\\Files\\Mount\\IMovableMount' => $baseDir . '/lib/public/Files/Mount/IMovableMount.php',
+ 'OCP\\Files\\Mount\\IShareOwnerlessMount' => $baseDir . '/lib/public/Files/Mount/IShareOwnerlessMount.php',
'OCP\\Files\\Mount\\ISystemMountPoint' => $baseDir . '/lib/public/Files/Mount/ISystemMountPoint.php',
'OCP\\Files\\Node' => $baseDir . '/lib/public/Files/Node.php',
'OCP\\Files\\NotEnoughSpaceException' => $baseDir . '/lib/public/Files/NotEnoughSpaceException.php',
@@ -1118,6 +1124,7 @@ return array(
'OC\\Comments\\Manager' => $baseDir . '/lib/private/Comments/Manager.php',
'OC\\Comments\\ManagerFactory' => $baseDir . '/lib/private/Comments/ManagerFactory.php',
'OC\\Config' => $baseDir . '/lib/private/Config.php',
+ 'OC\\Config\\UserConfig' => $baseDir . '/lib/private/Config/UserConfig.php',
'OC\\Console\\Application' => $baseDir . '/lib/private/Console/Application.php',
'OC\\Console\\TimestampFormatter' => $baseDir . '/lib/private/Console/TimestampFormatter.php',
'OC\\ContactsManager' => $baseDir . '/lib/private/ContactsManager.php',
@@ -1386,6 +1393,7 @@ return array(
'OC\\Core\\Migrations\\Version30000Date20240814180800' => $baseDir . '/core/Migrations/Version30000Date20240814180800.php',
'OC\\Core\\Migrations\\Version30000Date20240815080800' => $baseDir . '/core/Migrations/Version30000Date20240815080800.php',
'OC\\Core\\Migrations\\Version30000Date20240906095113' => $baseDir . '/core/Migrations/Version30000Date20240906095113.php',
+ 'OC\\Core\\Migrations\\Version31000Date20240814184402' => $baseDir . '/core/Migrations/Version31000Date20240814184402.php',
'OC\\Core\\Migrations\\Version31000Date20241018063111' => $baseDir . '/core/Migrations/Version31000Date20241018063111.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 526782636fb..e510e80d4c6 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -48,6 +48,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+ 'NCU\\Config\\Exceptions\\IncorrectTypeException' => __DIR__ . '/../../..' . '/lib/unstable/Config/Exceptions/IncorrectTypeException.php',
+ 'NCU\\Config\\Exceptions\\TypeConflictException' => __DIR__ . '/../../..' . '/lib/unstable/Config/Exceptions/TypeConflictException.php',
+ 'NCU\\Config\\Exceptions\\UnknownKeyException' => __DIR__ . '/../../..' . '/lib/unstable/Config/Exceptions/UnknownKeyException.php',
+ 'NCU\\Config\\IUserConfig' => __DIR__ . '/../../..' . '/lib/unstable/Config/IUserConfig.php',
+ 'NCU\\Config\\ValueType' => __DIR__ . '/../../..' . '/lib/unstable/Config/ValueType.php',
'OCP\\Accounts\\IAccount' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccount.php',
'OCP\\Accounts\\IAccountManager' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccountManager.php',
'OCP\\Accounts\\IAccountProperty' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccountProperty.php',
@@ -445,6 +450,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Files\\Mount\\IMountManager' => __DIR__ . '/../../..' . '/lib/public/Files/Mount/IMountManager.php',
'OCP\\Files\\Mount\\IMountPoint' => __DIR__ . '/../../..' . '/lib/public/Files/Mount/IMountPoint.php',
'OCP\\Files\\Mount\\IMovableMount' => __DIR__ . '/../../..' . '/lib/public/Files/Mount/IMovableMount.php',
+ 'OCP\\Files\\Mount\\IShareOwnerlessMount' => __DIR__ . '/../../..' . '/lib/public/Files/Mount/IShareOwnerlessMount.php',
'OCP\\Files\\Mount\\ISystemMountPoint' => __DIR__ . '/../../..' . '/lib/public/Files/Mount/ISystemMountPoint.php',
'OCP\\Files\\Node' => __DIR__ . '/../../..' . '/lib/public/Files/Node.php',
'OCP\\Files\\NotEnoughSpaceException' => __DIR__ . '/../../..' . '/lib/public/Files/NotEnoughSpaceException.php',
@@ -1159,6 +1165,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Comments\\Manager' => __DIR__ . '/../../..' . '/lib/private/Comments/Manager.php',
'OC\\Comments\\ManagerFactory' => __DIR__ . '/../../..' . '/lib/private/Comments/ManagerFactory.php',
'OC\\Config' => __DIR__ . '/../../..' . '/lib/private/Config.php',
+ 'OC\\Config\\UserConfig' => __DIR__ . '/../../..' . '/lib/private/Config/UserConfig.php',
'OC\\Console\\Application' => __DIR__ . '/../../..' . '/lib/private/Console/Application.php',
'OC\\Console\\TimestampFormatter' => __DIR__ . '/../../..' . '/lib/private/Console/TimestampFormatter.php',
'OC\\ContactsManager' => __DIR__ . '/../../..' . '/lib/private/ContactsManager.php',
@@ -1427,6 +1434,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version30000Date20240814180800' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240814180800.php',
'OC\\Core\\Migrations\\Version30000Date20240815080800' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240815080800.php',
'OC\\Core\\Migrations\\Version30000Date20240906095113' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240906095113.php',
+ 'OC\\Core\\Migrations\\Version31000Date20240814184402' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20240814184402.php',
'OC\\Core\\Migrations\\Version31000Date20241018063111' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20241018063111.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
diff --git a/lib/l10n/lv.js b/lib/l10n/lv.js
index d5d440ee2e0..f94775801ed 100644
--- a/lib/l10n/lv.js
+++ b/lib/l10n/lv.js
@@ -57,6 +57,7 @@ OC.L10N.register(
"Profile picture" : "Profila attēls",
"About" : "Par",
"Display name" : "Attēlojamais vārds",
+ "Role" : "Loma",
"Additional settings" : "Papildu iestatījumi",
"Oracle connection could not be established" : "Nevar izveidot savienojumu ar Oracle",
"Set an admin password." : "Iestatīt pārvaldītāja paroli.",
@@ -114,7 +115,7 @@ OC.L10N.register(
"a safe home for all your data" : "droša vieta visiem Taviem datiem",
"Application is not enabled" : "Lietotne nav iespējota",
"Authentication error" : "Autentifikācijas kļūda",
- "Token expired. Please reload page." : "Pilnvarai ir beidzies termiņš. Lūdzu, pārlādējiet lapu.",
+ "Token expired. Please reload page." : "Pilnvarai ir beidzies derīgums. Lūgums pārlādēt lapu.",
"PHP module %s not installed." : "PHP modulis %s nav instalēts.",
"Please ask your server administrator to install the module." : "Lūgums vaicāt savam servera pārvaldītāja, lai uzstāda moduli.",
"This is probably caused by a cache/accelerator such as Zend OPcache or eAccelerator." : "Tas ir iespējams, kešatmiņa / paātrinātājs, piemēram, Zend OPcache vai eAccelerator.",
diff --git a/lib/l10n/lv.json b/lib/l10n/lv.json
index 715869b5c3e..915fd7edd2d 100644
--- a/lib/l10n/lv.json
+++ b/lib/l10n/lv.json
@@ -55,6 +55,7 @@
"Profile picture" : "Profila attēls",
"About" : "Par",
"Display name" : "Attēlojamais vārds",
+ "Role" : "Loma",
"Additional settings" : "Papildu iestatījumi",
"Oracle connection could not be established" : "Nevar izveidot savienojumu ar Oracle",
"Set an admin password." : "Iestatīt pārvaldītāja paroli.",
@@ -112,7 +113,7 @@
"a safe home for all your data" : "droša vieta visiem Taviem datiem",
"Application is not enabled" : "Lietotne nav iespējota",
"Authentication error" : "Autentifikācijas kļūda",
- "Token expired. Please reload page." : "Pilnvarai ir beidzies termiņš. Lūdzu, pārlādējiet lapu.",
+ "Token expired. Please reload page." : "Pilnvarai ir beidzies derīgums. Lūgums pārlādēt lapu.",
"PHP module %s not installed." : "PHP modulis %s nav instalēts.",
"Please ask your server administrator to install the module." : "Lūgums vaicāt savam servera pārvaldītāja, lai uzstāda moduli.",
"This is probably caused by a cache/accelerator such as Zend OPcache or eAccelerator." : "Tas ir iespējams, kešatmiņa / paātrinātājs, piemēram, Zend OPcache vai eAccelerator.",
diff --git a/lib/l10n/pl.js b/lib/l10n/pl.js
index c5ce675bc40..fddcd73d1f7 100644
--- a/lib/l10n/pl.js
+++ b/lib/l10n/pl.js
@@ -19,9 +19,11 @@ OC.L10N.register(
"%1$s, %2$s and %3$s" : "%1$s, %2$s i %3$s",
"%1$s, %2$s, %3$s and %4$s" : "%1$s, %2$s, %3$s i %4$s",
"%1$s, %2$s, %3$s, %4$s and %5$s" : "%1$s, %2$s, %3$s, %4$s i %5$s",
+ "Education bundle" : "Pakiet edukacyjny",
"Enterprise bundle" : "Pakiet dla firm",
"Groupware bundle" : "Pakiet do pracy grupowej",
"Hub bundle" : "Pakiet centralny",
+ "Public sector bundle" : "Pakiet sektora publicznego",
"Social sharing bundle" : "Pakiet udostępniania społecznościowego",
"PHP %s or higher is required." : "Wymagane jest PHP %s lub wyższe.",
"PHP with a version lower than %s is required." : "Wymagane jest PHP z wersją niższą niż %s.",
@@ -36,6 +38,7 @@ OC.L10N.register(
"Server version %s or higher is required." : "Wymagana jest wersja serwera %s lub wyższa.",
"Server version %s or lower is required." : "Wymagana jest wersja serwera %s lub niższa.",
"Logged in account must be an admin, a sub admin or gotten special right to access this setting" : "Zalogowane konto musi być administratorem, subadministratorem lub posiadać specjalne uprawnienia, aby uzyskać dostęp do tego ustawienia",
+ "Your current IP address doesn’t allow you to perform admin actions" : "Twój obecny adres IP nie pozwala na wykonywanie czynności administracyjnych",
"Logged in account must be an admin or sub admin" : "Zalogowane konto musi być administratorem lub subadministratorem",
"Logged in account must be an admin" : "Zalogowane konto musi być administratorem",
"Wiping of device %s has started" : "Rozpoczęto czyszczenie urządzenia %s",
@@ -79,11 +82,15 @@ OC.L10N.register(
"Empty file" : "Pusty plik",
"Module with ID: %s does not exist. Please enable it in your apps settings or contact your administrator." : "Moduł o ID: %s nie istnieje. Włącz go w ustawieniach aplikacji lub skontaktuj się z administratorem.",
"Dot files are not allowed" : "Pliki z kropką są nie dozwolone",
+ "Invalid parent path" : "Nieprawidłowa ścieżka nadrzędna",
"File already exists" : "Plik już istnieje",
"Invalid path" : "Niewłaściwa ścieżka",
"Failed to create file from template" : "Nie udało się utworzyć pliku z szablonu",
"Templates" : "Szablony",
+ "Path contains invalid segments" : "Ścieżka zawiera nieprawidłowe segmenty",
+ "Filename is a reserved word" : "Nazwa pliku jest słowem zastrzeżonym",
"Filename contains at least one invalid character" : "Nazwa pliku zawiera co najmniej jeden nieprawidłowy znak",
+ "Filename is too long" : "Nazwa pliku jest za długa",
"Empty filename is not allowed" : "Pusta nazwa pliku jest niedozwolona",
"App \"%s\" cannot be installed because appinfo file cannot be read." : "Aplikacji \"%s\" nie można zainstalować, ponieważ nie można odczytać pliku appinfo.",
"App \"%s\" cannot be installed because it is not compatible with this version of the server." : "Aplikacji \"%s\" nie można zainstalować, ponieważ nie jest kompatybilna z tą wersją serwera.",
@@ -117,6 +124,8 @@ OC.L10N.register(
"Pronouns" : "Zaimki",
"Unknown account" : "Nieznane konto",
"Additional settings" : "Ustawienia dodatkowe",
+ "Enter the database Login and name for %s" : "Wprowadź login i nazwę bazy danych dla %s",
+ "Enter the database Login for %s" : "Wpisz logowanie do bazy danych dla %s",
"Enter the database name for %s" : "Podaj nazwę bazy danych dla %s",
"You cannot use dots in the database name %s" : "Nie można używać kropek w nazwie bazy danych %s",
"You need to enter details of an existing account." : "Musisz wprowadzić szczegółowe dane dla istniejącego konta.",
@@ -125,6 +134,7 @@ OC.L10N.register(
"For the best results, please consider using a GNU/Linux server instead." : "Aby uzyskać najlepszy efekt, rozważ użycie serwera GNU/Linux.",
"It seems that this %s instance is running on a 32-bit PHP environment and the open_basedir has been configured in php.ini. This will lead to problems with files over 4 GB and is highly discouraged." : "Wydaje się, że ta instancja %s używa PHP dla 32-bitowego środowiska, ponieważ open_basedir został tak skonfigurowany w php.ini. Doprowadzi to do problemów z plikami powyżej 4 GB i jest bardzo niezalecane.",
"Please remove the open_basedir setting within your php.ini or switch to 64-bit PHP." : "Usuń ustawienie open_basedir w php.ini lub przełącz na PHP 64-bitowe.",
+ "Set an admin Login." : "Ustaw login administratora.",
"Set an admin password." : "Ustaw hasło administratora.",
"Cannot create or write into the data directory %s" : "Nie można tworzyć ani zapisywać w katalogu %s",
"Sharing backend %s must implement the interface OCP\\Share_Backend" : "Zaplecze do udostępniania %s musi implementować interfejs OCP\\Share_Backend",
@@ -138,18 +148,46 @@ OC.L10N.register(
"%1$s shared »%2$s« with you and wants to add:" : "%1$s udostępnił Tobie »%2$s« i chce dodać: ",
"%1$s shared »%2$s« with you and wants to add" : " %1$s udostępnił Tobie »%2$s« i chce dodać",
"»%s« added a note to a file shared with you" : "»%s« dodał notatkę do udostępnionego dla Ciebie pliku",
+ "Passwords are enforced for link and mail shares" : "Hasła są wymuszane w przypadku udostępniania łączy i poczty",
+ "Share recipient is not a valid user" : "Odbiorca udostępnienia nie jest prawidłowym użytkownikiem",
+ "Share recipient is not a valid group" : "Odbiorca udostępnienia nie jest prawidłową grupą",
+ "Share recipient should be empty" : "Odbiorca udostępnienia powinien być pusty",
+ "Share recipient should not be empty" : "Odbiorca udostępnienia nie powinien być pusty",
+ "Share recipient is not a valid circle" : "Odbiorca udostępnienia nie jest prawidłowym kręgiem",
"Unknown share type" : "Nieznany typ udostępnienia",
+ "Cannot share with yourself" : "Nie można dzielić się ze sobą",
+ "Shared path must be set" : "Należy ustawić ścieżkę współdzieloną",
+ "Shared path must be either a file or a folder" : "Udostępniona ścieżka musi być plikiem lub katalogiem",
+ "You cannot share your root folder" : "Nie możesz udostępnić swojego katalogu głównego root",
"You are not allowed to share %s" : "Nie możesz udostępnić %s",
+ "Valid permissions are required for sharing" : "Do udostępniania wymagane są ważne uprawnienia",
"Cannot increase permissions of %s" : "Nie można zwiększyć uprawnień %s",
+ "Shares need at least read permissions" : "Udostępnianie wymaga co najmniej uprawnień do odczytu",
"Files cannot be shared with delete permissions" : "Pliki nie mogą zostać udostępnione z prawem do usuwania",
"Files cannot be shared with create permissions" : "Pliki nie mogą zostać udostępnione z prawem do tworzenia",
"Expiration date is in the past" : "Data ważności już minęła",
+ "Expiration date is enforced" : "Obowiązuje data ważności",
"_Cannot set expiration date more than %n day in the future_::_Cannot set expiration date more than %n days in the future_" : ["Nie można utworzyć daty wygaśnięcia na %n dzień do przodu","Nie można utworzyć daty wygaśnięcia na %n dni do przodu","Nie można utworzyć daty wygaśnięcia na %n dni do przodu","Nie można utworzyć daty wygaśnięcia na %n dni do przodu"],
"Sharing is only allowed with group members" : "Udostępnianie jest dozwolone tylko członkom grupy",
+ "Group sharing is now allowed" : "Udostępnianie grupowe jest teraz dozwolone",
+ "Sharing is only allowed within your own groups" : "Udostępnianie jest dozwolone wyłącznie w obrębie własnych grup",
+ "Path is already shared with this group" : "Ścieżka jest już udostępniona tej grupie",
+ "Link sharing is not allowed" : "Udostępnianie odnośników jest niedozwolone",
+ "Public upload is not allowed" : "Przesyłanie publiczne nie jest dozwolone",
+ "Path contains files shared with you" : "Ścieżka zawiera pliki udostępnione Tobie",
+ "Sharing is disabled" : "Udostępnianie jest wyłączone",
+ "Sharing is disabled for you" : "Udostępnianie jest dla Ciebie wyłączone",
+ "Cannot share with the share owner" : "Nie można udostępnić właścicielowi udziału",
+ "Share does not have a full ID" : "Udział nie ma pełnego identyfikatora",
+ "Cannot change share type" : "Nie można zmienić typu udziału",
+ "Can only update recipient on user shares" : "Może aktualizować odbiorców tylko w przypadku udziałów użytkownika",
+ "Invalid share recipient" : "Nieprawidłowy odbiorca udostępnienia",
"The requested share does not exist anymore" : "Żądane udostępnienie już nie istnieje",
"The requested share comes from a disabled user" : "Żądane udostępnienie pochodzi od wyłączonego użytkownika",
"The user was not created because the user limit has been reached. Check your notifications to learn more." : "Użytkownik nie został utworzony, ponieważ osiągnięto limit użytkowników. Sprawdź swoje powiadomienia, aby dowiedzieć się więcej.",
"Could not find category \"%s\"" : "Nie można znaleźć kategorii \"%s\"",
+ "Input text" : "Wprowadź tekst",
+ "The input text" : "Wprowadzony tekst",
"Sunday" : "Niedziela",
"Monday" : "Poniedziałek",
"Tuesday" : "Wtorek",
@@ -198,6 +236,7 @@ OC.L10N.register(
"A valid password must be provided" : "Należy podać prawidłowe hasło",
"The Login is already being used" : "Login jest już używany",
"Could not create account" : "Nie można utworzyć konta",
+ "Only the following characters are allowed in an Login: \"a-z\", \"A-Z\", \"0-9\", spaces and \"_.@-'\"" : "W loginie dozwolone są tylko następujące znaki: „a-z”, „A-Z”, „0-9”, spacje i „_.@-'”",
"A valid Login must be provided" : "Należy podać poprawny login",
"Login contains whitespace at the beginning or at the end" : "Login zawiera spacje na początku lub na końcu",
"Login must not consist of dots only" : "Login nie może składać się wyłącznie z kropek",
@@ -249,14 +288,37 @@ OC.L10N.register(
"Storage connection error. %s" : "Błąd połączenia z magazynem. %s",
"Storage is temporarily not available" : "Magazyn jest tymczasowo niedostępny",
"Storage connection timeout. %s" : "Limit czasu połączenia do magazynu. %s",
+ "Transcribe audio" : "Transkrypcja dźwięku",
+ "Transcribe the things said in an audio" : "Transkrypcja wypowiedzi w formie audio",
+ "Audio input" : "Wejście dźwięku",
+ "The audio to transcribe" : "Dźwięk do transkrypcji",
+ "Transcription" : "Transkrypcja",
+ "The transcribed text" : "Tekst transkrypcji",
+ "Writing style" : "Styl pisania",
+ "Source material" : "Materiał źródłowy",
+ "Describe the image you want to generate" : "Opisz obraz, który chcesz wygenerować",
+ "Number of images" : "Liczba obrazów",
+ "How many images to generate" : "Ile obrazów wygenerować",
+ "Output images" : "Obrazy wyjściowe",
+ "The generated images" : "Wygenerowane obrazy",
"Chat" : "Rozmowa",
+ "Chat history" : "Historia rozmów",
"Generates a possible headline for a text." : "Generuje możliwy nagłówek tekstu.",
"Summarize" : "Podsumuj",
"Summary" : "Podsumowanie",
"Extract topics" : "Wyodrębnij tematy",
+ "Topics" : "Tematy",
+ "The list of extracted topics" : "Lista wyodrębnionych tematów",
"Translate" : "Tłumaczenie",
+ "Translate text from one language to another" : "Tłumaczenie tekstu z jednego języka na inny",
+ "Origin text" : "Tekst źródłowy",
+ "The text to translate" : "Tekst do przetłumaczenia",
+ "Origin language" : "Język źródłowy",
+ "The language of the origin text" : "Język tekstu źródłowego",
"Target language" : "Język docelowy",
+ "The desired language to translate the origin text in" : "Żądany język, na który ma zostać przetłumaczony tekst źródłowy",
"Result" : "Wynik",
+ "The translated text" : "Przetłumaczony tekst",
"Free prompt" : "Monit bezpłatny",
"Runs an arbitrary prompt through the language model." : "Uruchamia dowolny monit w modelu języka.",
"Generate headline" : "Wygeneruj nagłówek",
diff --git a/lib/l10n/pl.json b/lib/l10n/pl.json
index 3e58a0e22b0..79fce096b87 100644
--- a/lib/l10n/pl.json
+++ b/lib/l10n/pl.json
@@ -17,9 +17,11 @@
"%1$s, %2$s and %3$s" : "%1$s, %2$s i %3$s",
"%1$s, %2$s, %3$s and %4$s" : "%1$s, %2$s, %3$s i %4$s",
"%1$s, %2$s, %3$s, %4$s and %5$s" : "%1$s, %2$s, %3$s, %4$s i %5$s",
+ "Education bundle" : "Pakiet edukacyjny",
"Enterprise bundle" : "Pakiet dla firm",
"Groupware bundle" : "Pakiet do pracy grupowej",
"Hub bundle" : "Pakiet centralny",
+ "Public sector bundle" : "Pakiet sektora publicznego",
"Social sharing bundle" : "Pakiet udostępniania społecznościowego",
"PHP %s or higher is required." : "Wymagane jest PHP %s lub wyższe.",
"PHP with a version lower than %s is required." : "Wymagane jest PHP z wersją niższą niż %s.",
@@ -34,6 +36,7 @@
"Server version %s or higher is required." : "Wymagana jest wersja serwera %s lub wyższa.",
"Server version %s or lower is required." : "Wymagana jest wersja serwera %s lub niższa.",
"Logged in account must be an admin, a sub admin or gotten special right to access this setting" : "Zalogowane konto musi być administratorem, subadministratorem lub posiadać specjalne uprawnienia, aby uzyskać dostęp do tego ustawienia",
+ "Your current IP address doesn’t allow you to perform admin actions" : "Twój obecny adres IP nie pozwala na wykonywanie czynności administracyjnych",
"Logged in account must be an admin or sub admin" : "Zalogowane konto musi być administratorem lub subadministratorem",
"Logged in account must be an admin" : "Zalogowane konto musi być administratorem",
"Wiping of device %s has started" : "Rozpoczęto czyszczenie urządzenia %s",
@@ -77,11 +80,15 @@
"Empty file" : "Pusty plik",
"Module with ID: %s does not exist. Please enable it in your apps settings or contact your administrator." : "Moduł o ID: %s nie istnieje. Włącz go w ustawieniach aplikacji lub skontaktuj się z administratorem.",
"Dot files are not allowed" : "Pliki z kropką są nie dozwolone",
+ "Invalid parent path" : "Nieprawidłowa ścieżka nadrzędna",
"File already exists" : "Plik już istnieje",
"Invalid path" : "Niewłaściwa ścieżka",
"Failed to create file from template" : "Nie udało się utworzyć pliku z szablonu",
"Templates" : "Szablony",
+ "Path contains invalid segments" : "Ścieżka zawiera nieprawidłowe segmenty",
+ "Filename is a reserved word" : "Nazwa pliku jest słowem zastrzeżonym",
"Filename contains at least one invalid character" : "Nazwa pliku zawiera co najmniej jeden nieprawidłowy znak",
+ "Filename is too long" : "Nazwa pliku jest za długa",
"Empty filename is not allowed" : "Pusta nazwa pliku jest niedozwolona",
"App \"%s\" cannot be installed because appinfo file cannot be read." : "Aplikacji \"%s\" nie można zainstalować, ponieważ nie można odczytać pliku appinfo.",
"App \"%s\" cannot be installed because it is not compatible with this version of the server." : "Aplikacji \"%s\" nie można zainstalować, ponieważ nie jest kompatybilna z tą wersją serwera.",
@@ -115,6 +122,8 @@
"Pronouns" : "Zaimki",
"Unknown account" : "Nieznane konto",
"Additional settings" : "Ustawienia dodatkowe",
+ "Enter the database Login and name for %s" : "Wprowadź login i nazwę bazy danych dla %s",
+ "Enter the database Login for %s" : "Wpisz logowanie do bazy danych dla %s",
"Enter the database name for %s" : "Podaj nazwę bazy danych dla %s",
"You cannot use dots in the database name %s" : "Nie można używać kropek w nazwie bazy danych %s",
"You need to enter details of an existing account." : "Musisz wprowadzić szczegółowe dane dla istniejącego konta.",
@@ -123,6 +132,7 @@
"For the best results, please consider using a GNU/Linux server instead." : "Aby uzyskać najlepszy efekt, rozważ użycie serwera GNU/Linux.",
"It seems that this %s instance is running on a 32-bit PHP environment and the open_basedir has been configured in php.ini. This will lead to problems with files over 4 GB and is highly discouraged." : "Wydaje się, że ta instancja %s używa PHP dla 32-bitowego środowiska, ponieważ open_basedir został tak skonfigurowany w php.ini. Doprowadzi to do problemów z plikami powyżej 4 GB i jest bardzo niezalecane.",
"Please remove the open_basedir setting within your php.ini or switch to 64-bit PHP." : "Usuń ustawienie open_basedir w php.ini lub przełącz na PHP 64-bitowe.",
+ "Set an admin Login." : "Ustaw login administratora.",
"Set an admin password." : "Ustaw hasło administratora.",
"Cannot create or write into the data directory %s" : "Nie można tworzyć ani zapisywać w katalogu %s",
"Sharing backend %s must implement the interface OCP\\Share_Backend" : "Zaplecze do udostępniania %s musi implementować interfejs OCP\\Share_Backend",
@@ -136,18 +146,46 @@
"%1$s shared »%2$s« with you and wants to add:" : "%1$s udostępnił Tobie »%2$s« i chce dodać: ",
"%1$s shared »%2$s« with you and wants to add" : " %1$s udostępnił Tobie »%2$s« i chce dodać",
"»%s« added a note to a file shared with you" : "»%s« dodał notatkę do udostępnionego dla Ciebie pliku",
+ "Passwords are enforced for link and mail shares" : "Hasła są wymuszane w przypadku udostępniania łączy i poczty",
+ "Share recipient is not a valid user" : "Odbiorca udostępnienia nie jest prawidłowym użytkownikiem",
+ "Share recipient is not a valid group" : "Odbiorca udostępnienia nie jest prawidłową grupą",
+ "Share recipient should be empty" : "Odbiorca udostępnienia powinien być pusty",
+ "Share recipient should not be empty" : "Odbiorca udostępnienia nie powinien być pusty",
+ "Share recipient is not a valid circle" : "Odbiorca udostępnienia nie jest prawidłowym kręgiem",
"Unknown share type" : "Nieznany typ udostępnienia",
+ "Cannot share with yourself" : "Nie można dzielić się ze sobą",
+ "Shared path must be set" : "Należy ustawić ścieżkę współdzieloną",
+ "Shared path must be either a file or a folder" : "Udostępniona ścieżka musi być plikiem lub katalogiem",
+ "You cannot share your root folder" : "Nie możesz udostępnić swojego katalogu głównego root",
"You are not allowed to share %s" : "Nie możesz udostępnić %s",
+ "Valid permissions are required for sharing" : "Do udostępniania wymagane są ważne uprawnienia",
"Cannot increase permissions of %s" : "Nie można zwiększyć uprawnień %s",
+ "Shares need at least read permissions" : "Udostępnianie wymaga co najmniej uprawnień do odczytu",
"Files cannot be shared with delete permissions" : "Pliki nie mogą zostać udostępnione z prawem do usuwania",
"Files cannot be shared with create permissions" : "Pliki nie mogą zostać udostępnione z prawem do tworzenia",
"Expiration date is in the past" : "Data ważności już minęła",
+ "Expiration date is enforced" : "Obowiązuje data ważności",
"_Cannot set expiration date more than %n day in the future_::_Cannot set expiration date more than %n days in the future_" : ["Nie można utworzyć daty wygaśnięcia na %n dzień do przodu","Nie można utworzyć daty wygaśnięcia na %n dni do przodu","Nie można utworzyć daty wygaśnięcia na %n dni do przodu","Nie można utworzyć daty wygaśnięcia na %n dni do przodu"],
"Sharing is only allowed with group members" : "Udostępnianie jest dozwolone tylko członkom grupy",
+ "Group sharing is now allowed" : "Udostępnianie grupowe jest teraz dozwolone",
+ "Sharing is only allowed within your own groups" : "Udostępnianie jest dozwolone wyłącznie w obrębie własnych grup",
+ "Path is already shared with this group" : "Ścieżka jest już udostępniona tej grupie",
+ "Link sharing is not allowed" : "Udostępnianie odnośników jest niedozwolone",
+ "Public upload is not allowed" : "Przesyłanie publiczne nie jest dozwolone",
+ "Path contains files shared with you" : "Ścieżka zawiera pliki udostępnione Tobie",
+ "Sharing is disabled" : "Udostępnianie jest wyłączone",
+ "Sharing is disabled for you" : "Udostępnianie jest dla Ciebie wyłączone",
+ "Cannot share with the share owner" : "Nie można udostępnić właścicielowi udziału",
+ "Share does not have a full ID" : "Udział nie ma pełnego identyfikatora",
+ "Cannot change share type" : "Nie można zmienić typu udziału",
+ "Can only update recipient on user shares" : "Może aktualizować odbiorców tylko w przypadku udziałów użytkownika",
+ "Invalid share recipient" : "Nieprawidłowy odbiorca udostępnienia",
"The requested share does not exist anymore" : "Żądane udostępnienie już nie istnieje",
"The requested share comes from a disabled user" : "Żądane udostępnienie pochodzi od wyłączonego użytkownika",
"The user was not created because the user limit has been reached. Check your notifications to learn more." : "Użytkownik nie został utworzony, ponieważ osiągnięto limit użytkowników. Sprawdź swoje powiadomienia, aby dowiedzieć się więcej.",
"Could not find category \"%s\"" : "Nie można znaleźć kategorii \"%s\"",
+ "Input text" : "Wprowadź tekst",
+ "The input text" : "Wprowadzony tekst",
"Sunday" : "Niedziela",
"Monday" : "Poniedziałek",
"Tuesday" : "Wtorek",
@@ -196,6 +234,7 @@
"A valid password must be provided" : "Należy podać prawidłowe hasło",
"The Login is already being used" : "Login jest już używany",
"Could not create account" : "Nie można utworzyć konta",
+ "Only the following characters are allowed in an Login: \"a-z\", \"A-Z\", \"0-9\", spaces and \"_.@-'\"" : "W loginie dozwolone są tylko następujące znaki: „a-z”, „A-Z”, „0-9”, spacje i „_.@-'”",
"A valid Login must be provided" : "Należy podać poprawny login",
"Login contains whitespace at the beginning or at the end" : "Login zawiera spacje na początku lub na końcu",
"Login must not consist of dots only" : "Login nie może składać się wyłącznie z kropek",
@@ -247,14 +286,37 @@
"Storage connection error. %s" : "Błąd połączenia z magazynem. %s",
"Storage is temporarily not available" : "Magazyn jest tymczasowo niedostępny",
"Storage connection timeout. %s" : "Limit czasu połączenia do magazynu. %s",
+ "Transcribe audio" : "Transkrypcja dźwięku",
+ "Transcribe the things said in an audio" : "Transkrypcja wypowiedzi w formie audio",
+ "Audio input" : "Wejście dźwięku",
+ "The audio to transcribe" : "Dźwięk do transkrypcji",
+ "Transcription" : "Transkrypcja",
+ "The transcribed text" : "Tekst transkrypcji",
+ "Writing style" : "Styl pisania",
+ "Source material" : "Materiał źródłowy",
+ "Describe the image you want to generate" : "Opisz obraz, który chcesz wygenerować",
+ "Number of images" : "Liczba obrazów",
+ "How many images to generate" : "Ile obrazów wygenerować",
+ "Output images" : "Obrazy wyjściowe",
+ "The generated images" : "Wygenerowane obrazy",
"Chat" : "Rozmowa",
+ "Chat history" : "Historia rozmów",
"Generates a possible headline for a text." : "Generuje możliwy nagłówek tekstu.",
"Summarize" : "Podsumuj",
"Summary" : "Podsumowanie",
"Extract topics" : "Wyodrębnij tematy",
+ "Topics" : "Tematy",
+ "The list of extracted topics" : "Lista wyodrębnionych tematów",
"Translate" : "Tłumaczenie",
+ "Translate text from one language to another" : "Tłumaczenie tekstu z jednego języka na inny",
+ "Origin text" : "Tekst źródłowy",
+ "The text to translate" : "Tekst do przetłumaczenia",
+ "Origin language" : "Język źródłowy",
+ "The language of the origin text" : "Język tekstu źródłowego",
"Target language" : "Język docelowy",
+ "The desired language to translate the origin text in" : "Żądany język, na który ma zostać przetłumaczony tekst źródłowy",
"Result" : "Wynik",
+ "The translated text" : "Przetłumaczony tekst",
"Free prompt" : "Monit bezpłatny",
"Runs an arbitrary prompt through the language model." : "Uruchamia dowolny monit w modelu języka.",
"Generate headline" : "Wygeneruj nagłówek",
diff --git a/lib/l10n/sk.js b/lib/l10n/sk.js
index 41527f6d870..c0d956626fe 100644
--- a/lib/l10n/sk.js
+++ b/lib/l10n/sk.js
@@ -38,6 +38,7 @@ OC.L10N.register(
"Server version %s or higher is required." : "Je vyžadovaná verzia servera %s alebo vyššia.",
"Server version %s or lower is required." : "Je vyžadovaná verzia servera %s alebo nižšia.",
"Logged in account must be an admin, a sub admin or gotten special right to access this setting" : "Prihlásený účet musí byť správcom, podadministrátorom alebo musí mať špeciálne právo na prístup k tomuto nastaveniu.",
+ "Your current IP address doesn’t allow you to perform admin actions" : "Vaša aktuálna adresa IP vám neumožňuje vykonávať akcie správcu",
"Logged in account must be an admin or sub admin" : "Prihlásený účet musí byť správcom alebo sub správcom.",
"Logged in account must be an admin" : "Prihlásený účet musí byť správcom",
"Wiping of device %s has started" : "Začalo sa mazanie zariadenia %s",
@@ -148,15 +149,32 @@ OC.L10N.register(
"%1$s shared »%2$s« with you and wants to add:" : "%1$s vám sprístupnil »%2$s« s poznámkou:",
"%1$s shared »%2$s« with you and wants to add" : "%1$s vám sprístupnil »%2$s« s poznámkou",
"»%s« added a note to a file shared with you" : "»%s« pridal poznámku k súboru ktorý s Vami zdieľa",
+ "Share recipient is not a valid user" : "Príjemca zdieľania nie je platným uživateľom",
+ "Share recipient is not a valid group" : "Príjemca zdieľania nie je platnou skupinou",
+ "Share recipient should be empty" : "Príjemca zdieľania musí byť prázdny",
+ "Share recipient should not be empty" : "Príjemca zdieľania nemôže byť prázdny",
+ "Share recipient is not a valid circle" : "Príjemca zdieľania nie je platný kruh",
"Unknown share type" : "Neplatný typ sprístupnenia",
+ "Share initiator must be set" : "Musí byť nastavený iniciátor zdieľania",
+ "Cannot share with yourself" : "Nie je možné zdieľanie so sebou samým",
+ "You cannot share your root folder" : "Nemôžete zdieľať váš koreňový adresár",
"You are not allowed to share %s" : "Nemôžete sprístupniť %s",
+ "Valid permissions are required for sharing" : "Pre zdieľanie sú potrebné korektné práva",
"Cannot increase permissions of %s" : "Nie je možné navýšiť oprávnenia pre %s",
+ "Shares need at least read permissions" : "Zdieľanie vyžaduje minimálne práva na čítanie",
"Files cannot be shared with delete permissions" : "Súbory nie je možné sprístupňovať s oprávneniami na odstránenie",
"Files cannot be shared with create permissions" : "Súbory nie je možné sprístupňovať s oprávneniami na vytváranie",
"Expiration date is in the past" : "Dátum konca platnosti je v minulosti",
"_Cannot set expiration date more than %n day in the future_::_Cannot set expiration date more than %n days in the future_" : ["Nie je možné nastaviť dátum konca platnosti viac ako %s deň v budúcnosti","Nie je možné nastaviť dátum konca platnosti viac ako %s dní v budúcnosti","Nie je možné nastaviť dátum konca platnosti viac ako %s dní v budúcnosti","Nie je možné nastaviť dátum konca platnosti viac ako %s dní v budúcnosti"],
"Sharing is only allowed with group members" : "Zdieľanie je možné iba s členmi skupiny",
"Sharing %s failed, because this item is already shared with the account %s" : "Zdieľanie %s zlyhalo, pretože táto položka už je užívateľovi %s zozdieľaná.",
+ "Sharing is disabled" : "Zdieľanie je zakázané",
+ "Sharing is disabled for you" : "Zdieľanie je pre vás zakázané",
+ "Cannot share with the share owner" : "Nie je možné zdieľať s vlastníkom",
+ "Share does not have a full ID" : "Zdieľanie nemá plné ID",
+ "Cannot change share type" : "Nie je možné zmeniť typ zdieľania",
+ "Invalid share recipient" : "Neplatný príjemca zdieľania",
+ "Group \"%s\" does not exist" : "Skupina \"%s\" neexistuje",
"The requested share does not exist anymore" : "Požadované zdieľanie už neexistuje",
"The requested share comes from a disabled user" : "Požadované zdieľanie pochádza od zakázaného užívateľa.",
"The user was not created because the user limit has been reached. Check your notifications to learn more." : "Bol dosiahnutý limit používateľov a používateľ nebol vytvorený. Pozrite sa do upozornení pre viac informácií.",
@@ -264,14 +282,78 @@ OC.L10N.register(
"Storage is temporarily not available" : "Úložisko je dočasne nedostupné",
"Storage connection timeout. %s" : "Vypršanie pripojenia k úložisku. %s",
"To allow this check to run you have to make sure that your Web server can connect to itself. Therefore it must be able to resolve and connect to at least one of its `trusted_domains` or the `overwrite.cli.url`. This failure may be the result of a server-side DNS mismatch or outbound firewall rule." : "Ak chcete povoliť spustenie tejto kontroly, musíte sa uistiť, že váš webový server sa môže pripojiť sám k sebe. Preto musí byť schopný rozpoznať a pripojiť sa aspoň k jednej zo svojich `trusted_domains` alebo `overwrite.cli.url`. Výsledkom nesprávneho nastavenia môže byť nesúlad DNS na strane servera alebo pravidla brány firewall pre výstup.",
+ "Transcribe audio" : "Prepísať zvuk",
+ "Transcribe the things said in an audio" : "Prepísať veci povedané vo zvuku",
"Audio input" : "Audio vstup",
+ "The audio to transcribe" : "Zvuk pre prepísanie",
+ "Transcription" : "Prepis",
+ "The transcribed text" : "Prepísaný text",
+ "Context write" : "Kontextový zápis",
+ "Writes text in a given style based on the provided source material." : "Napíše text v danom štýle na základe poskytnutého zdrojového materiálu.",
+ "Writing style" : "Štýl písania",
+ "Demonstrate a writing style that you would like to immitate" : "Ukážte štýl písania, ktorý by ste chceli napodobniť",
+ "Source material" : "Zdrojový materiál",
+ "The content that would like to be rewritten in the new writing style" : "Obsah, ktorý by bol prepísaný do nového štýlu písania",
+ "Generated text" : "Vygenerovaný text",
+ "The generated text with content from the source material in the given style" : "Vygenerovaný text s obsahom zo zdrojového materiálu v danom štýle",
+ "Emoji generator" : "Generátor emotikonov",
+ "Takes text and generates a representative emoji for it." : "Zoberie text a vygeneruje pre neho reprezentatívny emotikon.",
+ "The text to generate an emoji for" : "Text pre ktorý vygeneruje emotikon",
+ "Generated emoji" : "Vygenerované emotikony",
+ "The generated emoji based on the input text" : "Vygenerovaný emotikon založený na vstupnom texte",
+ "Generate image" : "Vygenerovať obrázok",
+ "Generate an image from a text prompt" : "Vygenerovať obrázok na základe textového zadania",
+ "Prompt" : "Zadanie",
+ "Describe the image you want to generate" : "Popíšte obrázok ktorý chcete vygenerovať",
+ "Number of images" : "Počet obrázkov",
+ "How many images to generate" : "Koľko obrázkov sa bude generovať",
+ "Output images" : "Výstupné obrázky",
+ "The generated images" : "Vygenerované obrázky",
+ "Generated reply" : "Vygenerovaná odpoveď",
+ "Chat" : "Chat",
+ "Chat with the assistant" : "Chat s asistentom",
+ "System prompt" : "Systémová výzva",
+ "Chat message" : "Správa chatu",
+ "Chat history" : "História chatu",
+ "Response message" : "Správa s odpoveďou",
+ "Formalize text" : "Formalizovať text",
+ "Formalized text" : "Formalizovaný text",
+ "The formalized text" : "Formalizovaný text",
+ "Generate a headline" : "Vygenerovať titulok",
"Generates a possible headline for a text." : "Generuje možný nadpis pre text.",
+ "Original text" : "Originálny text",
+ "The original text to generate a headline for" : "Originálny text pre ktorý sa vygeneruje titulok",
+ "The generated headline" : "Vygenerovaný titulok",
+ "Reformulate text" : "Preformulovať text",
+ "Takes a text and reformulates it" : "Zoberie text a preformuluje ho",
+ "Write a text that you want the assistant to reformulate" : "Napíšte text pre ktorý chcete aby asistent preforumuloval",
+ "Reformulated text" : "Preformulovaný text",
+ "The reformulated text, written by the assistant" : "Preformulovaný text, ktorý napísal asistent",
+ "Simplify text" : "Zjednodušiť text",
+ "Takes a text and simplifies it" : "Zobrať text a zjednodušiť ho",
+ "Write a text that you want the assistant to simplify" : "Napíšte text ktorý chcete od asistenta zjednodušiť",
+ "Simplified text" : "Zjednodušený text",
+ "The simplified text" : "Zjednodušený text",
"Summarize" : "Zhrnutie",
+ "Summarizes a text" : "Zhrnutie textu",
+ "The original text to summarize" : "Originálny text pre zhrnutie",
"Summary" : "Súhrn",
+ "The generated summary" : "Vygenerované zhrnutie",
"Extract topics" : "Extrahovať témy",
+ "Extracts topics from a text and outputs them separated by commas" : "Vyextrahovať témy z textu a vypísať ich ako zoznam oddelený čiarkami",
+ "The original text to extract topics from" : "Originálny text z ktorého sa vyextrahujú témy",
+ "Topics" : "Témy",
+ "The list of extracted topics" : "Zoznam vyextrahovaných tém",
"Translate" : "Preložiť",
+ "Translate text from one language to another" : "Preložiť text z jedného jazyka do druhého",
+ "Origin text" : "Pôvodný text",
+ "The text to translate" : "Text pre preklad",
+ "Origin language" : "Pôvodný jazyk",
+ "The language of the origin text" : "Jazyk pôvodného textu",
"Target language" : "Cieľový jazyk",
+ "The desired language to translate the origin text in" : "Požadovaný jazyk, do ktorého sa má preložiť pôvodný text",
"Result" : "Výsledok",
+ "The translated text" : "Preložený text",
"Free prompt" : "Bezplatný formulár",
"Runs an arbitrary prompt through the language model." : "Spúšťa ľubovoľný príkaz cez jazykový model.",
"Generate headline" : "Generovať nadpis",
diff --git a/lib/l10n/sk.json b/lib/l10n/sk.json
index b6368712d42..54f1c3c7c98 100644
--- a/lib/l10n/sk.json
+++ b/lib/l10n/sk.json
@@ -36,6 +36,7 @@
"Server version %s or higher is required." : "Je vyžadovaná verzia servera %s alebo vyššia.",
"Server version %s or lower is required." : "Je vyžadovaná verzia servera %s alebo nižšia.",
"Logged in account must be an admin, a sub admin or gotten special right to access this setting" : "Prihlásený účet musí byť správcom, podadministrátorom alebo musí mať špeciálne právo na prístup k tomuto nastaveniu.",
+ "Your current IP address doesn’t allow you to perform admin actions" : "Vaša aktuálna adresa IP vám neumožňuje vykonávať akcie správcu",
"Logged in account must be an admin or sub admin" : "Prihlásený účet musí byť správcom alebo sub správcom.",
"Logged in account must be an admin" : "Prihlásený účet musí byť správcom",
"Wiping of device %s has started" : "Začalo sa mazanie zariadenia %s",
@@ -146,15 +147,32 @@
"%1$s shared »%2$s« with you and wants to add:" : "%1$s vám sprístupnil »%2$s« s poznámkou:",
"%1$s shared »%2$s« with you and wants to add" : "%1$s vám sprístupnil »%2$s« s poznámkou",
"»%s« added a note to a file shared with you" : "»%s« pridal poznámku k súboru ktorý s Vami zdieľa",
+ "Share recipient is not a valid user" : "Príjemca zdieľania nie je platným uživateľom",
+ "Share recipient is not a valid group" : "Príjemca zdieľania nie je platnou skupinou",
+ "Share recipient should be empty" : "Príjemca zdieľania musí byť prázdny",
+ "Share recipient should not be empty" : "Príjemca zdieľania nemôže byť prázdny",
+ "Share recipient is not a valid circle" : "Príjemca zdieľania nie je platný kruh",
"Unknown share type" : "Neplatný typ sprístupnenia",
+ "Share initiator must be set" : "Musí byť nastavený iniciátor zdieľania",
+ "Cannot share with yourself" : "Nie je možné zdieľanie so sebou samým",
+ "You cannot share your root folder" : "Nemôžete zdieľať váš koreňový adresár",
"You are not allowed to share %s" : "Nemôžete sprístupniť %s",
+ "Valid permissions are required for sharing" : "Pre zdieľanie sú potrebné korektné práva",
"Cannot increase permissions of %s" : "Nie je možné navýšiť oprávnenia pre %s",
+ "Shares need at least read permissions" : "Zdieľanie vyžaduje minimálne práva na čítanie",
"Files cannot be shared with delete permissions" : "Súbory nie je možné sprístupňovať s oprávneniami na odstránenie",
"Files cannot be shared with create permissions" : "Súbory nie je možné sprístupňovať s oprávneniami na vytváranie",
"Expiration date is in the past" : "Dátum konca platnosti je v minulosti",
"_Cannot set expiration date more than %n day in the future_::_Cannot set expiration date more than %n days in the future_" : ["Nie je možné nastaviť dátum konca platnosti viac ako %s deň v budúcnosti","Nie je možné nastaviť dátum konca platnosti viac ako %s dní v budúcnosti","Nie je možné nastaviť dátum konca platnosti viac ako %s dní v budúcnosti","Nie je možné nastaviť dátum konca platnosti viac ako %s dní v budúcnosti"],
"Sharing is only allowed with group members" : "Zdieľanie je možné iba s členmi skupiny",
"Sharing %s failed, because this item is already shared with the account %s" : "Zdieľanie %s zlyhalo, pretože táto položka už je užívateľovi %s zozdieľaná.",
+ "Sharing is disabled" : "Zdieľanie je zakázané",
+ "Sharing is disabled for you" : "Zdieľanie je pre vás zakázané",
+ "Cannot share with the share owner" : "Nie je možné zdieľať s vlastníkom",
+ "Share does not have a full ID" : "Zdieľanie nemá plné ID",
+ "Cannot change share type" : "Nie je možné zmeniť typ zdieľania",
+ "Invalid share recipient" : "Neplatný príjemca zdieľania",
+ "Group \"%s\" does not exist" : "Skupina \"%s\" neexistuje",
"The requested share does not exist anymore" : "Požadované zdieľanie už neexistuje",
"The requested share comes from a disabled user" : "Požadované zdieľanie pochádza od zakázaného užívateľa.",
"The user was not created because the user limit has been reached. Check your notifications to learn more." : "Bol dosiahnutý limit používateľov a používateľ nebol vytvorený. Pozrite sa do upozornení pre viac informácií.",
@@ -262,14 +280,78 @@
"Storage is temporarily not available" : "Úložisko je dočasne nedostupné",
"Storage connection timeout. %s" : "Vypršanie pripojenia k úložisku. %s",
"To allow this check to run you have to make sure that your Web server can connect to itself. Therefore it must be able to resolve and connect to at least one of its `trusted_domains` or the `overwrite.cli.url`. This failure may be the result of a server-side DNS mismatch or outbound firewall rule." : "Ak chcete povoliť spustenie tejto kontroly, musíte sa uistiť, že váš webový server sa môže pripojiť sám k sebe. Preto musí byť schopný rozpoznať a pripojiť sa aspoň k jednej zo svojich `trusted_domains` alebo `overwrite.cli.url`. Výsledkom nesprávneho nastavenia môže byť nesúlad DNS na strane servera alebo pravidla brány firewall pre výstup.",
+ "Transcribe audio" : "Prepísať zvuk",
+ "Transcribe the things said in an audio" : "Prepísať veci povedané vo zvuku",
"Audio input" : "Audio vstup",
+ "The audio to transcribe" : "Zvuk pre prepísanie",
+ "Transcription" : "Prepis",
+ "The transcribed text" : "Prepísaný text",
+ "Context write" : "Kontextový zápis",
+ "Writes text in a given style based on the provided source material." : "Napíše text v danom štýle na základe poskytnutého zdrojového materiálu.",
+ "Writing style" : "Štýl písania",
+ "Demonstrate a writing style that you would like to immitate" : "Ukážte štýl písania, ktorý by ste chceli napodobniť",
+ "Source material" : "Zdrojový materiál",
+ "The content that would like to be rewritten in the new writing style" : "Obsah, ktorý by bol prepísaný do nového štýlu písania",
+ "Generated text" : "Vygenerovaný text",
+ "The generated text with content from the source material in the given style" : "Vygenerovaný text s obsahom zo zdrojového materiálu v danom štýle",
+ "Emoji generator" : "Generátor emotikonov",
+ "Takes text and generates a representative emoji for it." : "Zoberie text a vygeneruje pre neho reprezentatívny emotikon.",
+ "The text to generate an emoji for" : "Text pre ktorý vygeneruje emotikon",
+ "Generated emoji" : "Vygenerované emotikony",
+ "The generated emoji based on the input text" : "Vygenerovaný emotikon založený na vstupnom texte",
+ "Generate image" : "Vygenerovať obrázok",
+ "Generate an image from a text prompt" : "Vygenerovať obrázok na základe textového zadania",
+ "Prompt" : "Zadanie",
+ "Describe the image you want to generate" : "Popíšte obrázok ktorý chcete vygenerovať",
+ "Number of images" : "Počet obrázkov",
+ "How many images to generate" : "Koľko obrázkov sa bude generovať",
+ "Output images" : "Výstupné obrázky",
+ "The generated images" : "Vygenerované obrázky",
+ "Generated reply" : "Vygenerovaná odpoveď",
+ "Chat" : "Chat",
+ "Chat with the assistant" : "Chat s asistentom",
+ "System prompt" : "Systémová výzva",
+ "Chat message" : "Správa chatu",
+ "Chat history" : "História chatu",
+ "Response message" : "Správa s odpoveďou",
+ "Formalize text" : "Formalizovať text",
+ "Formalized text" : "Formalizovaný text",
+ "The formalized text" : "Formalizovaný text",
+ "Generate a headline" : "Vygenerovať titulok",
"Generates a possible headline for a text." : "Generuje možný nadpis pre text.",
+ "Original text" : "Originálny text",
+ "The original text to generate a headline for" : "Originálny text pre ktorý sa vygeneruje titulok",
+ "The generated headline" : "Vygenerovaný titulok",
+ "Reformulate text" : "Preformulovať text",
+ "Takes a text and reformulates it" : "Zoberie text a preformuluje ho",
+ "Write a text that you want the assistant to reformulate" : "Napíšte text pre ktorý chcete aby asistent preforumuloval",
+ "Reformulated text" : "Preformulovaný text",
+ "The reformulated text, written by the assistant" : "Preformulovaný text, ktorý napísal asistent",
+ "Simplify text" : "Zjednodušiť text",
+ "Takes a text and simplifies it" : "Zobrať text a zjednodušiť ho",
+ "Write a text that you want the assistant to simplify" : "Napíšte text ktorý chcete od asistenta zjednodušiť",
+ "Simplified text" : "Zjednodušený text",
+ "The simplified text" : "Zjednodušený text",
"Summarize" : "Zhrnutie",
+ "Summarizes a text" : "Zhrnutie textu",
+ "The original text to summarize" : "Originálny text pre zhrnutie",
"Summary" : "Súhrn",
+ "The generated summary" : "Vygenerované zhrnutie",
"Extract topics" : "Extrahovať témy",
+ "Extracts topics from a text and outputs them separated by commas" : "Vyextrahovať témy z textu a vypísať ich ako zoznam oddelený čiarkami",
+ "The original text to extract topics from" : "Originálny text z ktorého sa vyextrahujú témy",
+ "Topics" : "Témy",
+ "The list of extracted topics" : "Zoznam vyextrahovaných tém",
"Translate" : "Preložiť",
+ "Translate text from one language to another" : "Preložiť text z jedného jazyka do druhého",
+ "Origin text" : "Pôvodný text",
+ "The text to translate" : "Text pre preklad",
+ "Origin language" : "Pôvodný jazyk",
+ "The language of the origin text" : "Jazyk pôvodného textu",
"Target language" : "Cieľový jazyk",
+ "The desired language to translate the origin text in" : "Požadovaný jazyk, do ktorého sa má preložiť pôvodný text",
"Result" : "Výsledok",
+ "The translated text" : "Preložený text",
"Free prompt" : "Bezplatný formulár",
"Runs an arbitrary prompt through the language model." : "Spúšťa ľubovoľný príkaz cez jazykový model.",
"Generate headline" : "Generovať nadpis",
diff --git a/lib/private/AllConfig.php b/lib/private/AllConfig.php
index 46b53e3c1b2..72af6c960a5 100644
--- a/lib/private/AllConfig.php
+++ b/lib/private/AllConfig.php
@@ -6,8 +6,11 @@
*/
namespace OC;
+use NCU\Config\Exceptions\TypeConflictException;
+use NCU\Config\IUserConfig;
+use NCU\Config\ValueType;
+use OC\Config\UserConfig;
use OCP\Cache\CappedMemoryCache;
-use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\PreConditionNotMetException;
@@ -224,64 +227,33 @@ class AllConfig implements IConfig {
* @param string $key the key under which the value is being stored
* @param string|float|int $value the value that you want to store
* @param string $preCondition only update if the config value was previously the value passed as $preCondition
+ *
* @throws \OCP\PreConditionNotMetException if a precondition is specified and is not met
* @throws \UnexpectedValueException when trying to store an unexpected value
+ * @deprecated 31.0.0 - use {@see IUserConfig} directly
+ * @see IUserConfig::getValueString
+ * @see IUserConfig::getValueInt
+ * @see IUserConfig::getValueFloat
+ * @see IUserConfig::getValueArray
+ * @see IUserConfig::getValueBool
*/
public function setUserValue($userId, $appName, $key, $value, $preCondition = null) {
if (!is_int($value) && !is_float($value) && !is_string($value)) {
throw new \UnexpectedValueException('Only integers, floats and strings are allowed as value');
}
- // TODO - FIXME
- $this->fixDIInit();
-
- if ($appName === 'settings' && $key === 'email') {
- $value = strtolower((string)$value);
- }
-
- $prevValue = $this->getUserValue($userId, $appName, $key, null);
-
- if ($prevValue !== null) {
- if ($preCondition !== null && $prevValue !== (string)$preCondition) {
- throw new PreConditionNotMetException();
- } elseif ($prevValue === (string)$value) {
- return;
- } else {
- $qb = $this->connection->getQueryBuilder();
- $qb->update('preferences')
- ->set('configvalue', $qb->createNamedParameter($value))
- ->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId)))
- ->andWhere($qb->expr()->eq('appid', $qb->createNamedParameter($appName)))
- ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
- $qb->executeStatement();
-
- $this->userCache[$userId][$appName][$key] = (string)$value;
- return;
+ /** @var UserConfig $userPreferences */
+ $userPreferences = \OCP\Server::get(IUserConfig::class);
+ if ($preCondition !== null) {
+ try {
+ if ($userPreferences->hasKey($userId, $appName, $key) && $userPreferences->getValueMixed($userId, $appName, $key) !== (string)$preCondition) {
+ throw new PreConditionNotMetException();
+ }
+ } catch (TypeConflictException) {
}
}
- $preconditionArray = [];
- if (isset($preCondition)) {
- $preconditionArray = [
- 'configvalue' => $preCondition,
- ];
- }
-
- $this->connection->setValues('preferences', [
- 'userid' => $userId,
- 'appid' => $appName,
- 'configkey' => $key,
- ], [
- 'configvalue' => $value,
- ], $preconditionArray);
-
- // only add to the cache if we already loaded data for the user
- if (isset($this->userCache[$userId])) {
- if (!isset($this->userCache[$userId][$appName])) {
- $this->userCache[$userId][$appName] = [];
- }
- $this->userCache[$userId][$appName][$key] = (string)$value;
- }
+ $userPreferences->setValueMixed($userId, $appName, $key, (string)$value);
}
/**
@@ -291,15 +263,26 @@ class AllConfig implements IConfig {
* @param string $appName the appName that we stored the value under
* @param string $key the key under which the value is being stored
* @param mixed $default the default value to be returned if the value isn't set
+ *
* @return string
+ * @deprecated 31.0.0 - use {@see IUserConfig} directly
+ * @see IUserConfig::getValueString
+ * @see IUserConfig::getValueInt
+ * @see IUserConfig::getValueFloat
+ * @see IUserConfig::getValueArray
+ * @see IUserConfig::getValueBool
*/
public function getUserValue($userId, $appName, $key, $default = '') {
- $data = $this->getAllUserValues($userId);
- if (isset($data[$appName][$key])) {
- return $data[$appName][$key];
- } else {
+ if ($userId === null || $userId === '') {
+ return $default;
+ }
+ /** @var UserConfig $userPreferences */
+ $userPreferences = \OCP\Server::get(IUserConfig::class);
+ // because $default can be null ...
+ if (!$userPreferences->hasKey($userId, $appName, $key)) {
return $default;
}
+ return $userPreferences->getValueMixed($userId, $appName, $key, $default ?? '');
}
/**
@@ -307,15 +290,12 @@ class AllConfig implements IConfig {
*
* @param string $userId the userId of the user that we want to store the value under
* @param string $appName the appName that we stored the value under
+ *
* @return string[]
+ * @deprecated 31.0.0 - use {@see IUserConfig::getKeys} directly
*/
public function getUserKeys($userId, $appName) {
- $data = $this->getAllUserValues($userId);
- if (isset($data[$appName])) {
- return array_map('strval', array_keys($data[$appName]));
- } else {
- return [];
- }
+ return \OCP\Server::get(IUserConfig::class)->getKeys($userId, $appName);
}
/**
@@ -324,96 +304,63 @@ class AllConfig implements IConfig {
* @param string $userId the userId of the user that we want to store the value under
* @param string $appName the appName that we stored the value under
* @param string $key the key under which the value is being stored
+ *
+ * @deprecated 31.0.0 - use {@see IUserConfig::deleteUserConfig} directly
*/
public function deleteUserValue($userId, $appName, $key) {
- // TODO - FIXME
- $this->fixDIInit();
-
- $qb = $this->connection->getQueryBuilder();
- $qb->delete('preferences')
- ->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
- ->andWhere($qb->expr()->eq('appid', $qb->createNamedParameter($appName, IQueryBuilder::PARAM_STR)))
- ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key, IQueryBuilder::PARAM_STR)))
- ->executeStatement();
-
- if (isset($this->userCache[$userId][$appName])) {
- unset($this->userCache[$userId][$appName][$key]);
- }
+ \OCP\Server::get(IUserConfig::class)->deleteUserConfig($userId, $appName, $key);
}
/**
* Delete all user values
*
* @param string $userId the userId of the user that we want to remove all values from
+ *
+ * @deprecated 31.0.0 - use {@see IUserConfig::deleteAllUserConfig} directly
*/
public function deleteAllUserValues($userId) {
- // TODO - FIXME
- $this->fixDIInit();
- $qb = $this->connection->getQueryBuilder();
- $qb->delete('preferences')
- ->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
- ->executeStatement();
-
- unset($this->userCache[$userId]);
+ if ($userId === null) {
+ return;
+ }
+ \OCP\Server::get(IUserConfig::class)->deleteAllUserConfig($userId);
}
/**
* Delete all user related values of one app
*
* @param string $appName the appName of the app that we want to remove all values from
+ *
+ * @deprecated 31.0.0 - use {@see IUserConfig::deleteApp} directly
*/
public function deleteAppFromAllUsers($appName) {
- // TODO - FIXME
- $this->fixDIInit();
-
- $qb = $this->connection->getQueryBuilder();
- $qb->delete('preferences')
- ->where($qb->expr()->eq('appid', $qb->createNamedParameter($appName, IQueryBuilder::PARAM_STR)))
- ->executeStatement();
-
- foreach ($this->userCache as &$userCache) {
- unset($userCache[$appName]);
- }
+ \OCP\Server::get(IUserConfig::class)->deleteApp($appName);
}
/**
* Returns all user configs sorted by app of one user
*
* @param ?string $userId the user ID to get the app configs from
+ *
* @psalm-return array<string, array<string, string>>
* @return array[] - 2 dimensional array with the following structure:
* [ $appId =>
* [ $key => $value ]
* ]
+ * @deprecated 31.0.0 - use {@see IUserConfig::getAllValues} directly
*/
public function getAllUserValues(?string $userId): array {
- if (isset($this->userCache[$userId])) {
- return $this->userCache[$userId];
- }
if ($userId === null || $userId === '') {
- $this->userCache[''] = [];
- return $this->userCache[''];
+ return [];
}
- // TODO - FIXME
- $this->fixDIInit();
-
- $data = [];
-
- $qb = $this->connection->getQueryBuilder();
- $result = $qb->select('appid', 'configkey', 'configvalue')
- ->from('preferences')
- ->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
- ->executeQuery();
- while ($row = $result->fetch()) {
- $appId = $row['appid'];
- if (!isset($data[$appId])) {
- $data[$appId] = [];
+ $values = \OCP\Server::get(IUserConfig::class)->getAllValues($userId);
+ $result = [];
+ foreach ($values as $app => $list) {
+ foreach ($list as $key => $value) {
+ $result[$app][$key] = (string)$value;
}
- $data[$appId][$row['configkey']] = $row['configvalue'];
}
- $this->userCache[$userId] = $data;
- return $data;
+ return $result;
}
/**
@@ -422,38 +369,12 @@ class AllConfig implements IConfig {
* @param string $appName app to get the value for
* @param string $key the key to get the value for
* @param array $userIds the user IDs to fetch the values for
+ *
* @return array Mapped values: userId => value
+ * @deprecated 31.0.0 - use {@see IUserConfig::getValuesByUsers} directly
*/
public function getUserValueForUsers($appName, $key, $userIds) {
- // TODO - FIXME
- $this->fixDIInit();
-
- if (empty($userIds) || !is_array($userIds)) {
- return [];
- }
-
- $chunkedUsers = array_chunk($userIds, 50, true);
-
- $qb = $this->connection->getQueryBuilder();
- $qb->select('userid', 'configvalue')
- ->from('preferences')
- ->where($qb->expr()->eq('appid', $qb->createParameter('appName')))
- ->andWhere($qb->expr()->eq('configkey', $qb->createParameter('configKey')))
- ->andWhere($qb->expr()->in('userid', $qb->createParameter('userIds')));
-
- $userValues = [];
- foreach ($chunkedUsers as $chunk) {
- $qb->setParameter('appName', $appName, IQueryBuilder::PARAM_STR);
- $qb->setParameter('configKey', $key, IQueryBuilder::PARAM_STR);
- $qb->setParameter('userIds', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
- $result = $qb->executeQuery();
-
- while ($row = $result->fetch()) {
- $userValues[$row['userid']] = $row['configvalue'];
- }
- }
-
- return $userValues;
+ return \OCP\Server::get(IUserConfig::class)->getValuesByUsers($appName, $key, ValueType::MIXED, $userIds);
}
/**
@@ -462,32 +383,14 @@ class AllConfig implements IConfig {
* @param string $appName the app to get the user for
* @param string $key the key to get the user for
* @param string $value the value to get the user for
+ *
* @return list<string> of user IDs
+ * @deprecated 31.0.0 - use {@see IUserConfig::searchUsersByValueString} directly
*/
public function getUsersForUserValue($appName, $key, $value) {
- // TODO - FIXME
- $this->fixDIInit();
-
- $qb = $this->connection->getQueryBuilder();
- $configValueColumn = ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE)
- ? $qb->expr()->castColumn('configvalue', IQueryBuilder::PARAM_STR)
- : 'configvalue';
- $result = $qb->select('userid')
- ->from('preferences')
- ->where($qb->expr()->eq('appid', $qb->createNamedParameter($appName, IQueryBuilder::PARAM_STR)))
- ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key, IQueryBuilder::PARAM_STR)))
- ->andWhere($qb->expr()->eq(
- $configValueColumn,
- $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR))
- )->orderBy('userid')
- ->executeQuery();
-
- $userIDs = [];
- while ($row = $result->fetch()) {
- $userIDs[] = $row['userid'];
- }
-
- return $userIDs;
+ /** @var list<string> $result */
+ $result = iterator_to_array(\OCP\Server::get(IUserConfig::class)->searchUsersByValueString($appName, $key, $value));
+ return $result;
}
/**
@@ -496,38 +399,18 @@ class AllConfig implements IConfig {
* @param string $appName the app to get the user for
* @param string $key the key to get the user for
* @param string $value the value to get the user for
+ *
* @return list<string> of user IDs
+ * @deprecated 31.0.0 - use {@see IUserConfig::searchUsersByValueString} directly
*/
public function getUsersForUserValueCaseInsensitive($appName, $key, $value) {
- // TODO - FIXME
- $this->fixDIInit();
-
if ($appName === 'settings' && $key === 'email') {
- // Email address is always stored lowercase in the database
return $this->getUsersForUserValue($appName, $key, strtolower($value));
}
- $qb = $this->connection->getQueryBuilder();
- $configValueColumn = ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE)
- ? $qb->expr()->castColumn('configvalue', IQueryBuilder::PARAM_STR)
- : 'configvalue';
-
- $result = $qb->select('userid')
- ->from('preferences')
- ->where($qb->expr()->eq('appid', $qb->createNamedParameter($appName, IQueryBuilder::PARAM_STR)))
- ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key, IQueryBuilder::PARAM_STR)))
- ->andWhere($qb->expr()->eq(
- $qb->func()->lower($configValueColumn),
- $qb->createNamedParameter(strtolower($value), IQueryBuilder::PARAM_STR))
- )->orderBy('userid')
- ->executeQuery();
-
- $userIDs = [];
- while ($row = $result->fetch()) {
- $userIDs[] = $row['userid'];
- }
-
- return $userIDs;
+ /** @var list<string> $result */
+ $result = iterator_to_array(\OCP\Server::get(IUserConfig::class)->searchUsersByValueString($appName, $key, $value, true));
+ return $result;
}
public function getSystemConfig() {
diff --git a/lib/private/Config/UserConfig.php b/lib/private/Config/UserConfig.php
new file mode 100644
index 00000000000..37e109b2121
--- /dev/null
+++ b/lib/private/Config/UserConfig.php
@@ -0,0 +1,1806 @@
+<?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\ValueType;
+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 = [];
+
+ 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);
+ $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);
+ $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 |= UserConfig::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
+ ]);
+ }
+ }
+}
diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php
index 59d5a838d72..da798af46cd 100644
--- a/lib/private/Files/Cache/Cache.php
+++ b/lib/private/Files/Cache/Cache.php
@@ -249,14 +249,14 @@ class Cache implements ICache {
$file = $this->normalize($file);
if (isset($this->partial[$file])) { //add any saved partial data
- $data = array_merge($this->partial[$file], $data);
+ $data = array_merge($this->partial[$file]->getData(), $data);
unset($this->partial[$file]);
}
$requiredFields = ['size', 'mtime', 'mimetype'];
foreach ($requiredFields as $field) {
if (!isset($data[$field])) { //data not complete save as partial and return
- $this->partial[$file] = $data;
+ $this->partial[$file] = new CacheEntry($data);
return -1;
}
}
diff --git a/lib/private/Files/Cache/CacheQueryBuilder.php b/lib/private/Files/Cache/CacheQueryBuilder.php
index 2853ca033ad..5ae60ee80b6 100644
--- a/lib/private/Files/Cache/CacheQueryBuilder.php
+++ b/lib/private/Files/Cache/CacheQueryBuilder.php
@@ -48,7 +48,7 @@ class CacheQueryBuilder extends ExtendedQueryBuilder {
public function selectFileCache(?string $alias = null, bool $joinExtendedCache = true) {
$name = $alias ?: 'filecache';
$this->select("$name.fileid", 'storage', 'path', 'path_hash', "$name.parent", "$name.name", 'mimetype', 'mimepart', 'size', 'mtime',
- 'storage_mtime', 'encrypted', 'etag', "$name.permissions", 'checksum', 'unencrypted_size')
+ 'storage_mtime', 'encrypted', "$name.etag", "$name.permissions", 'checksum', 'unencrypted_size')
->from('filecache', $name);
if ($joinExtendedCache) {
diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php
index 334ca34294e..036ccad23a5 100644
--- a/lib/private/Files/Storage/Common.php
+++ b/lib/private/Files/Storage/Common.php
@@ -32,6 +32,7 @@ use OCP\Files\Storage\ILockingStorage;
use OCP\Files\Storage\IStorage;
use OCP\Files\Storage\IWriteStreamStorage;
use OCP\Files\StorageNotAvailableException;
+use OCP\IConfig;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use OCP\Server;
@@ -69,13 +70,14 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
}
protected function remove(string $path): bool {
- if ($this->is_dir($path)) {
- return $this->rmdir($path);
- } elseif ($this->is_file($path)) {
- return $this->unlink($path);
- } else {
- return false;
+ if ($this->file_exists($path)) {
+ if ($this->is_dir($path)) {
+ return $this->rmdir($path);
+ } elseif ($this->is_file($path)) {
+ return $this->unlink($path);
+ }
}
+ return false;
}
public function is_dir(string $path): bool {
@@ -91,11 +93,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
return 0; //by definition
} else {
$stat = $this->stat($path);
- if (isset($stat['size'])) {
- return $stat['size'];
- } else {
- return 0;
- }
+ return isset($stat['size']) ? $stat['size'] : 0;
}
}
@@ -208,7 +206,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
$targetStream = $this->fopen($target, 'w');
[, $result] = \OC_Helper::streamCopy($sourceStream, $targetStream);
if (!$result) {
- \OCP\Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target");
+ Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target");
}
$this->removeCachedFile($target);
return $result;
@@ -227,6 +225,9 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
public function hash(string $type, string $path, bool $raw = false): string|false {
$fh = $this->fopen($path, 'rb');
+ if (!$fh) {
+ return false;
+ }
$ctx = hash_init($type);
hash_update_stream($ctx, $fh);
fclose($fh);
@@ -241,7 +242,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
$dh = $this->opendir($path);
if (is_resource($dh)) {
while (($file = readdir($dh)) !== false) {
- if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
+ if (!Filesystem::isIgnoredDir($file)) {
if ($this->is_dir($path . '/' . $file)) {
mkdir($target . '/' . $file);
$this->addLocalFolder($path . '/' . $file, $target . '/' . $file);
@@ -259,7 +260,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
$dh = $this->opendir($dir);
if (is_resource($dh)) {
while (($item = readdir($dh)) !== false) {
- if (\OC\Files\Filesystem::isIgnoredDir($item)) {
+ if (Filesystem::isIgnoredDir($item)) {
continue;
}
if (strstr(strtolower($item), strtolower($query)) !== false) {
@@ -325,7 +326,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
}
if (!isset($this->watcher)) {
$this->watcher = new Watcher($storage);
- $globalPolicy = \OC::$server->getConfig()->getSystemValue('filesystem_check_changes', Watcher::CHECK_NEVER);
+ $globalPolicy = Server::get(IConfig::class)->getSystemValueInt('filesystem_check_changes', Watcher::CHECK_NEVER);
$this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy));
}
return $this->watcher;
@@ -340,8 +341,8 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
}
/** @var self $storage */
if (!isset($storage->propagator)) {
- $config = \OC::$server->getSystemConfig();
- $storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getValue('instanceid')]);
+ $config = Server::get(IConfig::class);
+ $storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getSystemValueString('instanceid')]);
}
return $storage->propagator;
}
@@ -386,7 +387,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
* @return string cleaned path
*/
public function cleanPath(string $path): string {
- if (strlen($path) == 0 or $path[0] != '/') {
+ if (strlen($path) == 0 || $path[0] != '/') {
$path = '/' . $path;
}
@@ -410,10 +411,10 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
if ($this->stat('')) {
return true;
}
- \OC::$server->get(LoggerInterface::class)->info('External storage not available: stat() failed');
+ Server::get(LoggerInterface::class)->info('External storage not available: stat() failed');
return false;
} catch (\Exception $e) {
- \OC::$server->get(LoggerInterface::class)->warning(
+ Server::get(LoggerInterface::class)->warning(
'External storage not available: ' . $e->getMessage(),
['exception' => $e]
);
@@ -475,7 +476,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
*/
protected function getFilenameValidator(): IFilenameValidator {
if ($this->filenameValidator === null) {
- $this->filenameValidator = \OCP\Server::get(IFilenameValidator::class);
+ $this->filenameValidator = Server::get(IFilenameValidator::class);
}
return $this->filenameValidator;
}
@@ -498,7 +499,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
$result = $this->mkdir($targetInternalPath);
if (is_resource($dh)) {
$result = true;
- while ($result and ($file = readdir($dh)) !== false) {
+ while ($result && ($file = readdir($dh)) !== false) {
if (!Filesystem::isIgnoredDir($file)) {
$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file);
}
@@ -512,7 +513,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
$this->writeStream($targetInternalPath, $source);
$result = true;
} catch (\Exception $e) {
- \OC::$server->get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]);
+ Server::get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]);
}
}
@@ -698,8 +699,8 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage,
private function getLockLogger(): ?LoggerInterface {
if (is_null($this->shouldLogLocks)) {
- $this->shouldLogLocks = \OC::$server->getConfig()->getSystemValueBool('filelocking.debug', false);
- $this->logger = $this->shouldLogLocks ? \OC::$server->get(LoggerInterface::class) : null;
+ $this->shouldLogLocks = Server::get(IConfig::class)->getSystemValueBool('filelocking.debug', false);
+ $this->logger = $this->shouldLogLocks ? Server::get(LoggerInterface::class) : null;
}
return $this->logger;
}
diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php
index 6f645c74c11..260f9218a88 100644
--- a/lib/private/Files/Storage/Local.php
+++ b/lib/private/Files/Storage/Local.php
@@ -17,6 +17,7 @@ use OCP\Files\IMimeTypeDetector;
use OCP\Files\Storage\IStorage;
use OCP\Files\StorageNotAvailableException;
use OCP\IConfig;
+use OCP\Server;
use OCP\Util;
use Psr\Log\LoggerInterface;
@@ -56,8 +57,8 @@ class Local extends \OC\Files\Storage\Common {
$this->datadir .= '/';
}
$this->dataDirLength = strlen($this->realDataDir);
- $this->config = \OC::$server->get(IConfig::class);
- $this->mimeTypeDetector = \OC::$server->get(IMimeTypeDetector::class);
+ $this->config = Server::get(IConfig::class);
+ $this->mimeTypeDetector = Server::get(IMimeTypeDetector::class);
$this->defUMask = $this->config->getSystemValue('localstorage.umask', 0022);
$this->caseInsensitive = $this->config->getSystemValueBool('localstorage.case_insensitive', false);
@@ -272,7 +273,7 @@ class Local extends \OC\Files\Storage\Common {
// sets the modification time of the file to the given value.
// If mtime is nil the current time is set.
// note that the access time of the file always changes to the current time.
- if ($this->file_exists($path) and !$this->isUpdatable($path)) {
+ if ($this->file_exists($path) && !$this->isUpdatable($path)) {
return false;
}
$oldMask = umask($this->defUMask);
@@ -328,24 +329,26 @@ class Local extends \OC\Files\Storage\Common {
$dstParent = dirname($target);
if (!$this->isUpdatable($srcParent)) {
- \OC::$server->get(LoggerInterface::class)->error('unable to rename, source directory is not writable : ' . $srcParent, ['app' => 'core']);
+ Server::get(LoggerInterface::class)->error('unable to rename, source directory is not writable : ' . $srcParent, ['app' => 'core']);
return false;
}
if (!$this->isUpdatable($dstParent)) {
- \OC::$server->get(LoggerInterface::class)->error('unable to rename, destination directory is not writable : ' . $dstParent, ['app' => 'core']);
+ Server::get(LoggerInterface::class)->error('unable to rename, destination directory is not writable : ' . $dstParent, ['app' => 'core']);
return false;
}
if (!$this->file_exists($source)) {
- \OC::$server->get(LoggerInterface::class)->error('unable to rename, file does not exists : ' . $source, ['app' => 'core']);
+ Server::get(LoggerInterface::class)->error('unable to rename, file does not exists : ' . $source, ['app' => 'core']);
return false;
}
- if ($this->is_dir($target)) {
- $this->rmdir($target);
- } elseif ($this->is_file($target)) {
- $this->unlink($target);
+ if ($this->file_exists($target)) {
+ if ($this->is_dir($target)) {
+ $this->rmdir($target);
+ } elseif ($this->is_file($target)) {
+ $this->unlink($target);
+ }
}
if ($this->is_dir($source)) {
@@ -485,7 +488,7 @@ class Local extends \OC\Files\Storage\Common {
return $fullPath;
}
- \OC::$server->get(LoggerInterface::class)->error("Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", ['app' => 'core']);
+ Server::get(LoggerInterface::class)->error("Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", ['app' => 'core']);
throw new ForbiddenException('Following symlinks is not allowed', false);
}
diff --git a/lib/private/Files/Storage/Wrapper/Availability.php b/lib/private/Files/Storage/Wrapper/Availability.php
index 70212cacab8..32c51a1b25e 100644
--- a/lib/private/Files/Storage/Wrapper/Availability.php
+++ b/lib/private/Files/Storage/Wrapper/Availability.php
@@ -24,7 +24,7 @@ class Availability extends Wrapper {
protected $config;
public function __construct(array $parameters) {
- $this->config = $parameters['config'] ?? \OC::$server->getConfig();
+ $this->config = $parameters['config'] ?? \OCP\Server::get(IConfig::class);
parent::__construct($parameters);
}
@@ -70,277 +70,128 @@ class Availability extends Wrapper {
}
}
- public function mkdir(string $path): bool {
+ /**
+ * Handles availability checks and delegates method calls dynamically
+ */
+ private function handleAvailability(string $method, mixed ...$args): mixed {
$this->checkAvailability();
try {
- return parent::mkdir($path);
+ return call_user_func_array([parent::class, $method], $args);
} catch (StorageNotAvailableException $e) {
$this->setUnavailable($e);
return false;
}
}
+ public function mkdir(string $path): bool {
+ return $this->handleAvailability('mkdir', $path);
+ }
+
public function rmdir(string $path): bool {
- $this->checkAvailability();
- try {
- return parent::rmdir($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('rmdir', $path);
}
public function opendir(string $path) {
- $this->checkAvailability();
- try {
- return parent::opendir($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('opendir', $path);
}
public function is_dir(string $path): bool {
- $this->checkAvailability();
- try {
- return parent::is_dir($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('is_dir', $path);
}
public function is_file(string $path): bool {
- $this->checkAvailability();
- try {
- return parent::is_file($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('is_file', $path);
}
public function stat(string $path): array|false {
- $this->checkAvailability();
- try {
- return parent::stat($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('stat', $path);
}
public function filetype(string $path): string|false {
- $this->checkAvailability();
- try {
- return parent::filetype($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('filetype', $path);
}
public function filesize(string $path): int|float|false {
- $this->checkAvailability();
- try {
- return parent::filesize($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('filesize', $path);
}
public function isCreatable(string $path): bool {
- $this->checkAvailability();
- try {
- return parent::isCreatable($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('isCreatable', $path);
}
public function isReadable(string $path): bool {
- $this->checkAvailability();
- try {
- return parent::isReadable($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('isReadable', $path);
}
public function isUpdatable(string $path): bool {
- $this->checkAvailability();
- try {
- return parent::isUpdatable($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('isUpdatable', $path);
}
public function isDeletable(string $path): bool {
- $this->checkAvailability();
- try {
- return parent::isDeletable($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('isDeletable', $path);
}
public function isSharable(string $path): bool {
- $this->checkAvailability();
- try {
- return parent::isSharable($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('isSharable', $path);
}
public function getPermissions(string $path): int {
- $this->checkAvailability();
- try {
- return parent::getPermissions($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return 0;
- }
+ return $this->handleAvailability('getPermissions', $path);
}
public function file_exists(string $path): bool {
if ($path === '') {
return true;
}
- $this->checkAvailability();
- try {
- return parent::file_exists($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('file_exists', $path);
}
public function filemtime(string $path): int|false {
- $this->checkAvailability();
- try {
- return parent::filemtime($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('filemtime', $path);
}
public function file_get_contents(string $path): string|false {
- $this->checkAvailability();
- try {
- return parent::file_get_contents($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('file_get_contents', $path);
}
public function file_put_contents(string $path, mixed $data): int|float|false {
- $this->checkAvailability();
- try {
- return parent::file_put_contents($path, $data);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('file_put_contents', $path, $data);
}
public function unlink(string $path): bool {
- $this->checkAvailability();
- try {
- return parent::unlink($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('unlink', $path);
}
public function rename(string $source, string $target): bool {
- $this->checkAvailability();
- try {
- return parent::rename($source, $target);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('rename', $source, $target);
}
public function copy(string $source, string $target): bool {
- $this->checkAvailability();
- try {
- return parent::copy($source, $target);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('copy', $source, $target);
}
public function fopen(string $path, string $mode) {
- $this->checkAvailability();
- try {
- return parent::fopen($path, $mode);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('fopen', $path, $mode);
}
public function getMimeType(string $path): string|false {
- $this->checkAvailability();
- try {
- return parent::getMimeType($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('getMimeType', $path);
}
public function hash(string $type, string $path, bool $raw = false): string|false {
- $this->checkAvailability();
- try {
- return parent::hash($type, $path, $raw);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('hash', $type, $path, $raw);
}
public function free_space(string $path): int|float|false {
- $this->checkAvailability();
- try {
- return parent::free_space($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('free_space', $path);
}
public function touch(string $path, ?int $mtime = null): bool {
- $this->checkAvailability();
- try {
- return parent::touch($path, $mtime);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('touch', $path, $mtime);
}
public function getLocalFile(string $path): string|false {
- $this->checkAvailability();
- try {
- return parent::getLocalFile($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('getLocalFile', $path);
}
public function hasUpdated(string $path, int $time): bool {
@@ -366,43 +217,19 @@ class Availability extends Wrapper {
}
public function getETag(string $path): string|false {
- $this->checkAvailability();
- try {
- return parent::getETag($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('getETag', $path);
}
public function getDirectDownload(string $path): array|false {
- $this->checkAvailability();
- try {
- return parent::getDirectDownload($path);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('getDirectDownload', $path);
}
public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
- $this->checkAvailability();
- try {
- return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('copyFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath);
}
public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
- $this->checkAvailability();
- try {
- return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
- } catch (StorageNotAvailableException $e) {
- $this->setUnavailable($e);
- return false;
- }
+ return $this->handleAvailability('moveFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath);
}
public function getMetaData(string $path): ?array {
diff --git a/lib/private/Files/Storage/Wrapper/Encryption.php b/lib/private/Files/Storage/Wrapper/Encryption.php
index c9ed9c7cb53..ba23f3c43ec 100644
--- a/lib/private/Files/Storage/Wrapper/Encryption.php
+++ b/lib/private/Files/Storage/Wrapper/Encryption.php
@@ -652,7 +652,7 @@ class Encryption extends Wrapper {
$result = true;
}
if (is_resource($dh)) {
- while ($result and ($file = readdir($dh)) !== false) {
+ while ($result && ($file = readdir($dh)) !== false) {
if (!Filesystem::isIgnoredDir($file)) {
$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
}
diff --git a/lib/private/Mail/Mailer.php b/lib/private/Mail/Mailer.php
index da3fcd35888..18636e183d0 100644
--- a/lib/private/Mail/Mailer.php
+++ b/lib/private/Mail/Mailer.php
@@ -27,6 +27,7 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Mailer as SymfonyMailer;
use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\Mailer\Transport\NullTransport;
use Symfony\Component\Mailer\Transport\SendmailTransport;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
@@ -255,9 +256,10 @@ class Mailer implements IMailer {
return $this->instance;
}
- $transport = null;
-
switch ($this->config->getSystemValueString('mail_smtpmode', 'smtp')) {
+ case 'null':
+ $transport = new NullTransport();
+ break;
case 'sendmail':
$transport = $this->getSendMailInstance();
break;
@@ -267,7 +269,9 @@ class Mailer implements IMailer {
break;
}
- return new SymfonyMailer($transport);
+ $this->instance = new SymfonyMailer($transport);
+
+ return $this->instance;
}
/**
diff --git a/lib/private/OCM/OCMDiscoveryService.php b/lib/private/OCM/OCMDiscoveryService.php
index 279162c76f2..203df4bbf9b 100644
--- a/lib/private/OCM/OCMDiscoveryService.php
+++ b/lib/private/OCM/OCMDiscoveryService.php
@@ -55,7 +55,12 @@ class OCMDiscoveryService implements IOCMDiscoveryService {
if (!$skipCache) {
try {
- $this->provider->import(json_decode($this->cache->get($remote) ?? '', true, 8, JSON_THROW_ON_ERROR) ?? []);
+ $cached = $this->cache->get($remote);
+ if ($cached === false) {
+ throw new OCMProviderException('Previous discovery failed.');
+ }
+
+ $this->provider->import(json_decode($cached ?? '', true, 8, JSON_THROW_ON_ERROR) ?? []);
if ($this->supportedAPIVersion($this->provider->getApiVersion())) {
return $this->provider; // if cache looks valid, we use it
}
@@ -85,8 +90,10 @@ class OCMDiscoveryService implements IOCMDiscoveryService {
$this->cache->set($remote, $body, 60 * 60 * 24);
}
} catch (JsonException|OCMProviderException $e) {
+ $this->cache->set($remote, false, 5 * 60);
throw new OCMProviderException('data returned by remote seems invalid - ' . ($body ?? ''));
} catch (\Exception $e) {
+ $this->cache->set($remote, false, 5 * 60);
$this->logger->warning('error while discovering ocm provider', [
'exception' => $e,
'remote' => $remote
@@ -95,6 +102,7 @@ class OCMDiscoveryService implements IOCMDiscoveryService {
}
if (!$this->supportedAPIVersion($this->provider->getApiVersion())) {
+ $this->cache->set($remote, false, 5 * 60);
throw new OCMProviderException('API version not supported');
}
diff --git a/lib/private/Server.php b/lib/private/Server.php
index 27a5f2662f8..d57ddf61c03 100644
--- a/lib/private/Server.php
+++ b/lib/private/Server.php
@@ -7,6 +7,7 @@
namespace OC;
use bantu\IniGetWrapper\IniGetWrapper;
+use NCU\Config\IUserConfig;
use OC\Accounts\AccountManager;
use OC\App\AppManager;
use OC\App\AppStore\Bundles\BundleFetcher;
@@ -567,6 +568,7 @@ class Server extends ServerContainer implements IServerContainer {
});
$this->registerAlias(IAppConfig::class, \OC\AppConfig::class);
+ $this->registerAlias(IUserConfig::class, \OC\Config\UserConfig::class);
$this->registerService(IFactory::class, function (Server $c) {
return new \OC\L10N\Factory(
diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php
index db2734190e4..40fbae541ce 100644
--- a/lib/private/Share20/DefaultShareProvider.php
+++ b/lib/private/Share20/DefaultShareProvider.php
@@ -798,13 +798,15 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USER)),
- $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))
+ $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP)),
+ $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_LINK)),
)
)
->andWhere($qb->expr()->orX(
$qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
$qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
))
+ ->orderBy('id', 'ASC')
->executeQuery();
$shares = [];
diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php
index 4856c051307..4dadcdbfcd7 100644
--- a/lib/private/Share20/Manager.php
+++ b/lib/private/Share20/Manager.php
@@ -17,6 +17,7 @@ use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountManager;
+use OCP\Files\Mount\IShareOwnerlessMount;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\HintException;
@@ -785,13 +786,13 @@ class Manager implements IManager {
* @throws \InvalidArgumentException
* @throws GenericShareException
*/
- public function updateShare(IShare $share) {
+ public function updateShare(IShare $share, bool $onlyValid = true) {
$expirationDateUpdated = false;
$this->canShare($share);
try {
- $originalShare = $this->getShareById($share->getFullId());
+ $originalShare = $this->getShareById($share->getFullId(), onlyValid: $onlyValid);
} catch (\UnexpectedValueException $e) {
throw new \InvalidArgumentException($this->l->t('Share does not have a full ID'));
}
@@ -1215,23 +1216,32 @@ class Manager implements IManager {
throw new \Exception('non-shallow getSharesInFolder is no longer supported');
}
- return array_reduce($providers, function ($shares, IShareProvider $provider) use ($userId, $node, $reshares) {
- $newShares = $provider->getSharesInFolder($userId, $node, $reshares);
- foreach ($newShares as $fid => $data) {
- if (!isset($shares[$fid])) {
- $shares[$fid] = [];
- }
+ $isOwnerless = $node->getMountPoint() instanceof IShareOwnerlessMount;
- $shares[$fid] = array_merge($shares[$fid], $data);
+ $shares = [];
+ foreach ($providers as $provider) {
+ if ($isOwnerless) {
+ foreach ($node->getDirectoryListing() as $childNode) {
+ $data = $provider->getSharesByPath($childNode);
+ $fid = $childNode->getId();
+ $shares[$fid] ??= [];
+ $shares[$fid] = array_merge($shares[$fid], $data);
+ }
+ } else {
+ foreach ($provider->getSharesInFolder($userId, $node, $reshares) as $fid => $data) {
+ $shares[$fid] ??= [];
+ $shares[$fid] = array_merge($shares[$fid], $data);
+ }
}
- return $shares;
- }, []);
+ }
+
+ return $shares;
}
/**
* @inheritdoc
*/
- public function getSharesBy($userId, $shareType, $path = null, $reshares = false, $limit = 50, $offset = 0) {
+ public function getSharesBy($userId, $shareType, $path = null, $reshares = false, $limit = 50, $offset = 0, bool $onlyValid = true) {
if ($path !== null &&
!($path instanceof \OCP\Files\File) &&
!($path instanceof \OCP\Files\Folder)) {
@@ -1244,7 +1254,11 @@ class Manager implements IManager {
return [];
}
- $shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
+ if ($path?->getMountPoint() instanceof IShareOwnerlessMount) {
+ $shares = array_filter($provider->getSharesByPath($path), static fn (IShare $share) => $share->getShareType() === $shareType);
+ } else {
+ $shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
+ }
/*
* Work around so we don't return expired shares but still follow
@@ -1256,11 +1270,13 @@ class Manager implements IManager {
while (true) {
$added = 0;
foreach ($shares as $share) {
- try {
- $this->checkShare($share);
- } catch (ShareNotFound $e) {
- // Ignore since this basically means the share is deleted
- continue;
+ if ($onlyValid) {
+ try {
+ $this->checkShare($share);
+ } catch (ShareNotFound $e) {
+ // Ignore since this basically means the share is deleted
+ continue;
+ }
}
$added++;
@@ -1288,7 +1304,12 @@ class Manager implements IManager {
$offset += $added;
// Fetch again $limit shares
- $shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
+ if ($path?->getMountPoint() instanceof IShareOwnerlessMount) {
+ // We already fetched all shares, so end here
+ $shares = [];
+ } else {
+ $shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
+ }
// No more shares means we are done
if (empty($shares)) {
@@ -1347,7 +1368,7 @@ class Manager implements IManager {
/**
* @inheritdoc
*/
- public function getShareById($id, $recipient = null) {
+ public function getShareById($id, $recipient = null, bool $onlyValid = true) {
if ($id === null) {
throw new ShareNotFound();
}
@@ -1362,7 +1383,9 @@ class Manager implements IManager {
$share = $provider->getShareById($id, $recipient);
- $this->checkShare($share);
+ if ($onlyValid) {
+ $this->checkShare($share);
+ }
return $share;
}
@@ -1469,6 +1492,15 @@ class Manager implements IManager {
$this->deleteShare($share);
throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
}
+
+ try {
+ $share->getNode();
+ // Ignore share, file is still accessible
+ } catch (NotFoundException) {
+ // Access lost, but maybe only temporarily, so don't delete the share right away
+ throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
+ }
+
if ($this->config->getAppValue('files_sharing', 'hide_disabled_user_shares', 'no') === 'yes') {
$uids = array_unique([$share->getShareOwner(),$share->getSharedBy()]);
foreach ($uids as $uid) {
diff --git a/lib/public/Files/Mount/IShareOwnerlessMount.php b/lib/public/Files/Mount/IShareOwnerlessMount.php
new file mode 100644
index 00000000000..b73ee620859
--- /dev/null
+++ b/lib/public/Files/Mount/IShareOwnerlessMount.php
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCP\Files\Mount;
+
+/**
+ * Denotes that shares created under this mountpoint will be manageable by everyone with share permission.
+ *
+ * @since 31.0.0
+ */
+interface IShareOwnerlessMount {
+}
diff --git a/lib/public/Share/IManager.php b/lib/public/Share/IManager.php
index b07bc8f8051..c3a8494a5ac 100644
--- a/lib/public/Share/IManager.php
+++ b/lib/public/Share/IManager.php
@@ -41,11 +41,12 @@ interface IManager {
* The state can't be changed this way: use acceptShare
*
* @param IShare $share
+ * @param bool $onlyValid Only updates valid shares, invalid shares will be deleted automatically and are not updated
* @return IShare The share object
* @throws \InvalidArgumentException
* @since 9.0.0
*/
- public function updateShare(IShare $share);
+ public function updateShare(IShare $share, bool $onlyValid = true);
/**
* Accept a share.
@@ -127,10 +128,11 @@ interface IManager {
* @param bool $reshares
* @param int $limit The maximum number of returned results, -1 for all results
* @param int $offset
+ * @param bool $onlyValid Only returns valid shares, invalid shares will be deleted automatically and are not returned
* @return IShare[]
* @since 9.0.0
*/
- public function getSharesBy($userId, $shareType, $path = null, $reshares = false, $limit = 50, $offset = 0);
+ public function getSharesBy($userId, $shareType, $path = null, $reshares = false, $limit = 50, $offset = 0, bool $onlyValid = true);
/**
* Get shares shared with $user.
@@ -168,11 +170,12 @@ interface IManager {
*
* @param string $id
* @param string|null $recipient userID of the recipient
+ * @param bool $onlyValid Only returns valid shares, invalid shares will be deleted automatically and are not returned
* @return IShare
* @throws ShareNotFound
* @since 9.0.0
*/
- public function getShareById($id, $recipient = null);
+ public function getShareById($id, $recipient = null, bool $onlyValid = true);
/**
* Get the share by token possible with password
diff --git a/lib/unstable/Config/Exceptions/IncorrectTypeException.php b/lib/unstable/Config/Exceptions/IncorrectTypeException.php
new file mode 100644
index 00000000000..a5e4954cdb2
--- /dev/null
+++ b/lib/unstable/Config/Exceptions/IncorrectTypeException.php
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace NCU\Config\Exceptions;
+
+use Exception;
+
+/**
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+class IncorrectTypeException extends Exception {
+}
diff --git a/lib/unstable/Config/Exceptions/TypeConflictException.php b/lib/unstable/Config/Exceptions/TypeConflictException.php
new file mode 100644
index 00000000000..c192b2c4f9d
--- /dev/null
+++ b/lib/unstable/Config/Exceptions/TypeConflictException.php
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace NCU\Config\Exceptions;
+
+use Exception;
+
+/**
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+class TypeConflictException extends Exception {
+}
diff --git a/lib/unstable/Config/Exceptions/UnknownKeyException.php b/lib/unstable/Config/Exceptions/UnknownKeyException.php
new file mode 100644
index 00000000000..5f83800cafc
--- /dev/null
+++ b/lib/unstable/Config/Exceptions/UnknownKeyException.php
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace NCU\Config\Exceptions;
+
+use Exception;
+
+/**
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+class UnknownKeyException extends Exception {
+}
diff --git a/lib/unstable/Config/IUserConfig.php b/lib/unstable/Config/IUserConfig.php
new file mode 100644
index 00000000000..21e5b6fb014
--- /dev/null
+++ b/lib/unstable/Config/IUserConfig.php
@@ -0,0 +1,694 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace NCU\Config;
+
+use Generator;
+use NCU\Config\Exceptions\IncorrectTypeException;
+use NCU\Config\Exceptions\UnknownKeyException;
+
+/**
+ * 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.
+ *
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+interface IUserConfig {
+ /** @since 31.0.0 */
+ public const FLAG_SENSITIVE = 1; // value is sensitive
+ /** @since 31.0.0 */
+ public const FLAG_INDEXED = 2; // value should be indexed
+
+ /**
+ * Get list of all userIds with config stored in database.
+ * If $appId is specified, will only limit the search to this value
+ *
+ * **WARNING:** ignore any cache and get data directly from database.
+ *
+ * @param string $appId optional id of app
+ *
+ * @return list<string> list of userIds
+ * @since 31.0.0
+ */
+ public function getUserIds(string $appId = ''): array;
+
+ /**
+ * Get list of all apps that have at least one config
+ * value related to $userId stored in database
+ *
+ * **WARNING:** ignore lazy filtering, all user config are loaded from database
+ *
+ * @param string $userId id of the user
+ *
+ * @return list<string> list of app ids
+ * @since 31.0.0
+ */
+ public function getApps(string $userId): array;
+
+ /**
+ * Returns all keys stored in database, related to user+app.
+ * Please note that the values are not returned.
+ *
+ * **WARNING:** ignore lazy filtering, all user config are loaded from database
+ *
+ * @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;
+
+ /**
+ * Check if a key exists in the list of stored config values.
+ *
+ * @param string $userId id of the user
+ * @param string $app id of the app
+ * @param string $key config key
+ * @param bool $lazy search within lazy loaded config
+ *
+ * @return bool TRUE if key exists
+ * @since 31.0.0
+ */
+ public function hasKey(string $userId, string $app, string $key, ?bool $lazy = false): bool;
+
+ /**
+ * best way to see if a value is set as sensitive (not displayed in report)
+ *
+ * @param string $userId id of the user
+ * @param string $app id of the app
+ * @param string $key config key
+ * @param bool|null $lazy search within lazy loaded config
+ *
+ * @return bool TRUE if value is sensitive
+ * @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;
+
+ /**
+ * best way to see if a value is set as indexed (so it can be search)
+ *
+ * @see self::searchUsersByValueString()
+ * @see self::searchUsersByValueInt()
+ * @see self::searchUsersByValueBool()
+ * @see self::searchUsersByValues()
+ *
+ * @param string $userId id of the user
+ * @param string $app id of the app
+ * @param string $key config key
+ * @param bool|null $lazy search within lazy loaded config
+ *
+ * @return bool TRUE if value is sensitive
+ * @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;
+
+ /**
+ * Returns if the config key stored in database is lazy loaded
+ *
+ * **WARNING:** ignore lazy filtering, all config values are loaded from database
+ *
+ * @param string $userId id of the user
+ * @param string $app id 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;
+
+ /**
+ * List all config values from an app with config key starting with $key.
+ * Returns an array with config key as key, stored value as value.
+ *
+ * **WARNING:** ignore lazy filtering, all config values are loaded from database
+ *
+ * @param string $userId id of the user
+ * @param string $app id of the app
+ * @param string $prefix config keys prefix to search, can be empty.
+ * @param bool $filtered filter sensitive config values
+ *
+ * @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;
+
+ /**
+ * List all config values of a user.
+ * Returns an array with config key as key, stored value as value.
+ *
+ * **WARNING:** ignore lazy filtering, all config values are loaded from database
+ *
+ * @param string $userId id of the user
+ * @param bool $filtered filter sensitive config values
+ *
+ * @return array<string, string|int|float|bool|array> [key => value]
+ * @since 31.0.0
+ */
+ public function getAllValues(string $userId, bool $filtered = false): array;
+
+ /**
+ * List all apps storing a specific config key and its stored value.
+ * Returns an array with appId as key, stored value as value.
+ *
+ * @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;
+
+ /**
+ * List all users storing a specific config key and its stored value.
+ * Returns an array with userId as key, stored value as value.
+ *
+ * **WARNING:** no caching, generate a fresh request
+ *
+ * @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 the search 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;
+
+ /**
+ * List all users storing a specific config key/value pair.
+ * Returns a list of user ids.
+ *
+ * **WARNING:** no caching, generate a fresh request
+ *
+ * @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;
+
+ /**
+ * List all users storing a specific config key/value pair.
+ * Returns a list of user ids.
+ *
+ * **WARNING:** no caching, generate a fresh request
+ *
+ * @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;
+
+ /**
+ * List all users storing a specific config key/value pair.
+ * Returns a list of user ids.
+ *
+ * **WARNING:** no caching, generate a fresh request
+ *
+ * @param string $app id of the app
+ * @param string $key config key
+ * @param array $values list of possible config values
+ *
+ * @return Generator<string>
+ * @since 31.0.0
+ */
+ public function searchUsersByValues(string $app, string $key, array $values): Generator;
+
+ /**
+ * List all users storing a specific config key/value pair.
+ * Returns a list of user ids.
+ *
+ * **WARNING:** no caching, generate a fresh request
+ *
+ * @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;
+
+ /**
+ * Get user config assigned to a config key.
+ * If config key is not found in database, default value is returned.
+ * If config key is set as lazy loaded, the $lazy argument needs to be set to TRUE.
+ *
+ * @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
+ * @since 31.0.0
+ * @see IUserConfig for explanation about lazy loading
+ * @see getValueInt()
+ * @see getValueFloat()
+ * @see getValueBool()
+ * @see getValueArray()
+ */
+ public function getValueString(string $userId, string $app, string $key, string $default = '', bool $lazy = false): string;
+
+ /**
+ * Get config value assigned to a config key.
+ * If config key is not found in database, default value is returned.
+ * If config key is set as lazy loaded, the $lazy argument needs to be set to TRUE.
+ *
+ * @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
+ * @since 31.0.0
+ * @see IUserConfig for explanation about lazy loading
+ * @see getValueString()
+ * @see getValueFloat()
+ * @see getValueBool()
+ * @see getValueArray()
+ */
+ public function getValueInt(string $userId, string $app, string $key, int $default = 0, bool $lazy = false): int;
+
+ /**
+ * Get config value assigned to a config key.
+ * If config key is not found in database, default value is returned.
+ * If config key is set as lazy loaded, the $lazy argument needs to be set to TRUE.
+ *
+ * @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
+ * @since 31.0.0
+ * @see IUserConfig for explanation about lazy loading
+ * @see getValueString()
+ * @see getValueInt()
+ * @see getValueBool()
+ * @see getValueArray()
+ */
+ public function getValueFloat(string $userId, string $app, string $key, float $default = 0, bool $lazy = false): float;
+
+ /**
+ * Get config value assigned to a config key.
+ * If config key is not found in database, default value is returned.
+ * If config key is set as lazy loaded, the $lazy argument needs to be set to TRUE.
+ *
+ * @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
+ * @since 31.0.0
+ * @see IUserPrefences for explanation about lazy loading
+ * @see getValueString()
+ * @see getValueInt()
+ * @see getValueFloat()
+ * @see getValueArray()
+ */
+ public function getValueBool(string $userId, string $app, string $key, bool $default = false, bool $lazy = false): bool;
+
+ /**
+ * Get config value assigned to a config key.
+ * If config key is not found in database, default value is returned.
+ * If config key is set as lazy loaded, the $lazy argument needs to be set to TRUE.
+ *
+ * @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
+ * @since 31.0.0
+ * @see IUserConfig for explanation about lazy loading
+ * @see getValueString()
+ * @see getValueInt()
+ * @see getValueFloat()
+ * @see getValueBool()
+ */
+ public function getValueArray(string $userId, string $app, string $key, array $default = [], bool $lazy = false): array;
+
+ /**
+ * returns the type of config value
+ *
+ * **WARNING:** ignore lazy filtering, all config values are loaded from database
+ * unless lazy is set to false
+ *
+ * @param string $userId id of the user
+ * @param string $app id of the app
+ * @param string $key config key
+ * @param bool|null $lazy
+ *
+ * @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;
+
+ /**
+ * returns a bitflag related to config value
+ *
+ * **WARNING:** ignore lazy filtering, all config values are loaded from database
+ * unless lazy is set to false
+ *
+ * @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 a bitflag in relation to the config 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;
+
+ /**
+ * Store a config key and its value in database
+ *
+ * If config key is already known with the exact same config value, the database is not updated.
+ * If config key is not supposed to be read during the boot of the cloud, it is advised to set it as lazy loaded.
+ *
+ * If config value was previously stored as sensitive or lazy loaded, status cannot be altered without using {@see deleteKey()} first
+ *
+ * @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 $sensitive if TRUE value will be hidden when listing config values.
+ * @param bool $lazy set config as lazy loaded
+ *
+ * @return bool TRUE if value was different, therefor updated in database
+ * @since 31.0.0
+ * @see IUserConfig for explanation about lazy loading
+ * @see setValueInt()
+ * @see setValueFloat()
+ * @see setValueBool()
+ * @see setValueArray()
+ */
+ public function setValueString(string $userId, string $app, string $key, string $value, bool $lazy = false, int $flags = 0): bool;
+
+ /**
+ * Store a config key and its value in database
+ *
+ * When handling huge value around and/or above 2,147,483,647, a debug log will be generated
+ * on 64bits system, as php int type reach its limit (and throw an exception) on 32bits when using huge numbers.
+ *
+ * When using huge numbers, it is advised to use {@see \OCP\Util::numericToNumber()} and {@see setValueString()}
+ *
+ * If config key is already known with the exact same config value, the database is not updated.
+ * If config key is not supposed to be read during the boot of the cloud, it is advised to set it as lazy loaded.
+ *
+ * If config value was previously stored as sensitive or lazy loaded, status cannot be altered without using {@see deleteKey()} first
+ *
+ * @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 $sensitive if TRUE value will be hidden when listing config values.
+ * @param bool $lazy set config as lazy loaded
+ *
+ * @return bool TRUE if value was different, therefor updated in database
+ * @since 31.0.0
+ * @see IUserConfig for explanation about lazy loading
+ * @see setValueString()
+ * @see setValueFloat()
+ * @see setValueBool()
+ * @see setValueArray()
+ */
+ public function setValueInt(string $userId, string $app, string $key, int $value, bool $lazy = false, int $flags = 0): bool;
+
+ /**
+ * Store a config key and its value in database.
+ *
+ * If config key is already known with the exact same config value, the database is not updated.
+ * If config key is not supposed to be read during the boot of the cloud, it is advised to set it as lazy loaded.
+ *
+ * If config value was previously stored as sensitive or lazy loaded, status cannot be altered without using {@see deleteKey()} first
+ *
+ * @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 $sensitive if TRUE value will be hidden when listing config values.
+ * @param bool $lazy set config as lazy loaded
+ *
+ * @return bool TRUE if value was different, therefor updated in database
+ * @since 31.0.0
+ * @see IUserConfig for explanation about lazy loading
+ * @see setValueString()
+ * @see setValueInt()
+ * @see setValueBool()
+ * @see setValueArray()
+ */
+ public function setValueFloat(string $userId, string $app, string $key, float $value, bool $lazy = false, int $flags = 0): bool;
+
+ /**
+ * Store a config key and its value in database
+ *
+ * If config key is already known with the exact same config value, the database is not updated.
+ * If config key is not supposed to be read during the boot of the cloud, it is advised to set it as lazy loaded.
+ *
+ * If config value was previously stored as lazy loaded, status cannot be altered without using {@see deleteKey()} first
+ *
+ * @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
+ * @since 31.0.0
+ * @see IUserConfig for explanation about lazy loading
+ * @see setValueString()
+ * @see setValueInt()
+ * @see setValueFloat()
+ * @see setValueArray()
+ */
+ public function setValueBool(string $userId, string $app, string $key, bool $value, bool $lazy = false): bool;
+
+ /**
+ * Store a config key and its value in database
+ *
+ * If config key is already known with the exact same config value, the database is not updated.
+ * If config key is not supposed to be read during the boot of the cloud, it is advised to set it as lazy loaded.
+ *
+ * If config value was previously stored as sensitive or lazy loaded, status cannot be altered without using {@see deleteKey()} first
+ *
+ * @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 $sensitive if TRUE value will be hidden when listing config values.
+ * @param bool $lazy set config as lazy loaded
+ *
+ * @return bool TRUE if value was different, therefor updated in database
+ * @since 31.0.0
+ * @see IUserConfig for explanation about lazy loading
+ * @see setValueString()
+ * @see setValueInt()
+ * @see setValueFloat()
+ * @see setValueBool()
+ */
+ public function setValueArray(string $userId, string $app, string $key, array $value, bool $lazy = false, int $flags = 0): bool;
+
+ /**
+ * switch sensitive status of a config value
+ *
+ * **WARNING:** ignore lazy filtering, all config values are loaded from database
+ *
+ * @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 database update were necessary
+ * @since 31.0.0
+ */
+ public function updateSensitive(string $userId, string $app, string $key, bool $sensitive): bool;
+
+ /**
+ * switch sensitive loading status of a config key for all users
+ *
+ * **Warning:** heavy on resources, MUST only be used on occ command or migrations
+ *
+ * @param string $app id of the app
+ * @param string $key config key
+ * @param bool $sensitive TRUE to set as sensitive, FALSE to unset
+ *
+ * @since 31.0.0
+ */
+ public function updateGlobalSensitive(string $app, string $key, bool $sensitive): void;
+
+
+ /**
+ * switch indexed status of a config value
+ *
+ * **WARNING:** ignore lazy filtering, all config values are loaded from database
+ *
+ * @param string $userId id of the user
+ * @param string $app id of the app
+ * @param string $key config key
+ * @param bool $indexed TRUE to set as indexed, FALSE to unset
+ *
+ * @return bool TRUE if database update were necessary
+ * @since 31.0.0
+ */
+ public function updateIndexed(string $userId, string $app, string $key, bool $indexed): bool;
+
+ /**
+ * switch sensitive loading status of a config key for all users
+ *
+ * **Warning:** heavy on resources, MUST only be used on occ command or migrations
+ *
+ * @param string $app id of the app
+ * @param string $key config key
+ * @param bool $indexed TRUE to set as indexed, FALSE to unset
+ * @since 31.0.0
+ */
+ public function updateGlobalIndexed(string $app, string $key, bool $indexed): void;
+
+ /**
+ * switch lazy loading status of a config value
+ *
+ * @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 database update was necessary
+ * @since 31.0.0
+ */
+ public function updateLazy(string $userId, string $app, string $key, bool $lazy): bool;
+
+ /**
+ * switch lazy loading status of a config key for all users
+ *
+ * **Warning:** heavy on resources, MUST only be used on occ command or migrations
+ *
+ * @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;
+
+ /**
+ * returns an array contains details about a config value
+ *
+ * ```
+ * [
+ * "app" => "myapp",
+ * "key" => "mykey",
+ * "value" => "its_value",
+ * "lazy" => false,
+ * "type" => 4,
+ * "typeString" => "string",
+ * 'sensitive' => true
+ * ]
+ * ```
+ *
+ * @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;
+
+ /**
+ * Delete single config key from database.
+ *
+ * @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;
+
+ /**
+ * Delete config values from all users linked to a specific config keys
+ *
+ * @param string $app id of the app
+ * @param string $key config key
+ *
+ * @since 31.0.0
+ */
+ public function deleteKey(string $app, string $key): void;
+
+ /**
+ * delete all config keys linked to an app
+ *
+ * @param string $app id of the app
+ * @since 31.0.0
+ */
+ public function deleteApp(string $app): void;
+
+ /**
+ * delete all config keys linked to a user
+ *
+ * @param string $userId id of the user
+ * @since 31.0.0
+ */
+ public function deleteAllUserConfig(string $userId): void;
+
+ /**
+ * Clear the cache for a single user
+ *
+ * The cache will be rebuilt only the next time a user config is requested.
+ *
+ * @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;
+
+ /**
+ * Clear the cache for all users.
+ * The cache will be rebuilt only the next time a user config is requested.
+ *
+ * @since 31.0.0
+ */
+ public function clearCacheAll(): void;
+}
diff --git a/lib/unstable/Config/ValueType.php b/lib/unstable/Config/ValueType.php
new file mode 100644
index 00000000000..3e1e47e9d7c
--- /dev/null
+++ b/lib/unstable/Config/ValueType.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace NCU\Config;
+
+use NCU\Config\Exceptions\IncorrectTypeException;
+use UnhandledMatchError;
+
+/**
+ * Listing of available value type for typed config value
+ *
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+enum ValueType: int {
+ /** @since 31.0.0 */
+ case MIXED = 0;
+ /** @since 31.0.0 */
+ case STRING = 1;
+ /** @since 31.0.0 */
+ case INT = 2;
+ /** @since 31.0.0 */
+ case FLOAT = 3;
+ /** @since 31.0.0 */
+ case BOOL = 4;
+ /** @since 31.0.0 */
+ case ARRAY = 5;
+
+ /**
+ * get ValueType from string
+ *
+ * @param string $definition
+ *
+ * @return self
+ * @throws IncorrectTypeException
+ * @since 31.0.0
+ */
+ public static function fromStringDefinition(string $definition): self {
+ try {
+ return match ($definition) {
+ 'mixed' => self::MIXED,
+ 'string' => self::STRING,
+ 'int' => self::INT,
+ 'float' => self::FLOAT,
+ 'bool' => self::BOOL,
+ 'array' => self::ARRAY
+ };
+ } catch (\UnhandledMatchError) {
+ throw new IncorrectTypeException('unknown string definition');
+ }
+ }
+
+ /**
+ * get string definition for current enum value
+ *
+ * @return string
+ * @throws IncorrectTypeException
+ * @since 31.0.0
+ */
+ public function getDefinition(): string {
+ try {
+ return match ($this) {
+ self::MIXED => 'mixed',
+ self::STRING => 'string',
+ self::INT => 'int',
+ self::FLOAT => 'float',
+ self::BOOL => 'bool',
+ self::ARRAY => 'array',
+ };
+ } catch (UnhandledMatchError) {
+ throw new IncorrectTypeException('unknown type definition ' . $this->value);
+ }
+ }
+}