aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private')
-rw-r--r--lib/private/Activity/Manager.php4
-rw-r--r--lib/private/AllConfig.php18
-rw-r--r--lib/private/App/AppStore/Fetcher/Fetcher.php3
-rw-r--r--lib/private/App/DependencyAnalyzer.php2
-rw-r--r--lib/private/App/InfoParser.php2
-rw-r--r--lib/private/AppConfig.php46
-rw-r--r--lib/private/AppFramework/App.php2
-rw-r--r--lib/private/AppFramework/Bootstrap/RegistrationContext.php4
-rw-r--r--lib/private/AppFramework/DependencyInjection/DIContainer.php6
-rw-r--r--lib/private/AppFramework/Http/Dispatcher.php10
-rw-r--r--lib/private/AppFramework/Http/Request.php31
-rw-r--r--lib/private/AppFramework/Middleware/MiddlewareDispatcher.php8
-rw-r--r--lib/private/AppFramework/Middleware/Security/CORSMiddleware.php2
-rw-r--r--lib/private/AppFramework/Middleware/Security/CSPMiddleware.php25
-rw-r--r--lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php2
-rw-r--r--lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php4
-rw-r--r--lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php8
-rw-r--r--lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php35
-rw-r--r--lib/private/AppFramework/Utility/ControllerMethodReflector.php4
-rw-r--r--lib/private/AppFramework/Utility/SimpleContainer.php2
-rw-r--r--lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php2
-rw-r--r--lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php6
-rw-r--r--lib/private/Authentication/Token/PublicKeyTokenMapper.php17
-rw-r--r--lib/private/Authentication/Token/PublicKeyTokenProvider.php31
-rw-r--r--lib/private/Authentication/Token/RemoteWipe.php4
-rw-r--r--lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php4
-rw-r--r--lib/private/Authentication/TwoFactorAuth/Manager.php6
-rw-r--r--lib/private/Authentication/WebAuthn/CredentialRepository.php9
-rw-r--r--lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php11
-rw-r--r--lib/private/Authentication/WebAuthn/Manager.php15
-rw-r--r--lib/private/Avatar/Avatar.php6
-rw-r--r--lib/private/Avatar/AvatarManager.php2
-rw-r--r--lib/private/BackgroundJob/JobList.php6
-rw-r--r--lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php2
-rw-r--r--lib/private/Calendar/Manager.php3
-rw-r--r--lib/private/Calendar/ResourcesRoomsUpdater.php2
-rw-r--r--lib/private/Collaboration/AutoComplete/Manager.php2
-rw-r--r--lib/private/Collaboration/Collaborators/UserPlugin.php4
-rw-r--r--lib/private/Collaboration/Reference/ReferenceManager.php4
-rw-r--r--lib/private/Collaboration/Resources/Manager.php18
-rw-r--r--lib/private/Collaboration/Resources/Resource.php2
-rw-r--r--lib/private/Comments/Comment.php4
-rw-r--r--lib/private/Comments/Manager.php28
-rw-r--r--lib/private/Config.php4
-rw-r--r--lib/private/Console/Application.php12
-rw-r--r--lib/private/Console/TimestampFormatter.php2
-rw-r--r--lib/private/Contacts/ContactsMenu/ContactsStore.php2
-rw-r--r--lib/private/ContactsManager.php16
-rw-r--r--lib/private/DB/Adapter.php30
-rw-r--r--lib/private/DB/AdapterSqlite.php4
-rw-r--r--lib/private/DB/ArrayResult.php74
-rw-r--r--lib/private/DB/Connection.php174
-rw-r--r--lib/private/DB/ConnectionAdapter.php10
-rw-r--r--lib/private/DB/ConnectionFactory.php30
-rw-r--r--lib/private/DB/MigrationService.php36
-rw-r--r--lib/private/DB/Migrator.php2
-rw-r--r--lib/private/DB/PreparedStatement.php2
-rw-r--r--lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php28
-rw-r--r--lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php18
-rw-r--r--lib/private/DB/QueryBuilder/Literal.php2
-rw-r--r--lib/private/DB/QueryBuilder/Parameter.php2
-rw-r--r--lib/private/DB/QueryBuilder/Partitioned/InvalidPartitionedQueryException.php79
-rw-r--r--lib/private/DB/QueryBuilder/Partitioned/JoinCondition.php173
-rw-r--r--lib/private/DB/QueryBuilder/Partitioned/PartitionQuery.php75
-rw-r--r--lib/private/DB/QueryBuilder/Partitioned/PartitionSplit.php74
-rw-r--r--lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php426
-rw-r--r--lib/private/DB/QueryBuilder/Partitioned/PartitionedResult.php61
-rw-r--r--lib/private/DB/QueryBuilder/QueryBuilder.php57
-rw-r--r--lib/private/DB/QueryBuilder/QueryFunction.php2
-rw-r--r--lib/private/DB/QueryBuilder/QuoteHelper.php2
-rw-r--r--lib/private/DB/QueryBuilder/Sharded/AutoIncrementHandler.php152
-rw-r--r--lib/private/DB/QueryBuilder/Sharded/CrossShardMoveHelper.php162
-rw-r--r--lib/private/DB/QueryBuilder/Sharded/HashShardMapper.php21
-rw-r--r--lib/private/DB/QueryBuilder/Sharded/InvalidShardedQueryException.php29
-rw-r--r--lib/private/DB/QueryBuilder/Sharded/RoundRobinShardMapper.php20
-rw-r--r--lib/private/DB/QueryBuilder/Sharded/ShardConnectionManager.php43
-rw-r--r--lib/private/DB/QueryBuilder/Sharded/ShardDefinition.php66
-rw-r--r--lib/private/DB/QueryBuilder/Sharded/ShardQueryRunner.php197
-rw-r--r--lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php407
-rw-r--r--lib/private/DB/SchemaWrapper.php3
-rw-r--r--lib/private/DateTimeFormatter.php128
-rw-r--r--lib/private/DirectEditing/Token.php2
-rw-r--r--lib/private/Encryption/DecryptAll.php14
-rw-r--r--lib/private/Encryption/EncryptionWrapper.php4
-rw-r--r--lib/private/Encryption/File.php2
-rw-r--r--lib/private/Encryption/HookManager.php2
-rw-r--r--lib/private/EventSource.php6
-rw-r--r--lib/private/Federation/CloudIdManager.php2
-rw-r--r--lib/private/Files/Cache/Cache.php109
-rw-r--r--lib/private/Files/Cache/FailedCache.php2
-rw-r--r--lib/private/Files/Cache/QuerySearchHelper.php2
-rw-r--r--lib/private/Files/Cache/Storage.php2
-rw-r--r--lib/private/Files/Cache/Updater.php4
-rw-r--r--lib/private/Files/Config/MountProviderCollection.php6
-rw-r--r--lib/private/Files/Config/UserMountCache.php2
-rw-r--r--lib/private/Files/FileInfo.php12
-rw-r--r--lib/private/Files/FilenameValidator.php31
-rw-r--r--lib/private/Files/Filesystem.php2
-rw-r--r--lib/private/Files/Mount/Manager.php4
-rw-r--r--lib/private/Files/Mount/RootMountProvider.php2
-rw-r--r--lib/private/Files/Node/Folder.php2
-rw-r--r--lib/private/Files/Node/LazyUserFolder.php2
-rw-r--r--lib/private/Files/Node/Node.php15
-rw-r--r--lib/private/Files/Node/NonExistingFile.php8
-rw-r--r--lib/private/Files/Node/NonExistingFolder.php8
-rw-r--r--lib/private/Files/Node/Root.php2
-rw-r--r--lib/private/Files/ObjectStore/Azure.php4
-rw-r--r--lib/private/Files/ObjectStore/ObjectStoreStorage.php27
-rw-r--r--lib/private/Files/ObjectStore/S3ConnectionTrait.php6
-rw-r--r--lib/private/Files/ObjectStore/S3ObjectTrait.php18
-rw-r--r--lib/private/Files/ObjectStore/S3Signature.php2
-rw-r--r--lib/private/Files/ObjectStore/SwiftFactory.php2
-rw-r--r--lib/private/Files/Search/SearchQuery.php8
-rw-r--r--lib/private/Files/SetupManager.php13
-rw-r--r--lib/private/Files/Storage/Common.php26
-rw-r--r--lib/private/Files/Storage/DAV.php26
-rw-r--r--lib/private/Files/Storage/Home.php2
-rw-r--r--lib/private/Files/Storage/PolyFill/CopyDirectory.php2
-rw-r--r--lib/private/Files/Storage/StorageFactory.php2
-rw-r--r--lib/private/Files/Storage/Wrapper/Encryption.php10
-rw-r--r--lib/private/Files/Storage/Wrapper/Quota.php2
-rw-r--r--lib/private/Files/Storage/Wrapper/Wrapper.php2
-rw-r--r--lib/private/Files/Stream/Encryption.php2
-rw-r--r--lib/private/Files/Stream/SeekableHttpStream.php2
-rw-r--r--lib/private/Files/Type/Loader.php6
-rw-r--r--lib/private/Files/View.php31
-rw-r--r--lib/private/FilesMetadata/Service/IndexRequestService.php20
-rw-r--r--lib/private/FilesMetadata/Service/MetadataRequestService.php30
-rw-r--r--lib/private/FullTextSearch/Model/IndexDocument.php8
-rw-r--r--lib/private/Group/Database.php6
-rw-r--r--lib/private/Group/Group.php4
-rw-r--r--lib/private/Group/Manager.php2
-rw-r--r--lib/private/Group/MetaData.php2
-rw-r--r--lib/private/Http/Client/Client.php568
-rw-r--r--lib/private/Http/Client/ClientService.php2
-rw-r--r--lib/private/Http/Client/GuzzlePromiseAdapter.php4
-rw-r--r--lib/private/Http/Client/NegativeDnsCache.php4
-rw-r--r--lib/private/Http/WellKnown/RequestManager.php4
-rw-r--r--lib/private/Image.php (renamed from lib/private/legacy/OC_Image.php)269
-rw-r--r--lib/private/Installer.php2
-rw-r--r--lib/private/L10N/L10N.php20
-rw-r--r--lib/private/LDAP/NullLDAPProviderFactory.php2
-rwxr-xr-xlib/private/LargeFileHelper.php2
-rw-r--r--lib/private/Lock/MemcacheLockingProvider.php4
-rw-r--r--lib/private/Log.php8
-rw-r--r--lib/private/Log/File.php2
-rw-r--r--lib/private/Log/LogDetails.php2
-rw-r--r--lib/private/Log/PsrLoggerAdapter.php18
-rw-r--r--lib/private/Mail/EMailTemplate.php12
-rw-r--r--lib/private/Mail/Mailer.php34
-rw-r--r--lib/private/Mail/Provider/Manager.php32
-rw-r--r--lib/private/Memcache/CADTrait.php17
-rw-r--r--lib/private/Memcache/Factory.php45
-rw-r--r--lib/private/Memcache/LoggerWrapperCache.php11
-rw-r--r--lib/private/Memcache/NullCache.php5
-rw-r--r--lib/private/Memcache/ProfilerWrapperCache.php14
-rw-r--r--lib/private/Memcache/Redis.php14
-rw-r--r--lib/private/NaturalSort.php2
-rw-r--r--lib/private/Notification/Manager.php8
-rw-r--r--lib/private/OCM/Model/OCMProvider.php4
-rw-r--r--lib/private/OCM/Model/OCMResource.php4
-rw-r--r--lib/private/Preview/BackgroundCleanupJob.php57
-rw-r--r--lib/private/Preview/Bitmap.php2
-rw-r--r--lib/private/Preview/Generator.php8
-rw-r--r--lib/private/Preview/HEIC.php4
-rw-r--r--lib/private/Preview/MarkDown.php2
-rw-r--r--lib/private/Preview/Movie.php2
-rw-r--r--lib/private/Preview/SVG.php2
-rw-r--r--lib/private/Preview/TXT.php4
-rw-r--r--lib/private/PreviewManager.php2
-rw-r--r--lib/private/Profile/ProfileManager.php2
-rw-r--r--lib/private/Profiler/FileProfilerStorage.php2
-rw-r--r--lib/private/RedisFactory.php2
-rw-r--r--lib/private/Repair.php2
-rw-r--r--lib/private/Repair/CleanTags.php23
-rw-r--r--lib/private/Repair/Collation.php16
-rw-r--r--lib/private/Repair/NC25/AddMissingSecretJob.php4
-rw-r--r--lib/private/Repair/OldGroupMembershipShares.php8
-rw-r--r--lib/private/Repair/Owncloud/MigrateOauthTables.php10
-rw-r--r--lib/private/Repair/Owncloud/SaveAccountsTableData.php2
-rw-r--r--lib/private/Repair/RemoveLinkShares.php4
-rw-r--r--lib/private/Repair/RepairInvalidShares.php4
-rw-r--r--lib/private/Repair/RepairLogoDimension.php17
-rw-r--r--lib/private/Repair/RepairMimeTypes.php13
-rw-r--r--lib/private/Route/Route.php2
-rw-r--r--lib/private/Route/Router.php38
-rw-r--r--lib/private/Search/FilterFactory.php2
-rw-r--r--lib/private/Security/Bruteforce/Backend/DatabaseBackend.php2
-rw-r--r--lib/private/Security/Bruteforce/Throttler.php4
-rw-r--r--lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php6
-rw-r--r--lib/private/Security/Ip/Range.php2
-rw-r--r--lib/private/Security/Ip/RemoteAddress.php2
-rw-r--r--lib/private/Security/SecureRandom.php2
-rw-r--r--lib/private/Server.php71
-rw-r--r--lib/private/Session/Internal.php8
-rw-r--r--lib/private/Settings/Section.php10
-rw-r--r--lib/private/Setup.php12
-rw-r--r--lib/private/Setup/AbstractDatabase.php14
-rw-r--r--lib/private/Setup/OCI.php6
-rw-r--r--lib/private/Setup/PostgreSQL.php30
-rw-r--r--lib/private/Share20/DefaultShareProvider.php95
-rw-r--r--lib/private/Share20/Manager.php7
-rw-r--r--lib/private/Share20/ProviderFactory.php5
-rw-r--r--lib/private/Share20/Share.php12
-rw-r--r--lib/private/Share20/ShareAttributes.php6
-rw-r--r--lib/private/SpeechToText/SpeechToTextManager.php29
-rw-r--r--lib/private/SpeechToText/TranscriptionJob.php2
-rw-r--r--lib/private/StreamImage.php8
-rw-r--r--lib/private/Streamer.php2
-rw-r--r--lib/private/SystemConfig.php20
-rw-r--r--lib/private/SystemTag/SystemTagObjectMapper.php2
-rw-r--r--lib/private/TagManager.php2
-rw-r--r--lib/private/Tags.php4
-rw-r--r--lib/private/Talk/Broker.php8
-rw-r--r--lib/private/TaskProcessing/Manager.php245
-rw-r--r--lib/private/TaskProcessing/SynchronousBackgroundJob.php66
-rw-r--r--lib/private/TempManager.php2
-rw-r--r--lib/private/Template/Base.php9
-rw-r--r--lib/private/Template/JSConfigHelper.php32
-rw-r--r--lib/private/TemplateLayout.php11
-rw-r--r--lib/private/TextProcessing/Manager.php61
-rw-r--r--lib/private/TextToImage/Manager.php8
-rw-r--r--lib/private/URLGenerator.php4
-rw-r--r--lib/private/Updater.php2
-rw-r--r--lib/private/Updater/VersionCheck.php2
-rw-r--r--lib/private/User/Database.php2
-rw-r--r--lib/private/User/Manager.php6
-rw-r--r--lib/private/User/User.php4
-rw-r--r--lib/private/legacy/OC_App.php4
-rw-r--r--lib/private/legacy/OC_Files.php6
-rw-r--r--lib/private/legacy/OC_Helper.php20
-rw-r--r--lib/private/legacy/OC_Hook.php6
-rw-r--r--lib/private/legacy/OC_Template.php15
-rw-r--r--lib/private/legacy/OC_User.php8
-rw-r--r--lib/private/legacy/OC_Util.php20
-rw-r--r--lib/private/legacy/template/functions.php12
236 files changed, 4404 insertions, 1402 deletions
diff --git a/lib/private/Activity/Manager.php b/lib/private/Activity/Manager.php
index 8b60dc49ec9..54d50a47dcb 100644
--- a/lib/private/Activity/Manager.php
+++ b/lib/private/Activity/Manager.php
@@ -291,7 +291,7 @@ class Manager implements IManager {
public function isFormattingFilteredObject(): bool {
return $this->formattingObjectType !== null && $this->formattingObjectId !== null
&& $this->formattingObjectType === $this->request->getParam('object_type')
- && $this->formattingObjectId === (int) $this->request->getParam('object_id');
+ && $this->formattingObjectId === (int)$this->request->getParam('object_id');
}
/**
@@ -344,7 +344,7 @@ class Manager implements IManager {
* @throws \UnexpectedValueException If the token is invalid, does not exist or is not unique
*/
protected function getUserFromToken(): string {
- $token = (string) $this->request->getParam('token', '');
+ $token = (string)$this->request->getParam('token', '');
if (strlen($token) !== 30) {
throw new \UnexpectedValueException('The token is invalid');
}
diff --git a/lib/private/AllConfig.php b/lib/private/AllConfig.php
index 58eee772fbf..263b5283133 100644
--- a/lib/private/AllConfig.php
+++ b/lib/private/AllConfig.php
@@ -107,7 +107,7 @@ class AllConfig implements IConfig {
* @since 16.0.0
*/
public function getSystemValueBool(string $key, bool $default = false): bool {
- return (bool) $this->getSystemValue($key, $default);
+ return (bool)$this->getSystemValue($key, $default);
}
/**
@@ -121,7 +121,7 @@ class AllConfig implements IConfig {
* @since 16.0.0
*/
public function getSystemValueInt(string $key, int $default = 0): int {
- return (int) $this->getSystemValue($key, $default);
+ return (int)$this->getSystemValue($key, $default);
}
/**
@@ -135,7 +135,7 @@ class AllConfig implements IConfig {
* @since 16.0.0
*/
public function getSystemValueString(string $key, string $default = ''): string {
- return (string) $this->getSystemValue($key, $default);
+ return (string)$this->getSystemValue($key, $default);
}
/**
@@ -236,7 +236,7 @@ class AllConfig implements IConfig {
$this->fixDIInit();
if ($appName === 'settings' && $key === 'email') {
- $value = strtolower((string) $value);
+ $value = strtolower((string)$value);
}
$prevValue = $this->getUserValue($userId, $appName, $key, null);
@@ -382,9 +382,9 @@ class AllConfig implements IConfig {
* @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 ]
- * ]
+ * [ $appId =>
+ * [ $key => $value ]
+ * ]
*/
public function getAllUserValues(?string $userId): array {
if (isset($this->userCache[$userId])) {
@@ -462,7 +462,7 @@ 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 array of user IDs
+ * @return list<string> of user IDs
*/
public function getUsersForUserValue($appName, $key, $value) {
// TODO - FIXME
@@ -496,7 +496,7 @@ 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 array of user IDs
+ * @return list<string> of user IDs
*/
public function getUsersForUserValueCaseInsensitive($appName, $key, $value) {
// TODO - FIXME
diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php
index ad76befc5fa..28252f264c3 100644
--- a/lib/private/App/AppStore/Fetcher/Fetcher.php
+++ b/lib/private/App/AppStore/Fetcher/Fetcher.php
@@ -86,7 +86,8 @@ abstract class Fetcher {
$response = $client->get($this->getEndpoint(), $options);
} catch (ConnectException $e) {
$this->config->setAppValue('settings', 'appstore-fetcher-lastFailure', (string)time());
- throw $e;
+ $this->logger->error('Failed to connect to the app store', ['exception' => $e]);
+ return [];
}
$responseJson = [];
diff --git a/lib/private/App/DependencyAnalyzer.php b/lib/private/App/DependencyAnalyzer.php
index d963c74de79..72b38ca12c7 100644
--- a/lib/private/App/DependencyAnalyzer.php
+++ b/lib/private/App/DependencyAnalyzer.php
@@ -66,7 +66,7 @@ class DependencyAnalyzer {
* @param string $first
* @param string $second
* @return string[] first element is the first version, second element is the
- * second version
+ * second version
*/
private function normalizeVersions($first, $second) {
$first = explode('.', $first);
diff --git a/lib/private/App/InfoParser.php b/lib/private/App/InfoParser.php
index 54afd0069fb..d0e67b82f21 100644
--- a/lib/private/App/InfoParser.php
+++ b/lib/private/App/InfoParser.php
@@ -223,7 +223,7 @@ class InfoParser {
$totalElement = count($xml->{$element});
if (!isset($array[$element])) {
- $array[$element] = $totalElement > 1 ? [] : "";
+ $array[$element] = $totalElement > 1 ? [] : '';
}
/** @var \SimpleXMLElement $node */
// Has attributes
diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php
index d046557e42c..c27150a67dc 100644
--- a/lib/private/AppConfig.php
+++ b/lib/private/AppConfig.php
@@ -748,11 +748,11 @@ class AppConfig implements IAppConfig {
try {
$insert = $this->connection->getQueryBuilder();
$insert->insert('appconfig')
- ->setValue('appid', $insert->createNamedParameter($app))
- ->setValue('lazy', $insert->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
- ->setValue('type', $insert->createNamedParameter($type, IQueryBuilder::PARAM_INT))
- ->setValue('configkey', $insert->createNamedParameter($key))
- ->setValue('configvalue', $insert->createNamedParameter($value));
+ ->setValue('appid', $insert->createNamedParameter($app))
+ ->setValue('lazy', $insert->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
+ ->setValue('type', $insert->createNamedParameter($type, IQueryBuilder::PARAM_INT))
+ ->setValue('configkey', $insert->createNamedParameter($key))
+ ->setValue('configvalue', $insert->createNamedParameter($value));
$insert->executeStatement();
$inserted = true;
} catch (DBException $e) {
@@ -807,11 +807,11 @@ class AppConfig implements IAppConfig {
$update = $this->connection->getQueryBuilder();
$update->update('appconfig')
- ->set('configvalue', $update->createNamedParameter($value))
- ->set('lazy', $update->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
- ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
- ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
- ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
+ ->set('configvalue', $update->createNamedParameter($value))
+ ->set('lazy', $update->createNamedParameter(($lazy) ? 1 : 0, IQueryBuilder::PARAM_INT))
+ ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
+ ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
+ ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
$update->executeStatement();
}
@@ -869,9 +869,9 @@ class AppConfig implements IAppConfig {
$update = $this->connection->getQueryBuilder();
$update->update('appconfig')
- ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
- ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
- ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
+ ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
+ ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
+ ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
$update->executeStatement();
$this->valueTypes[$app][$key] = $type;
@@ -927,10 +927,10 @@ class AppConfig implements IAppConfig {
$update = $this->connection->getQueryBuilder();
$update->update('appconfig')
- ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
- ->set('configvalue', $update->createNamedParameter($value))
- ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
- ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
+ ->set('type', $update->createNamedParameter($type, IQueryBuilder::PARAM_INT))
+ ->set('configvalue', $update->createNamedParameter($value))
+ ->where($update->expr()->eq('appid', $update->createNamedParameter($app)))
+ ->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
$update->executeStatement();
$this->valueTypes[$app][$key] = $type;
@@ -962,9 +962,9 @@ class AppConfig implements IAppConfig {
$update = $this->connection->getQueryBuilder();
$update->update('appconfig')
- ->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)));
+ ->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();
// At this point, it is a lot safer to clean cache
@@ -1075,8 +1075,8 @@ class AppConfig implements IAppConfig {
$this->assertParams($app, $key);
$qb = $this->connection->getQueryBuilder();
$qb->delete('appconfig')
- ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
- ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
+ ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)))
+ ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter($key)));
$qb->executeStatement();
unset($this->lazyCache[$app][$key]);
@@ -1094,7 +1094,7 @@ class AppConfig implements IAppConfig {
$this->assertParams($app);
$qb = $this->connection->getQueryBuilder();
$qb->delete('appconfig')
- ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
+ ->where($qb->expr()->eq('appid', $qb->createNamedParameter($app)));
$qb->executeStatement();
$this->clearCache();
diff --git a/lib/private/AppFramework/App.php b/lib/private/AppFramework/App.php
index 9f9fb32dbcb..c5f61d7e938 100644
--- a/lib/private/AppFramework/App.php
+++ b/lib/private/AppFramework/App.php
@@ -37,7 +37,7 @@ class App {
* namespace tag or uppercasing the appid's first letter
* @param string $appId the app id
* @param string $topNamespace the namespace which should be prepended to
- * the transformed app id, defaults to OCA\
+ * the transformed app id, defaults to OCA\
* @return string the starting namespace for the app
*/
public static function buildAppNamespace(string $appId, string $topNamespace = 'OCA\\'): string {
diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php
index f59d5b55706..d7a380f9e1d 100644
--- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php
+++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php
@@ -538,10 +538,10 @@ class RegistrationContext {
public function registerTalkBackend(string $appId, string $backend) {
// Some safeguards for invalid registrations
if ($appId !== 'spreed') {
- throw new RuntimeException("Only the Talk app is allowed to register a Talk backend");
+ throw new RuntimeException('Only the Talk app is allowed to register a Talk backend');
}
if ($this->talkBackendRegistration !== null) {
- throw new RuntimeException("There can only be one Talk backend");
+ throw new RuntimeException('There can only be one Talk backend');
}
$this->talkBackendRegistration = new ServiceRegistration($appId, $backend);
diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php
index 208031b942f..a96e050c0e6 100644
--- a/lib/private/AppFramework/DependencyInjection/DIContainer.php
+++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php
@@ -36,6 +36,7 @@ use OCP\Files\IAppData;
use OCP\Group\ISubAdmin;
use OCP\IConfig;
use OCP\IDBConnection;
+use OCP\IGroupManager;
use OCP\IInitialStateService;
use OCP\IL10N;
use OCP\ILogger;
@@ -228,8 +229,8 @@ class DIContainer extends SimpleContainer implements IAppContainer {
$server->get(LoggerInterface::class),
$c->get('AppName'),
$server->getUserSession()->isLoggedIn(),
- $this->getUserId() !== null && $server->getGroupManager()->isAdmin($this->getUserId()),
- $server->getUserSession()->getUser() !== null && $server->query(ISubAdmin::class)->isSubAdmin($server->getUserSession()->getUser()),
+ $c->get(IGroupManager::class),
+ $c->get(ISubAdmin::class),
$server->getAppManager(),
$server->getL10N('lib'),
$c->get(AuthorizedGroupMapper::class),
@@ -241,7 +242,6 @@ class DIContainer extends SimpleContainer implements IAppContainer {
new OC\AppFramework\Middleware\Security\CSPMiddleware(
$server->query(OC\Security\CSP\ContentSecurityPolicyManager::class),
$server->query(OC\Security\CSP\ContentSecurityPolicyNonceManager::class),
- $server->query(OC\Security\CSRF\CsrfTokenManager::class)
)
);
$dispatcher->registerMiddleware(
diff --git a/lib/private/AppFramework/Http/Dispatcher.php b/lib/private/AppFramework/Http/Dispatcher.php
index bbb68972a41..b7952df8d19 100644
--- a/lib/private/AppFramework/Http/Dispatcher.php
+++ b/lib/private/AppFramework/Http/Dispatcher.php
@@ -55,9 +55,9 @@ class Dispatcher {
/**
* @param Http $protocol the http protocol with contains all status headers
* @param MiddlewareDispatcher $middlewareDispatcher the dispatcher which
- * runs the middleware
+ * runs the middleware
* @param ControllerMethodReflector $reflector the reflector that is used to inject
- * the arguments for the controller
+ * the arguments for the controller
* @param IRequest $request the incoming request
* @param IConfig $config
* @param ConnectionAdapter $connection
@@ -89,10 +89,10 @@ class Dispatcher {
* Handles a request and calls the dispatcher on the controller
* @param Controller $controller the controller which will be called
* @param string $methodName the method name which will be called on
- * the controller
+ * the controller
* @return array $array[0] contains a string with the http main header,
- * $array[1] contains headers in the form: $key => value, $array[2] contains
- * the response output
+ * $array[1] contains headers in the form: $key => value, $array[2] contains
+ * the response output
* @throws \Exception
*/
public function dispatch(Controller $controller, string $methodName): array {
diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php
index f790dae226c..4bbeabb7aae 100644
--- a/lib/private/AppFramework/Http/Request.php
+++ b/lib/private/AppFramework/Http/Request.php
@@ -38,6 +38,7 @@ class Request implements \ArrayAccess, \Countable, IRequest {
public const USER_AGENT_CHROME = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)( Ubuntu Chromium\/[0-9.]+|) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+( (Vivaldi|Brave|OPR)\/[0-9.]+|)$/';
// Safari User Agent from http://www.useragentstring.com/pages/Safari/
public const USER_AGENT_SAFARI = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ Safari\/[0-9.A-Z]+$/';
+ public const USER_AGENT_SAFARI_MOBILE = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ (Mobile\/[0-9.A-Z]+) Safari\/[0-9.A-Z]+$/';
// Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
public const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#';
public const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#';
@@ -66,15 +67,15 @@ class Request implements \ArrayAccess, \Countable, IRequest {
/**
* @param array $vars An associative array with the following optional values:
- * - array 'urlParams' the parameters which were matched from the URL
- * - array 'get' the $_GET array
- * - array|string 'post' the $_POST array or JSON string
- * - array 'files' the $_FILES array
- * - array 'server' the $_SERVER array
- * - array 'env' the $_ENV array
- * - array 'cookies' the $_COOKIE array
- * - string 'method' the request method (GET, POST etc)
- * - string|false 'requesttoken' the requesttoken or false when not available
+ * - array 'urlParams' the parameters which were matched from the URL
+ * - array 'get' the $_GET array
+ * - array|string 'post' the $_POST array or JSON string
+ * - array 'files' the $_FILES array
+ * - array 'server' the $_SERVER array
+ * - array 'env' the $_ENV array
+ * - array 'cookies' the $_COOKIE array
+ * - string 'method' the request method (GET, POST etc)
+ * - string|false 'requesttoken' the requesttoken or false when not available
* @param IRequestId $requestId
* @param IConfig $config
* @param CsrfTokenManager|null $csrfTokenManager
@@ -283,11 +284,11 @@ class Request implements \ArrayAccess, \Countable, IRequest {
* In case of json requests the encoded json body is accessed
*
* @param string $key the key which you want to access in the URL Parameter
- * placeholder, $_POST or $_GET array.
- * The priority how they're returned is the following:
- * 1. URL parameters
- * 2. POST parameters
- * 3. GET parameters
+ * placeholder, $_POST or $_GET array.
+ * The priority how they're returned is the following:
+ * 1. URL parameters
+ * 2. POST parameters
+ * 3. GET parameters
* @param mixed $default If the key is not found, this value will be returned
* @return mixed the content of the array
*/
@@ -836,7 +837,7 @@ class Request implements \ArrayAccess, \Countable, IRequest {
* Returns the overwritehost setting from the config if set and
* if the overwrite condition is met
* @return string|null overwritehost value or null if not defined or the defined condition
- * isn't met
+ * isn't met
*/
private function getOverwriteHost() {
if ($this->config->getSystemValueString('overwritehost') !== '' && $this->isOverwriteCondition()) {
diff --git a/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php b/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php
index 2b5acc8b75f..c9b51f26f34 100644
--- a/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php
+++ b/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php
@@ -23,7 +23,7 @@ class MiddlewareDispatcher {
/**
* @var int counter which tells us what middleware was executed once an
- * exception occurs
+ * exception occurs
*/
private int $middlewareCounter;
@@ -84,10 +84,10 @@ class MiddlewareDispatcher {
*
* @param Controller $controller the controller that is being called
* @param string $methodName the name of the method that will be called on
- * the controller
+ * the controller
* @param \Exception $exception the thrown exception
* @return Response a Response object if the middleware can handle the
- * exception
+ * exception
* @throws \Exception the passed in exception if it can't handle it
*/
public function afterException(Controller $controller, string $methodName, \Exception $exception): Response {
@@ -109,7 +109,7 @@ class MiddlewareDispatcher {
*
* @param Controller $controller the controller that is being called
* @param string $methodName the name of the method that will be called on
- * the controller
+ * the controller
* @param Response $response the generated response from the controller
* @return Response a Response object
*/
diff --git a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php
index 3f0755b1b91..10c8f8c7aee 100644
--- a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php
+++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php
@@ -31,7 +31,7 @@ use ReflectionMethod;
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
*/
class CORSMiddleware extends Middleware {
- /** @var IRequest */
+ /** @var IRequest */
private $request;
/** @var ControllerMethodReflector */
private $reflector;
diff --git a/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php b/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php
index 2115c07c0fc..e88c9563c00 100644
--- a/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php
+++ b/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php
@@ -10,7 +10,6 @@ namespace OC\AppFramework\Middleware\Security;
use OC\Security\CSP\ContentSecurityPolicyManager;
use OC\Security\CSP\ContentSecurityPolicyNonceManager;
-use OC\Security\CSRF\CsrfTokenManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\EmptyContentSecurityPolicy;
@@ -18,19 +17,11 @@ use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Middleware;
class CSPMiddleware extends Middleware {
- /** @var ContentSecurityPolicyManager */
- private $contentSecurityPolicyManager;
- /** @var ContentSecurityPolicyNonceManager */
- private $cspNonceManager;
- /** @var CsrfTokenManager */
- private $csrfTokenManager;
-
- public function __construct(ContentSecurityPolicyManager $policyManager,
- ContentSecurityPolicyNonceManager $cspNonceManager,
- CsrfTokenManager $csrfTokenManager) {
- $this->contentSecurityPolicyManager = $policyManager;
- $this->cspNonceManager = $cspNonceManager;
- $this->csrfTokenManager = $csrfTokenManager;
+
+ public function __construct(
+ private ContentSecurityPolicyManager $policyManager,
+ private ContentSecurityPolicyNonceManager $cspNonceManager,
+ ) {
}
/**
@@ -49,11 +40,11 @@ class CSPMiddleware extends Middleware {
return $response;
}
- $defaultPolicy = $this->contentSecurityPolicyManager->getDefaultPolicy();
- $defaultPolicy = $this->contentSecurityPolicyManager->mergePolicies($defaultPolicy, $policy);
+ $defaultPolicy = $this->policyManager->getDefaultPolicy();
+ $defaultPolicy = $this->policyManager->mergePolicies($defaultPolicy, $policy);
if ($this->cspNonceManager->browserSupportsCspV3()) {
- $defaultPolicy->useJsNonce($this->csrfTokenManager->getToken()->getEncryptedValue());
+ $defaultPolicy->useJsNonce($this->cspNonceManager->getNonce());
}
$response->setContentSecurityPolicy($defaultPolicy);
diff --git a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php
index a983de23597..34933e13ecd 100644
--- a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php
+++ b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php
@@ -93,7 +93,7 @@ class PasswordConfirmationMiddleware extends Middleware {
return;
}
- $lastConfirm = (int) $this->session->get('last-password-confirm');
+ $lastConfirm = (int)$this->session->get('last-password-confirm');
// TODO: confirm excludedUserBackEnds can go away and remove it
if (!isset($this->excludedUserBackEnds[$backendClassName]) && $lastConfirm < ($this->timeFactory->getTime() - (30 * 60 + 15))) { // allow 15 seconds delay
throw new NotConfirmedException();
diff --git a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php
index 511ee3fc28a..f4d120ebc30 100644
--- a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php
+++ b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php
@@ -112,8 +112,8 @@ class RateLimitingMiddleware extends Middleware {
if ($annotationLimit !== '' && $annotationPeriod !== '') {
return new $attributeClass(
- (int) $annotationLimit,
- (int) $annotationPeriod,
+ (int)$annotationLimit,
+ (int)$annotationPeriod,
);
}
diff --git a/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php b/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php
index e0bb96f132b..efe56e0b124 100644
--- a/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php
+++ b/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php
@@ -46,13 +46,13 @@ class SameSiteCookieMiddleware extends Middleware {
public function afterException($controller, $methodName, \Exception $exception) {
if ($exception instanceof LaxSameSiteCookieFailedException) {
- $respone = new Response();
- $respone->setStatus(Http::STATUS_FOUND);
- $respone->addHeader('Location', $this->request->getRequestUri());
+ $response = new Response();
+ $response->setStatus(Http::STATUS_FOUND);
+ $response->addHeader('Location', $this->request->getRequestUri());
$this->setSameSiteCookie();
- return $respone;
+ return $response;
}
throw $exception;
diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
index b8de09072ce..88987290244 100644
--- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
+++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
@@ -36,6 +36,8 @@ use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Middleware;
use OCP\AppFramework\OCSController;
+use OCP\Group\ISubAdmin;
+use OCP\IGroupManager;
use OCP\IL10N;
use OCP\INavigationManager;
use OCP\IRequest;
@@ -53,6 +55,9 @@ use ReflectionMethod;
* check fails
*/
class SecurityMiddleware extends Middleware {
+ private ?bool $isAdminUser = null;
+ private ?bool $isSubAdmin = null;
+
public function __construct(
private IRequest $request,
private ControllerMethodReflector $reflector,
@@ -61,8 +66,8 @@ class SecurityMiddleware extends Middleware {
private LoggerInterface $logger,
private string $appName,
private bool $isLoggedIn,
- private bool $isAdminUser,
- private bool $isSubAdmin,
+ private IGroupManager $groupManager,
+ private ISubAdmin $subAdminManager,
private IAppManager $appManager,
private IL10N $l10n,
private AuthorizedGroupMapper $groupAuthorizationMapper,
@@ -71,6 +76,22 @@ class SecurityMiddleware extends Middleware {
) {
}
+ private function isAdminUser(): bool {
+ if ($this->isAdminUser === null) {
+ $user = $this->userSession->getUser();
+ $this->isAdminUser = $user && $this->groupManager->isAdmin($user->getUID());
+ }
+ return $this->isAdminUser;
+ }
+
+ private function isSubAdmin(): bool {
+ if ($this->isSubAdmin === null) {
+ $user = $this->userSession->getUser();
+ $this->isSubAdmin = $user && $this->subAdminManager->isSubAdmin($user);
+ }
+ return $this->isSubAdmin;
+ }
+
/**
* This runs all the security checks before a method call. The
* security checks are determined by inspecting the controller method
@@ -114,10 +135,10 @@ class SecurityMiddleware extends Middleware {
}
if (!$authorized && $this->hasAnnotationOrAttribute($reflectionMethod, 'AuthorizedAdminSetting', AuthorizedAdminSetting::class)) {
- $authorized = $this->isAdminUser;
+ $authorized = $this->isAdminUser();
if (!$authorized && $this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)) {
- $authorized = $this->isSubAdmin;
+ $authorized = $this->isSubAdmin();
}
if (!$authorized) {
@@ -139,14 +160,14 @@ class SecurityMiddleware extends Middleware {
}
}
if ($this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
- && !$this->isSubAdmin
- && !$this->isAdminUser
+ && !$this->isSubAdmin()
+ && !$this->isAdminUser()
&& !$authorized) {
throw new NotAdminException($this->l10n->t('Logged in account must be an admin or sub admin'));
}
if (!$this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
&& !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class)
- && !$this->isAdminUser
+ && !$this->isAdminUser()
&& !$authorized) {
throw new NotAdminException($this->l10n->t('Logged in account must be an admin'));
}
diff --git a/lib/private/AppFramework/Utility/ControllerMethodReflector.php b/lib/private/AppFramework/Utility/ControllerMethodReflector.php
index 9c08f58b384..2031327dfae 100644
--- a/lib/private/AppFramework/Utility/ControllerMethodReflector.php
+++ b/lib/private/AppFramework/Utility/ControllerMethodReflector.php
@@ -82,9 +82,9 @@ class ControllerMethodReflector implements IControllerMethodReflector {
/**
* Inspects the PHPDoc parameters for types
* @param string $parameter the parameter whose type comments should be
- * parsed
+ * parsed
* @return string|null type in the type parameters (@param int $something)
- * would return int or null if not existing
+ * would return int or null if not existing
*/
public function getType(string $parameter) {
if (array_key_exists($parameter, $this->types)) {
diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php
index bf0ef36d13c..56de4a34cf6 100644
--- a/lib/private/AppFramework/Utility/SimpleContainer.php
+++ b/lib/private/AppFramework/Utility/SimpleContainer.php
@@ -89,7 +89,7 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer {
}
// don't lose the error we got while trying to query by type
- throw new QueryException($e->getMessage(), (int) $e->getCode(), $e);
+ throw new QueryException($e->getMessage(), (int)$e->getCode(), $e);
}
}
diff --git a/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php b/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php
index d95bcd98cf9..5781c1edf16 100644
--- a/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php
+++ b/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php
@@ -45,7 +45,7 @@ class RemoteWipeNotificationsListener implements IEventListener {
$notification->setApp('auth')
->setUser($token->getUID())
->setDateTime($this->timeFactory->getDateTime())
- ->setObject('token', (string) $token->getId())
+ ->setObject('token', (string)$token->getId())
->setSubject($event, [
'name' => $token->getName(),
]);
diff --git a/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php b/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php
index 697aea71c6d..8523fb6abc7 100644
--- a/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php
+++ b/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php
@@ -39,7 +39,7 @@ class UserDeletedFilesCleanupListener implements IEventListener {
$userHome = $this->mountProviderCollection->getHomeMountForUser($event->getUser());
$storage = $userHome->getStorage();
if (!$storage) {
- throw new \Exception("Account has no home storage");
+ throw new \Exception('Account has no home storage');
}
// remove all wrappers, so we do the delete directly on the home storage bypassing any wrapper
@@ -52,7 +52,7 @@ class UserDeletedFilesCleanupListener implements IEventListener {
}
if ($event instanceof UserDeletedEvent) {
if (!isset($this->homeStorageCache[$event->getUser()->getUID()])) {
- throw new \Exception("UserDeletedEvent fired without matching BeforeUserDeletedEvent");
+ throw new \Exception('UserDeletedEvent fired without matching BeforeUserDeletedEvent');
}
$storage = $this->homeStorageCache[$event->getUser()->getUID()];
$cache = $storage->getCache();
@@ -60,7 +60,7 @@ class UserDeletedFilesCleanupListener implements IEventListener {
if ($cache instanceof Cache) {
$cache->clear();
} else {
- throw new \Exception("Home storage has invalid cache");
+ throw new \Exception('Home storage has invalid cache');
}
}
}
diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php
index 0db5c4f53e7..b1341fe1898 100644
--- a/lib/private/Authentication/Token/PublicKeyTokenMapper.php
+++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php
@@ -36,17 +36,20 @@ class PublicKeyTokenMapper extends QBMapper {
/**
* @param int $olderThan
- * @param int $remember
+ * @param int $type
+ * @param int|null $remember
*/
- public function invalidateOld(int $olderThan, int $remember = IToken::DO_NOT_REMEMBER) {
+ public function invalidateOld(int $olderThan, int $type = IToken::TEMPORARY_TOKEN, ?int $remember = null) {
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
- $qb->delete($this->tableName)
+ $delete = $qb->delete($this->tableName)
->where($qb->expr()->lt('last_activity', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT)))
- ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT)))
- ->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT)))
- ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)))
- ->execute();
+ ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
+ ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)));
+ if ($remember !== null) {
+ $delete->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT)));
+ }
+ $delete->executeStatement();
}
public function invalidateLastUsedBefore(string $uid, int $before): int {
diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php
index a3bfa3147a5..4eddd5c80f7 100644
--- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php
+++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php
@@ -162,7 +162,7 @@ class PublicKeyTokenProvider implements IProvider {
$this->rotate($token, $tokenId, $tokenId);
} catch (DoesNotExistException) {
$this->cacheInvalidHash($tokenHash);
- throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
+ throw new InvalidTokenException('Token does not exist: ' . $ex->getMessage(), 0, $ex);
}
}
@@ -232,7 +232,7 @@ class PublicKeyTokenProvider implements IProvider {
$token = $this->getToken($oldSessionId);
if (!($token instanceof PublicKeyToken)) {
- throw new InvalidTokenException("Invalid token type");
+ throw new InvalidTokenException('Invalid token type');
}
$password = null;
@@ -281,10 +281,19 @@ class PublicKeyTokenProvider implements IProvider {
public function invalidateOldTokens() {
$olderThan = $this->time->getTime() - $this->config->getSystemValueInt('session_lifetime', 60 * 60 * 24);
$this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']);
- $this->mapper->invalidateOld($olderThan, OCPIToken::DO_NOT_REMEMBER);
+ $this->mapper->invalidateOld($olderThan, OCPIToken::TEMPORARY_TOKEN, OCPIToken::DO_NOT_REMEMBER);
+
$rememberThreshold = $this->time->getTime() - $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
$this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']);
- $this->mapper->invalidateOld($rememberThreshold, OCPIToken::REMEMBER);
+ $this->mapper->invalidateOld($rememberThreshold, OCPIToken::TEMPORARY_TOKEN, OCPIToken::REMEMBER);
+
+ $wipeThreshold = $this->time->getTime() - $this->config->getSystemValueInt('token_auth_wipe_token_retention', 60 * 60 * 24 * 60);
+ $this->logger->debug('Invalidating auth tokens marked for remote wipe older than ' . date('c', $wipeThreshold), ['app' => 'cron']);
+ $this->mapper->invalidateOld($wipeThreshold, OCPIToken::WIPE_TOKEN);
+
+ $authTokenThreshold = $this->time->getTime() - $this->config->getSystemValueInt('token_auth_token_retention', 60 * 60 * 24 * 365);
+ $this->logger->debug('Invalidating auth tokens older than ' . date('c', $authTokenThreshold), ['app' => 'cron']);
+ $this->mapper->invalidateOld($authTokenThreshold, OCPIToken::PERMANENT_TOKEN);
}
public function invalidateLastUsedBefore(string $uid, int $before): void {
@@ -293,7 +302,7 @@ class PublicKeyTokenProvider implements IProvider {
public function updateToken(OCPIToken $token) {
if (!($token instanceof PublicKeyToken)) {
- throw new InvalidTokenException("Invalid token type");
+ throw new InvalidTokenException('Invalid token type');
}
$this->mapper->update($token);
$this->cacheToken($token);
@@ -301,7 +310,7 @@ class PublicKeyTokenProvider implements IProvider {
public function updateTokenActivity(OCPIToken $token) {
if (!($token instanceof PublicKeyToken)) {
- throw new InvalidTokenException("Invalid token type");
+ throw new InvalidTokenException('Invalid token type');
}
$activityInterval = $this->config->getSystemValueInt('token_auth_activity_update', 60);
@@ -322,7 +331,7 @@ class PublicKeyTokenProvider implements IProvider {
public function getPassword(OCPIToken $savedToken, string $tokenId): string {
if (!($savedToken instanceof PublicKeyToken)) {
- throw new InvalidTokenException("Invalid token type");
+ throw new InvalidTokenException('Invalid token type');
}
if ($savedToken->getPassword() === null) {
@@ -338,7 +347,7 @@ class PublicKeyTokenProvider implements IProvider {
public function setPassword(OCPIToken $token, string $tokenId, string $password) {
if (!($token instanceof PublicKeyToken)) {
- throw new InvalidTokenException("Invalid token type");
+ throw new InvalidTokenException('Invalid token type');
}
$this->atomic(function () use ($password, $token) {
@@ -363,7 +372,7 @@ class PublicKeyTokenProvider implements IProvider {
public function rotate(OCPIToken $token, string $oldTokenId, string $newTokenId): OCPIToken {
if (!($token instanceof PublicKeyToken)) {
- throw new InvalidTokenException("Invalid token type");
+ throw new InvalidTokenException('Invalid token type');
}
// Decrypt private key with oldTokenId
@@ -396,7 +405,7 @@ class PublicKeyTokenProvider implements IProvider {
} catch (\Exception $ex2) {
// Delete the invalid token
$this->invalidateToken($token);
- throw new InvalidTokenException("Could not decrypt token password: " . $ex->getMessage(), 0, $ex2);
+ throw new InvalidTokenException('Could not decrypt token password: ' . $ex->getMessage(), 0, $ex2);
}
}
}
@@ -486,7 +495,7 @@ class PublicKeyTokenProvider implements IProvider {
public function markPasswordInvalid(OCPIToken $token, string $tokenId) {
if (!($token instanceof PublicKeyToken)) {
- throw new InvalidTokenException("Invalid token type");
+ throw new InvalidTokenException('Invalid token type');
}
$token->setPasswordInvalid(true);
diff --git a/lib/private/Authentication/Token/RemoteWipe.php b/lib/private/Authentication/Token/RemoteWipe.php
index 43c2bd060d1..80ba330b66d 100644
--- a/lib/private/Authentication/Token/RemoteWipe.php
+++ b/lib/private/Authentication/Token/RemoteWipe.php
@@ -98,7 +98,7 @@ class RemoteWipe {
$dbToken = $e->getToken();
- $this->logger->info("user " . $dbToken->getUID() . " started a remote wipe");
+ $this->logger->info('user ' . $dbToken->getUID() . ' started a remote wipe');
$this->eventDispatcher->dispatch(RemoteWipeStarted::class, new RemoteWipeStarted($dbToken));
@@ -126,7 +126,7 @@ class RemoteWipe {
$this->tokenProvider->invalidateToken($token);
- $this->logger->info("user " . $dbToken->getUID() . " finished a remote wipe");
+ $this->logger->info('user ' . $dbToken->getUID() . ' finished a remote wipe');
$this->eventDispatcher->dispatch(RemoteWipeFinished::class, new RemoteWipeFinished($dbToken));
return true;
diff --git a/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php b/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php
index c84b7f1af20..cdbd8b48cf2 100644
--- a/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php
+++ b/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php
@@ -29,7 +29,7 @@ class ProviderUserAssignmentDao {
* Get all assigned provider IDs for the given user ID
*
* @return array<string, bool> where the array key is the provider ID (string) and the
- * value is the enabled state (bool)
+ * value is the enabled state (bool)
*/
public function getState(string $uid): array {
$qb = $this->conn->getQueryBuilder();
@@ -95,7 +95,7 @@ class ProviderUserAssignmentDao {
return [
'provider_id' => (string)$row['provider_id'],
'uid' => (string)$row['uid'],
- 'enabled' => ((int) $row['enabled']) === 1,
+ 'enabled' => ((int)$row['enabled']) === 1,
];
}, $rows));
}
diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php
index 2585646c998..072ffc4f86f 100644
--- a/lib/private/Authentication/TwoFactorAuth/Manager.php
+++ b/lib/private/Authentication/TwoFactorAuth/Manager.php
@@ -192,7 +192,7 @@ class Manager {
if (!empty($missing)) {
// There was at least one provider missing
- $this->logger->alert(count($missing) . " two-factor auth providers failed to load", ['app' => 'core']);
+ $this->logger->alert(count($missing) . ' two-factor auth providers failed to load', ['app' => 'core']);
return true;
}
@@ -322,7 +322,7 @@ class Manager {
$tokenId = $token->getId();
$tokensNeeding2FA = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
- if (!\in_array((string) $tokenId, $tokensNeeding2FA, true)) {
+ if (!\in_array((string)$tokenId, $tokensNeeding2FA, true)) {
$this->session->set(self::SESSION_UID_DONE, $user->getUID());
return false;
}
@@ -359,7 +359,7 @@ class Manager {
$id = $this->session->getId();
$token = $this->tokenProvider->getToken($id);
- $this->config->setUserValue($user->getUID(), 'login_token_2fa', (string) $token->getId(), (string)$this->timeFactory->getTime());
+ $this->config->setUserValue($user->getUID(), 'login_token_2fa', (string)$token->getId(), (string)$this->timeFactory->getTime());
}
public function clearTwoFactorPending(string $userId) {
diff --git a/lib/private/Authentication/WebAuthn/CredentialRepository.php b/lib/private/Authentication/WebAuthn/CredentialRepository.php
index f32136f9594..203f2ef9020 100644
--- a/lib/private/Authentication/WebAuthn/CredentialRepository.php
+++ b/lib/private/Authentication/WebAuthn/CredentialRepository.php
@@ -44,7 +44,7 @@ class CredentialRepository implements PublicKeyCredentialSourceRepository {
}, $entities);
}
- public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null): PublicKeyCredentialEntity {
+ public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null, bool $userVerification = false): PublicKeyCredentialEntity {
$oldEntity = null;
try {
@@ -58,13 +58,18 @@ class CredentialRepository implements PublicKeyCredentialSourceRepository {
$name = 'default';
}
- $entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource);
+ $entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource, $userVerification);
if ($oldEntity) {
$entity->setId($oldEntity->getId());
if ($defaultName) {
$entity->setName($oldEntity->getName());
}
+
+ // Don't downgrade UV just because it was skipped during a login due to another key
+ if ($oldEntity->getUserVerification()) {
+ $entity->setUserVerification(true);
+ }
}
return $this->credentialMapper->insertOrUpdate($entity);
diff --git a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php
index 443a7985cae..6c4bc3ca81b 100644
--- a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php
+++ b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php
@@ -23,6 +23,10 @@ use Webauthn\PublicKeyCredentialSource;
* @method void setPublicKeyCredentialId(string $id);
* @method string getData();
* @method void setData(string $data);
+ *
+ * @since 30.0.0 Add userVerification attribute
+ * @method bool|null getUserVerification();
+ * @method void setUserVerification(bool $userVerification);
*/
class PublicKeyCredentialEntity extends Entity implements JsonSerializable {
/** @var string */
@@ -37,20 +41,25 @@ class PublicKeyCredentialEntity extends Entity implements JsonSerializable {
/** @var string */
protected $data;
+ /** @var bool|null */
+ protected $userVerification;
+
public function __construct() {
$this->addType('name', 'string');
$this->addType('uid', 'string');
$this->addType('publicKeyCredentialId', 'string');
$this->addType('data', 'string');
+ $this->addType('userVerification', 'boolean');
}
- public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource): PublicKeyCredentialEntity {
+ public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource, bool $userVerification): PublicKeyCredentialEntity {
$publicKeyCredentialEntity = new self();
$publicKeyCredentialEntity->setName($name);
$publicKeyCredentialEntity->setUid($publicKeyCredentialSource->getUserHandle());
$publicKeyCredentialEntity->setPublicKeyCredentialId(base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId()));
$publicKeyCredentialEntity->setData(json_encode($publicKeyCredentialSource));
+ $publicKeyCredentialEntity->setUserVerification($userVerification);
return $publicKeyCredentialEntity;
}
diff --git a/lib/private/Authentication/WebAuthn/Manager.php b/lib/private/Authentication/WebAuthn/Manager.php
index 007be245992..7aa7a3c8f3a 100644
--- a/lib/private/Authentication/WebAuthn/Manager.php
+++ b/lib/private/Authentication/WebAuthn/Manager.php
@@ -88,8 +88,8 @@ class Manager {
];
$authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
- null,
- AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
+ AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
+ AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED,
null,
false,
);
@@ -151,7 +151,8 @@ class Manager {
}
// Persist the data
- return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name);
+ $userVerification = $response->attestationObject->authData->isUserVerified();
+ return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name, $userVerification);
}
private function stripPort(string $serverHost): string {
@@ -160,7 +161,11 @@ class Manager {
public function startAuthentication(string $uid, string $serverHost): PublicKeyCredentialRequestOptions {
// List of registered PublicKeyCredentialDescriptor classes associated to the user
- $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) {
+ $userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED;
+ $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) use (&$userVerificationRequirement) {
+ if ($entity->getUserVerification() !== true) {
+ $userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
+ }
$credential = $entity->toPublicKeyCredentialSource();
return new PublicKeyCredentialDescriptor(
$credential->type,
@@ -173,7 +178,7 @@ class Manager {
random_bytes(32), // Challenge
$this->stripPort($serverHost), // Relying Party ID
$registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes
- AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
+ $userVerificationRequirement,
60000, // Timeout
);
}
diff --git a/lib/private/Avatar/Avatar.php b/lib/private/Avatar/Avatar.php
index 1ad70001f13..bf29d57b88d 100644
--- a/lib/private/Avatar/Avatar.php
+++ b/lib/private/Avatar/Avatar.php
@@ -88,8 +88,8 @@ abstract class Avatar implements IAvatar {
$userDisplayName = $this->getDisplayName();
$fgRGB = $this->avatarBackgroundColor($userDisplayName);
$bgRGB = $fgRGB->alphaBlending(0.1, $darkTheme ? new Color(0, 0, 0) : new Color(255, 255, 255));
- $fill = sprintf("%02x%02x%02x", $bgRGB->red(), $bgRGB->green(), $bgRGB->blue());
- $fgFill = sprintf("%02x%02x%02x", $fgRGB->red(), $fgRGB->green(), $fgRGB->blue());
+ $fill = sprintf('%02x%02x%02x', $bgRGB->red(), $bgRGB->green(), $bgRGB->blue());
+ $fgFill = sprintf('%02x%02x%02x', $fgRGB->red(), $fgRGB->green(), $fgRGB->blue());
$text = $this->getAvatarText();
$toReplace = ['{size}', '{fill}', '{fgFill}', '{letter}'];
return str_replace($toReplace, [$size, $fill, $fgFill, $text], $this->svgTemplate);
@@ -104,7 +104,7 @@ abstract class Avatar implements IAvatar {
}
$formats = Imagick::queryFormats();
// Avatar generation breaks if RSVG format is enabled. Fall back to gd in that case
- if (in_array("RSVG", $formats, true)) {
+ if (in_array('RSVG', $formats, true)) {
return null;
}
try {
diff --git a/lib/private/Avatar/AvatarManager.php b/lib/private/Avatar/AvatarManager.php
index f8ce4d5b656..60a3d358bf4 100644
--- a/lib/private/Avatar/AvatarManager.php
+++ b/lib/private/Avatar/AvatarManager.php
@@ -115,7 +115,7 @@ class AvatarManager implements IAvatarManager {
$folder->delete();
} catch (NotFoundException $e) {
$this->logger->debug("No cache for the user $userId. Ignoring avatar deletion");
- } catch (NotPermittedException | StorageNotAvailableException $e) {
+ } catch (NotPermittedException|StorageNotAvailableException $e) {
$this->logger->error("Unable to delete user avatars for $userId. gnoring avatar deletion");
} catch (NoUserException $e) {
$this->logger->debug("Account $userId not found. Ignoring avatar deletion");
diff --git a/lib/private/BackgroundJob/JobList.php b/lib/private/BackgroundJob/JobList.php
index 3978ae635f7..5059199a182 100644
--- a/lib/private/BackgroundJob/JobList.php
+++ b/lib/private/BackgroundJob/JobList.php
@@ -129,7 +129,7 @@ class JobList implements IJobList {
$row = $result->fetch();
$result->closeCursor();
- return (bool) $row;
+ return (bool)$row;
}
public function getJobs($job, ?int $limit, int $offset): array {
@@ -302,8 +302,8 @@ class JobList implements IJobList {
// This most likely means an invalid job was enqueued. We can ignore it.
return null;
}
- $job->setId((int) $row['id']);
- $job->setLastRun((int) $row['last_run']);
+ $job->setId((int)$row['id']);
+ $job->setLastRun((int)$row['last_run']);
$job->setArgument(json_decode($row['argument'], true));
return $job;
} catch (AutoloadNotAllowedException $e) {
diff --git a/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php b/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php
index bb5bc3cadb2..1d0a502df6d 100644
--- a/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php
+++ b/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php
@@ -70,7 +70,7 @@ class GenerateBlurhashMetadata implements IEventListener {
}
$metadata->setString('blurhash', $this->generateBlurHash($image))
- ->setEtag('blurhash', $currentEtag);
+ ->setEtag('blurhash', $currentEtag);
}
/**
diff --git a/lib/private/Calendar/Manager.php b/lib/private/Calendar/Manager.php
index 7ae577c9d7f..fa324273f5c 100644
--- a/lib/private/Calendar/Manager.php
+++ b/lib/private/Calendar/Manager.php
@@ -53,7 +53,7 @@ class Manager implements IManager {
* @param string $pattern which should match within the $searchProperties
* @param array $searchProperties defines the properties within the query pattern should match
* @param array $options - optional parameters:
- * ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
+ * ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
* @param integer|null $limit - limit number of search results
* @param integer|null $offset - offset for paging of search results
* @return array an array of events/journals/todos which are arrays of arrays of key-value-pairs
@@ -193,6 +193,7 @@ class Manager implements IManager {
foreach ($r as $o) {
$o['calendar-key'] = $calendar->getKey();
+ $o['calendar-uri'] = $calendar->getUri();
$results[] = $o;
}
}
diff --git a/lib/private/Calendar/ResourcesRoomsUpdater.php b/lib/private/Calendar/ResourcesRoomsUpdater.php
index ae2a2f3a650..eacdaf0aeb4 100644
--- a/lib/private/Calendar/ResourcesRoomsUpdater.php
+++ b/lib/private/Calendar/ResourcesRoomsUpdater.php
@@ -406,7 +406,7 @@ class ResourcesRoomsUpdater {
->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
$result = $query->executeQuery();
- $id = (int) $result->fetchOne();
+ $id = (int)$result->fetchOne();
$result->closeCursor();
return $id;
}
diff --git a/lib/private/Collaboration/AutoComplete/Manager.php b/lib/private/Collaboration/AutoComplete/Manager.php
index d7298d9deef..382b9188535 100644
--- a/lib/private/Collaboration/AutoComplete/Manager.php
+++ b/lib/private/Collaboration/AutoComplete/Manager.php
@@ -13,7 +13,7 @@ class Manager implements IManager {
/** @var string[] */
protected array $sorters = [];
- /** @var ISorter[] */
+ /** @var ISorter[] */
protected array $sorterInstances = [];
public function __construct(
diff --git a/lib/private/Collaboration/Collaborators/UserPlugin.php b/lib/private/Collaboration/Collaborators/UserPlugin.php
index b4cb77ad5b8..d196abae042 100644
--- a/lib/private/Collaboration/Collaborators/UserPlugin.php
+++ b/lib/private/Collaboration/Collaborators/UserPlugin.php
@@ -73,7 +73,7 @@ class UserPlugin implements ISearchPlugin {
foreach ($currentUserGroups as $userGroupId) {
$usersInGroup = $this->groupManager->displayNamesInGroup($userGroupId, $search, $limit, $offset);
foreach ($usersInGroup as $userId => $displayName) {
- $userId = (string) $userId;
+ $userId = (string)$userId;
$user = $this->userManager->get($userId);
if (!$user->isEnabled()) {
// Ignore disabled users
@@ -130,7 +130,7 @@ class UserPlugin implements ISearchPlugin {
foreach ($users as $uid => $user) {
$userDisplayName = $user->getDisplayName();
$userEmail = $user->getSystemEMailAddress();
- $uid = (string) $uid;
+ $uid = (string)$uid;
$status = [];
if (array_key_exists($uid, $userStatuses)) {
diff --git a/lib/private/Collaboration/Reference/ReferenceManager.php b/lib/private/Collaboration/Reference/ReferenceManager.php
index 5a1b39d9dff..9287b66b2a2 100644
--- a/lib/private/Collaboration/Reference/ReferenceManager.php
+++ b/lib/private/Collaboration/Reference/ReferenceManager.php
@@ -231,7 +231,7 @@ class ReferenceManager implements IReferenceManager {
}
$configKey = 'provider-last-use_' . $providerId;
- $this->config->setUserValue($userId, 'references', $configKey, (string) $timestamp);
+ $this->config->setUserValue($userId, 'references', $configKey, (string)$timestamp);
return true;
}
return false;
@@ -254,7 +254,7 @@ class ReferenceManager implements IReferenceManager {
$timestamps = [];
foreach ($keys as $key) {
$providerId = substr($key, strlen($prefix));
- $timestamp = (int) $this->config->getUserValue($userId, 'references', $key);
+ $timestamp = (int)$this->config->getUserValue($userId, 'references', $key);
$timestamps[$providerId] = $timestamp;
}
return $timestamps;
diff --git a/lib/private/Collaboration/Resources/Manager.php b/lib/private/Collaboration/Resources/Manager.php
index 6c9b77d41c5..8d1e4b13287 100644
--- a/lib/private/Collaboration/Resources/Manager.php
+++ b/lib/private/Collaboration/Resources/Manager.php
@@ -53,7 +53,7 @@ class Manager implements IManager {
throw new CollectionException('Collection not found');
}
- return new Collection($this, $this->connection, (int) $row['id'], (string) $row['name']);
+ return new Collection($this, $this->connection, (int)$row['id'], (string)$row['name']);
}
/**
@@ -82,12 +82,12 @@ class Manager implements IManager {
throw new CollectionException('Collection not found');
}
- $access = $row['access'] === null ? null : (bool) $row['access'];
+ $access = $row['access'] === null ? null : (bool)$row['access'];
if ($user instanceof IUser) {
- return new Collection($this, $this->connection, (int) $row['id'], (string) $row['name'], $user, $access);
+ return new Collection($this, $this->connection, (int)$row['id'], (string)$row['name'], $user, $access);
}
- return new Collection($this, $this->connection, (int) $row['id'], (string) $row['name'], $user, $access);
+ return new Collection($this, $this->connection, (int)$row['id'], (string)$row['name'], $user, $access);
}
/**
@@ -122,7 +122,7 @@ class Manager implements IManager {
$foundResults = 0;
while ($row = $result->fetch()) {
$foundResults++;
- $access = $row['access'] === null ? null : (bool) $row['access'];
+ $access = $row['access'] === null ? null : (bool)$row['access'];
$collection = new Collection($this, $this->connection, (int)$row['id'], (string)$row['name'], $user, $access);
if ($collection->canAccess($user)) {
$collections[] = $collection;
@@ -186,7 +186,7 @@ class Manager implements IManager {
throw new ResourceException('Resource not found');
}
- $access = $row['access'] === null ? null : (bool) $row['access'];
+ $access = $row['access'] === null ? null : (bool)$row['access'];
if ($user instanceof IUser) {
return new Resource($this, $this->connection, $type, $id, $user, $access);
}
@@ -217,7 +217,7 @@ class Manager implements IManager {
$resources = [];
$result = $query->execute();
while ($row = $result->fetch()) {
- $access = $row['access'] === null ? null : (bool) $row['access'];
+ $access = $row['access'] === null ? null : (bool)$row['access'];
$resources[] = new Resource($this, $this->connection, $row['resource_type'], $row['resource_id'], $user, $access);
}
$result->closeCursor();
@@ -311,7 +311,7 @@ class Manager implements IManager {
$hasAccess = null;
$result = $query->execute();
if ($row = $result->fetch()) {
- $hasAccess = (bool) $row['access'];
+ $hasAccess = (bool)$row['access'];
}
$result->closeCursor();
@@ -331,7 +331,7 @@ class Manager implements IManager {
$hasAccess = null;
$result = $query->execute();
if ($row = $result->fetch()) {
- $hasAccess = (bool) $row['access'];
+ $hasAccess = (bool)$row['access'];
}
$result->closeCursor();
diff --git a/lib/private/Collaboration/Resources/Resource.php b/lib/private/Collaboration/Resources/Resource.php
index ae011e319de..34f68aeee11 100644
--- a/lib/private/Collaboration/Resources/Resource.php
+++ b/lib/private/Collaboration/Resources/Resource.php
@@ -104,7 +104,7 @@ class Resource implements IResource {
$result = $query->execute();
while ($row = $result->fetch()) {
- $collections[] = $this->manager->getCollection((int) $row['collection_id']);
+ $collections[] = $this->manager->getCollection((int)$row['collection_id']);
}
$result->closeCursor();
diff --git a/lib/private/Comments/Comment.php b/lib/private/Comments/Comment.php
index 422e29c084d..3c44c02fe2e 100644
--- a/lib/private/Comments/Comment.php
+++ b/lib/private/Comments/Comment.php
@@ -34,8 +34,8 @@ class Comment implements IComment {
/**
* Comment constructor.
*
- * @param array $data optional, array with keys according to column names from
- * the comments database scheme
+ * @param array $data optional, array with keys according to column names from
+ * the comments database scheme
*/
public function __construct(?array $data = null) {
if (is_array($data)) {
diff --git a/lib/private/Comments/Manager.php b/lib/private/Comments/Manager.php
index 21ff5322f23..9237c884691 100644
--- a/lib/private/Comments/Manager.php
+++ b/lib/private/Comments/Manager.php
@@ -33,10 +33,10 @@ class Manager implements ICommentsManager {
/** @var IComment[] */
protected array $commentsCache = [];
- /** @var \Closure[] */
+ /** @var \Closure[] */
protected array $eventHandlerClosures = [];
- /** @var ICommentsEventHandler[] */
+ /** @var ICommentsEventHandler[] */
protected array $eventHandlers = [];
/** @var \Closure[] */
@@ -308,10 +308,10 @@ class Manager implements ICommentsManager {
* @param string $objectType the object type, e.g. 'files'
* @param string $objectId the id of the object
* @param int $limit optional, number of maximum comments to be returned. if
- * not specified, all comments are returned.
+ * not specified, all comments are returned.
* @param int $offset optional, starting point
* @param \DateTime $notOlderThan optional, timestamp of the oldest comments
- * that may be returned
+ * that may be returned
* @return list<IComment>
* @since 9.0.0
*/
@@ -362,7 +362,7 @@ class Manager implements ICommentsManager {
* @param int $lastKnownCommentId the last known comment (will be used as offset)
* @param string $sortDirection direction of the comments (`asc` or `desc`)
* @param int $limit optional, number of maximum comments to be returned. if
- * set to 0, all comments are returned.
+ * set to 0, all comments are returned.
* @param bool $includeLastKnown
* @return list<IComment>
*/
@@ -392,7 +392,7 @@ class Manager implements ICommentsManager {
* @param int $lastKnownCommentId the last known comment (will be used as offset)
* @param string $sortDirection direction of the comments (`asc` or `desc`)
* @param int $limit optional, number of maximum comments to be returned. if
- * set to 0, all comments are returned.
+ * set to 0, all comments are returned.
* @param bool $includeLastKnown
* @return list<IComment>
*/
@@ -608,7 +608,7 @@ class Manager implements ICommentsManager {
* @param $objectType string the object type, e.g. 'files'
* @param $objectId string the id of the object
* @param \DateTime $notOlderThan optional, timestamp of the oldest comments
- * that may be returned
+ * that may be returned
* @param string $verb Limit the verb of the comment - Added in 14.0.0
* @return Int
* @since 9.0.0
@@ -675,7 +675,7 @@ class Manager implements ICommentsManager {
$result = $query->executeQuery();
while ($row = $result->fetch()) {
- $unreadComments[$row['object_id']] = (int) $row['num_comments'];
+ $unreadComments[$row['object_id']] = (int)$row['num_comments'];
}
$result->closeCursor();
}
@@ -723,7 +723,7 @@ class Manager implements ICommentsManager {
$data = $result->fetch();
$result->closeCursor();
- return (int) ($data['num_messages'] ?? 0);
+ return (int)($data['num_messages'] ?? 0);
}
/**
@@ -751,7 +751,7 @@ class Manager implements ICommentsManager {
$data = $result->fetch();
$result->closeCursor();
- return (int) ($data['id'] ?? 0);
+ return (int)($data['id'] ?? 0);
}
/**
@@ -808,9 +808,9 @@ class Manager implements ICommentsManager {
return [];
}
$children = $directory->getDirectoryListing();
- $ids = array_map(fn (FileInfo $child) => (string) $child->getId(), $children);
+ $ids = array_map(fn (FileInfo $child) => (string)$child->getId(), $children);
- $ids[] = (string) $directory->getId();
+ $ids[] = (string)$directory->getId();
$counts = $this->getNumberOfUnreadCommentsForObjects('files', $ids, $user);
return array_filter($counts, function (int $count) {
return $count > 0;
@@ -1079,7 +1079,7 @@ class Manager implements ICommentsManager {
$result = $this->update($comment);
}
- if ($result && !!$comment->getParentId()) {
+ if ($result && (bool)$comment->getParentId()) {
$this->updateChildrenInformation(
$comment->getParentId(),
$comment->getCreationDateTime()
@@ -1141,7 +1141,7 @@ class Manager implements ICommentsManager {
->andWhere($qb->expr()->eq('actor_id', $qb->createNamedParameter($reaction->getActorId())))
->andWhere($qb->expr()->eq('reaction', $qb->createNamedParameter($reaction->getMessage())));
$result = $qb->executeQuery();
- $exists = (int) $result->fetchOne();
+ $exists = (int)$result->fetchOne();
if (!$exists) {
$qb = $this->dbConn->getQueryBuilder();
try {
diff --git a/lib/private/Config.php b/lib/private/Config.php
index ee30b8efc5e..6823ab7027f 100644
--- a/lib/private/Config.php
+++ b/lib/private/Config.php
@@ -184,7 +184,7 @@ class Config {
// Invalidate opcache (only if the timestamp changed)
if (function_exists('opcache_invalidate')) {
- opcache_invalidate($file, false);
+ @opcache_invalidate($file, false);
}
$filePointer = @fopen($file, 'r');
@@ -268,7 +268,7 @@ class Config {
$df = disk_free_space($this->configDir);
$size = strlen($content) + 10240;
if ($df !== false && $df < (float)$size) {
- throw new \Exception($this->configDir . " does not have enough space for writing the config file! Not writing it back!");
+ throw new \Exception($this->configDir . ' does not have enough space for writing the config file! Not writing it back!');
}
}
diff --git a/lib/private/Console/Application.php b/lib/private/Console/Application.php
index 14baa528940..16ed8894386 100644
--- a/lib/private/Console/Application.php
+++ b/lib/private/Console/Application.php
@@ -94,7 +94,7 @@ class Application {
try {
$this->loadCommandsFromInfoXml($info['commands']);
} catch (\Throwable $e) {
- $output->writeln("<error>" . $e->getMessage() . "</error>");
+ $output->writeln('<error>' . $e->getMessage() . '</error>');
$this->logger->error($e->getMessage(), [
'exception' => $e,
]);
@@ -116,13 +116,13 @@ class Application {
}
} elseif ($input->getArgument('command') !== '_completion' && $input->getArgument('command') !== 'maintenance:install') {
$errorOutput = $output->getErrorOutput();
- $errorOutput->writeln("Nextcloud is not installed - only a limited number of commands are available");
+ $errorOutput->writeln('Nextcloud is not installed - only a limited number of commands are available');
}
} catch (NeedsUpdateException $e) {
if ($input->getArgument('command') !== '_completion') {
$errorOutput = $output->getErrorOutput();
- $errorOutput->writeln("Nextcloud or one of the apps require upgrade - only a limited number of commands are available");
- $errorOutput->writeln("You may use your browser or the occ upgrade command to do the upgrade");
+ $errorOutput->writeln('Nextcloud or one of the apps require upgrade - only a limited number of commands are available');
+ $errorOutput->writeln('You may use your browser or the occ upgrade command to do the upgrade');
}
}
@@ -134,7 +134,7 @@ class Application {
$output->writeln((string)$error['hint']);
$output->writeln('');
}
- throw new \Exception("Environment not properly prepared.");
+ throw new \Exception('Environment not properly prepared.');
}
}
}
@@ -145,7 +145,7 @@ class Application {
*
* @param InputInterface $input The input implementation for reading inputs.
* @param ConsoleOutputInterface $output The output implementation
- * for writing outputs.
+ * for writing outputs.
* @return void
*/
private function writeMaintenanceModeInfo(InputInterface $input, ConsoleOutputInterface $output): void {
diff --git a/lib/private/Console/TimestampFormatter.php b/lib/private/Console/TimestampFormatter.php
index de0675cc5df..da1b7ba48dd 100644
--- a/lib/private/Console/TimestampFormatter.php
+++ b/lib/private/Console/TimestampFormatter.php
@@ -81,7 +81,7 @@ class TimestampFormatter implements OutputFormatterInterface {
*
* @param string|null $message The message to style
* @return string|null The styled message, prepended with a timestamp using the
- * log timezone and dateformat, e.g. "2015-06-23T17:24:37+02:00"
+ * log timezone and dateformat, e.g. "2015-06-23T17:24:37+02:00"
*/
public function format(?string $message): ?string {
if (!$this->formatter->isDecorated()) {
diff --git a/lib/private/Contacts/ContactsMenu/ContactsStore.php b/lib/private/Contacts/ContactsMenu/ContactsStore.php
index d7cdb5efebf..d15e6e35706 100644
--- a/lib/private/Contacts/ContactsMenu/ContactsStore.php
+++ b/lib/private/Contacts/ContactsMenu/ContactsStore.php
@@ -348,7 +348,7 @@ class ContactsStore implements IContactsStore {
$entry->setFullName($contact['FN']);
}
- $avatarPrefix = "VALUE=uri:";
+ $avatarPrefix = 'VALUE=uri:';
if (!empty($contact['PHOTO']) && str_starts_with($contact['PHOTO'], $avatarPrefix)) {
$entry->setAvatar(substr($contact['PHOTO'], strlen($avatarPrefix)));
}
diff --git a/lib/private/ContactsManager.php b/lib/private/ContactsManager.php
index f67cb196eef..7dd2bf33124 100644
--- a/lib/private/ContactsManager.php
+++ b/lib/private/ContactsManager.php
@@ -19,14 +19,14 @@ class ContactsManager implements IManager {
* @param string $pattern which should match within the $searchProperties
* @param array $searchProperties defines the properties within the query pattern should match
* @param array $options = array() to define the search behavior
- * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array
- * example: ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['type => 'HOME', 'value' => 'g@h.i']]
- * - 'escape_like_param' - If set to false wildcards _ and % are not escaped
- * - 'limit' - Set a numeric limit for the search results
- * - 'offset' - Set the offset for the limited search results
- * - 'enumeration' - (since 23.0.0) Whether user enumeration on system address book is allowed
- * - 'fullmatch' - (since 23.0.0) Whether matching on full detail in system address book is allowed
- * - 'strict_search' - (since 23.0.0) Whether the search pattern is full string or partial search
+ * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array
+ * example: ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['type => 'HOME', 'value' => 'g@h.i']]
+ * - 'escape_like_param' - If set to false wildcards _ and % are not escaped
+ * - 'limit' - Set a numeric limit for the search results
+ * - 'offset' - Set the offset for the limited search results
+ * - 'enumeration' - (since 23.0.0) Whether user enumeration on system address book is allowed
+ * - 'fullmatch' - (since 23.0.0) Whether matching on full detail in system address book is allowed
+ * - 'strict_search' - (since 23.0.0) Whether the search pattern is full string or partial search
* @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, enumeration?: bool, fullmatch?: bool, strict_search?: bool} $options
* @return array an array of contacts which are arrays of key-value-pairs
*/
diff --git a/lib/private/DB/Adapter.php b/lib/private/DB/Adapter.php
index b5be14e5dc6..71824bda9e8 100644
--- a/lib/private/DB/Adapter.php
+++ b/lib/private/DB/Adapter.php
@@ -32,7 +32,7 @@ class Adapter {
* @throws Exception
*/
public function lastInsertId($table) {
- return (int) $this->conn->realLastInsertId($table);
+ return (int)$this->conn->realLastInsertId($table);
}
/**
@@ -46,11 +46,10 @@ class Adapter {
/**
* Create an exclusive read+write lock on a table
*
- * @param string $tableName
* @throws Exception
* @since 9.1.0
*/
- public function lockTable($tableName) {
+ public function lockTable(string $tableName) {
$this->conn->beginTransaction();
$this->conn->executeUpdate('LOCK TABLE `' .$tableName . '` IN EXCLUSIVE MODE');
}
@@ -73,19 +72,21 @@ class Adapter {
* @param string $table The table name (will replace *PREFIX* with the actual prefix)
* @param array $input data that should be inserted into the table (column name => value)
* @param array|null $compare List of values that should be checked for "if not exists"
- * If this is null or an empty array, all keys of $input will be compared
- * Please note: text fields (clob) must not be used in the compare array
+ * If this is null or an empty array, all keys of $input will be compared
+ * Please note: text fields (clob) must not be used in the compare array
* @return int number of inserted rows
* @throws Exception
* @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
*/
public function insertIfNotExist($table, $input, ?array $compare = null) {
- if (empty($compare)) {
- $compare = array_keys($input);
- }
- $query = 'INSERT INTO `' .$table . '` (`'
- . implode('`,`', array_keys($input)) . '`) SELECT '
- . str_repeat('?,', count($input) - 1).'? ' // Is there a prettier alternative?
+ $compare = $compare ?: array_keys($input);
+
+ // Prepare column names and generate placeholders
+ $columns = '`' . implode('`,`', array_keys($input)) . '`';
+ $placeholders = implode(', ', array_fill(0, count($input), '?'));
+
+ $query = 'INSERT INTO `' . $table . '` (' . $columns . ') '
+ . 'SELECT ' . $placeholders . ' '
. 'FROM `' . $table . '` WHERE ';
$inserts = array_values($input);
@@ -104,10 +105,9 @@ class Adapter {
try {
return $this->conn->executeUpdate($query, $inserts);
} catch (UniqueConstraintViolationException $e) {
- // if this is thrown then a concurrent insert happened between the insert and the sub-select in the insert, that should have avoided it
- // it's fine to ignore this then
- //
- // more discussions about this can be found at https://github.com/nextcloud/server/pull/12315
+ // This exception indicates a concurrent insert happened between
+ // the insert and the sub-select in the insert, which is safe to ignore.
+ // More details: https://github.com/nextcloud/server/pull/12315
return 0;
}
}
diff --git a/lib/private/DB/AdapterSqlite.php b/lib/private/DB/AdapterSqlite.php
index 24274cbcda6..0023ee15364 100644
--- a/lib/private/DB/AdapterSqlite.php
+++ b/lib/private/DB/AdapterSqlite.php
@@ -38,8 +38,8 @@ class AdapterSqlite extends Adapter {
* @param string $table The table name (will replace *PREFIX* with the actual prefix)
* @param array $input data that should be inserted into the table (column name => value)
* @param array|null $compare List of values that should be checked for "if not exists"
- * If this is null or an empty array, all keys of $input will be compared
- * Please note: text fields (clob) must not be used in the compare array
+ * If this is null or an empty array, all keys of $input will be compared
+ * Please note: text fields (clob) must not be used in the compare array
* @return int number of inserted rows
* @throws \Doctrine\DBAL\Exception
* @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
diff --git a/lib/private/DB/ArrayResult.php b/lib/private/DB/ArrayResult.php
new file mode 100644
index 00000000000..b567ad23d57
--- /dev/null
+++ b/lib/private/DB/ArrayResult.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OC\DB;
+
+use OCP\DB\IResult;
+use PDO;
+
+/**
+ * Wrap an array or rows into a result interface
+ */
+class ArrayResult implements IResult {
+ protected int $count;
+
+ public function __construct(
+ protected array $rows,
+ ) {
+ $this->count = count($this->rows);
+ }
+
+ public function closeCursor(): bool {
+ // noop
+ return true;
+ }
+
+ public function fetch(int $fetchMode = PDO::FETCH_ASSOC) {
+ $row = array_shift($this->rows);
+ if (!$row) {
+ return false;
+ }
+ return match ($fetchMode) {
+ PDO::FETCH_ASSOC => $row,
+ PDO::FETCH_NUM => array_values($row),
+ PDO::FETCH_COLUMN => current($row),
+ default => throw new \InvalidArgumentException('Fetch mode not supported for array result'),
+ };
+
+ }
+
+ public function fetchAll(int $fetchMode = PDO::FETCH_ASSOC): array {
+ return match ($fetchMode) {
+ PDO::FETCH_ASSOC => $this->rows,
+ PDO::FETCH_NUM => array_map(function ($row) {
+ return array_values($row);
+ }, $this->rows),
+ PDO::FETCH_COLUMN => array_map(function ($row) {
+ return current($row);
+ }, $this->rows),
+ default => throw new \InvalidArgumentException('Fetch mode not supported for array result'),
+ };
+ }
+
+ public function fetchColumn() {
+ return $this->fetchOne();
+ }
+
+ public function fetchOne() {
+ $row = $this->fetch();
+ if ($row) {
+ return current($row);
+ } else {
+ return false;
+ }
+ }
+
+ public function rowCount(): int {
+ return $this->count;
+ }
+}
diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php
index 3cdd5fd06c0..ecc6d09bc95 100644
--- a/lib/private/DB/Connection.php
+++ b/lib/private/DB/Connection.php
@@ -23,11 +23,21 @@ use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Statement;
+use OC\DB\QueryBuilder\Partitioned\PartitionedQueryBuilder;
+use OC\DB\QueryBuilder\Partitioned\PartitionSplit;
use OC\DB\QueryBuilder\QueryBuilder;
+use OC\DB\QueryBuilder\Sharded\AutoIncrementHandler;
+use OC\DB\QueryBuilder\Sharded\CrossShardMoveHelper;
+use OC\DB\QueryBuilder\Sharded\RoundRobinShardMapper;
+use OC\DB\QueryBuilder\Sharded\ShardConnectionManager;
+use OC\DB\QueryBuilder\Sharded\ShardDefinition;
use OC\SystemConfig;
use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\DB\QueryBuilder\Sharded\IShardMapper;
use OCP\Diagnostics\IEventLogger;
+use OCP\ICacheFactory;
use OCP\IDBConnection;
+use OCP\ILogger;
use OCP\IRequestId;
use OCP\PreConditionNotMetException;
use OCP\Profiler\IProfiler;
@@ -75,6 +85,29 @@ class Connection extends PrimaryReadReplicaConnection {
protected bool $logRequestId;
protected string $requestId;
+ /** @var array<string, list<string>> */
+ protected array $partitions;
+ /** @var ShardDefinition[] */
+ protected array $shards = [];
+ protected ShardConnectionManager $shardConnectionManager;
+ protected AutoIncrementHandler $autoIncrementHandler;
+ protected bool $isShardingEnabled;
+
+ public const SHARD_PRESETS = [
+ 'filecache' => [
+ 'companion_keys' => [
+ 'file_id',
+ ],
+ 'companion_tables' => [
+ 'filecache_extended',
+ 'files_metadata',
+ ],
+ 'primary_key' => 'fileid',
+ 'shard_key' => 'storage',
+ 'table' => 'filecache',
+ ],
+ ];
+
/**
* Initializes a new instance of the Connection class.
*
@@ -98,7 +131,17 @@ class Connection extends PrimaryReadReplicaConnection {
parent::__construct($params, $driver, $config, $eventManager);
$this->adapter = new $params['adapter']($this);
$this->tablePrefix = $params['tablePrefix'];
-
+ $this->isShardingEnabled = isset($this->params['sharding']) && !empty($this->params['sharding']);
+
+ if ($this->isShardingEnabled) {
+ /** @psalm-suppress InvalidArrayOffset */
+ $this->shardConnectionManager = $this->params['shard_connection_manager'] ?? Server::get(ShardConnectionManager::class);
+ /** @psalm-suppress InvalidArrayOffset */
+ $this->autoIncrementHandler = $this->params['auto_increment_handler'] ?? new AutoIncrementHandler(
+ Server::get(ICacheFactory::class),
+ $this->shardConnectionManager,
+ );
+ }
$this->systemConfig = \OC::$server->getSystemConfig();
$this->clock = Server::get(ClockInterface::class);
$this->logger = Server::get(LoggerInterface::class);
@@ -117,10 +160,54 @@ class Connection extends PrimaryReadReplicaConnection {
$this->_config->setSQLLogger($debugStack);
}
+ /** @var array<string, array{shards: array[], mapper: ?string}> $shardConfig */
+ $shardConfig = $this->params['sharding'] ?? [];
+ $shardNames = array_keys($shardConfig);
+ $this->shards = array_map(function (array $config, string $name) {
+ if (!isset(self::SHARD_PRESETS[$name])) {
+ throw new \Exception("Shard preset $name not found");
+ }
+
+ $shardMapperClass = $config['mapper'] ?? RoundRobinShardMapper::class;
+ $shardMapper = Server::get($shardMapperClass);
+ if (!$shardMapper instanceof IShardMapper) {
+ throw new \Exception("Invalid shard mapper: $shardMapperClass");
+ }
+ return new ShardDefinition(
+ self::SHARD_PRESETS[$name]['table'],
+ self::SHARD_PRESETS[$name]['primary_key'],
+ self::SHARD_PRESETS[$name]['companion_keys'],
+ self::SHARD_PRESETS[$name]['shard_key'],
+ $shardMapper,
+ self::SHARD_PRESETS[$name]['companion_tables'],
+ $config['shards']
+ );
+ }, $shardConfig, $shardNames);
+ $this->shards = array_combine($shardNames, $this->shards);
+ $this->partitions = array_map(function (ShardDefinition $shard) {
+ return array_merge([$shard->table], $shard->companionTables);
+ }, $this->shards);
+
$this->setNestTransactionsWithSavepoints(true);
}
/**
+ * @return IDBConnection[]
+ */
+ public function getShardConnections(): array {
+ $connections = [];
+ if ($this->isShardingEnabled) {
+ foreach ($this->shards as $shardDefinition) {
+ foreach ($shardDefinition->getAllShards() as $shard) {
+ /** @var ConnectionAdapter $connection */
+ $connections[] = $this->shardConnectionManager->getConnection($shardDefinition, $shard);
+ }
+ }
+ }
+ return $connections;
+ }
+
+ /**
* @throws Exception
*/
public function connect($connectionName = null) {
@@ -168,11 +255,27 @@ class Connection extends PrimaryReadReplicaConnection {
*/
public function getQueryBuilder(): IQueryBuilder {
$this->queriesBuilt++;
- return new QueryBuilder(
+
+ $builder = new QueryBuilder(
new ConnectionAdapter($this),
$this->systemConfig,
$this->logger
);
+ if ($this->isShardingEnabled && count($this->partitions) > 0) {
+ $builder = new PartitionedQueryBuilder(
+ $builder,
+ $this->shards,
+ $this->shardConnectionManager,
+ $this->autoIncrementHandler,
+ );
+ foreach ($this->partitions as $name => $tables) {
+ $partition = new PartitionSplit($name, $tables);
+ $builder->addPartition($partition);
+ }
+ return $builder;
+ } else {
+ return $builder;
+ }
}
/**
@@ -239,10 +342,10 @@ class Connection extends PrimaryReadReplicaConnection {
if ($limit === -1 || $limit === null) {
$limit = null;
} else {
- $limit = (int) $limit;
+ $limit = (int)$limit;
}
if ($offset !== null) {
- $offset = (int) $offset;
+ $offset = (int)$offset;
}
if (!is_null($limit)) {
$platform = $this->getDatabasePlatform();
@@ -259,10 +362,10 @@ class Connection extends PrimaryReadReplicaConnection {
* If the query is parametrized, a prepared statement is used.
* If an SQLLogger is configured, the execution is logged.
*
- * @param string $sql The SQL query to execute.
- * @param array $params The parameters to bind to the query, if any.
- * @param array $types The types the previous parameters are in.
- * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp The query cache profile, optional.
+ * @param string $sql The SQL query to execute.
+ * @param array $params The parameters to bind to the query, if any.
+ * @param array $types The types the previous parameters are in.
+ * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp The query cache profile, optional.
*
* @return Result The executed statement.
*
@@ -291,7 +394,7 @@ class Connection extends PrimaryReadReplicaConnection {
// Read to a table that has been written to previously
// While this might not necessarily mean that we did a read after write it is an indication for a code path to check
$this->logger->log(
- (int) ($this->systemConfig->getValue('loglevel_dirty_database_queries', null) ?? 0),
+ (int)($this->systemConfig->getValue('loglevel_dirty_database_queries', null) ?? 0),
'dirty table reads: ' . $sql,
[
'tables' => array_keys($this->tableDirtyWrites),
@@ -339,9 +442,9 @@ class Connection extends PrimaryReadReplicaConnection {
*
* This method supports PDO binding types as well as DBAL mapping types.
*
- * @param string $sql The SQL query.
- * @param array $params The query parameters.
- * @param array $types The parameter types.
+ * @param string $sql The SQL query.
+ * @param array $params The query parameters.
+ * @param array $types The parameter types.
*
* @return int The number of affected rows.
*
@@ -426,8 +529,8 @@ class Connection extends PrimaryReadReplicaConnection {
* @param string $table The table name (will replace *PREFIX* with the actual prefix)
* @param array $input data that should be inserted into the table (column name => value)
* @param array|null $compare List of values that should be checked for "if not exists"
- * If this is null or an empty array, all keys of $input will be compared
- * Please note: text fields (clob) must not be used in the compare array
+ * If this is null or an empty array, all keys of $input will be compared
+ * Please note: text fields (clob) must not be used in the compare array
* @return int number of inserted rows
* @throws \Doctrine\DBAL\Exception
* @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
@@ -609,7 +712,7 @@ class Connection extends PrimaryReadReplicaConnection {
$statement = $this->replaceTablePrefix($statement);
$statement = $this->adapter->fixupStatement($statement);
if ($this->logRequestId) {
- return $statement . " /* reqid: " . $this->requestId . " */";
+ return $statement . ' /* reqid: ' . $this->requestId . ' */';
} else {
return $statement;
}
@@ -686,6 +789,9 @@ class Connection extends PrimaryReadReplicaConnection {
return $migrator->generateChangeScript($toSchema);
} else {
$migrator->migrate($toSchema);
+ foreach ($this->getShardConnections() as $shardConnection) {
+ $shardConnection->migrateToSchema($toSchema);
+ }
}
}
@@ -719,7 +825,20 @@ class Connection extends PrimaryReadReplicaConnection {
$this->transactionBacktrace = null;
$this->transactionActiveSince = null;
if ($timeTook > 1) {
- $this->logger->debug('Transaction took ' . $timeTook . 's', ['exception' => new \Exception('Transaction took ' . $timeTook . 's')]);
+ $logLevel = match (true) {
+ $timeTook > 20 * 60 => ILogger::ERROR,
+ $timeTook > 5 * 60 => ILogger::WARN,
+ $timeTook > 10 => ILogger::INFO,
+ default => ILogger::DEBUG,
+ };
+ $this->logger->log(
+ $logLevel,
+ 'Transaction took ' . $timeTook . 's',
+ [
+ 'exception' => new \Exception('Transaction took ' . $timeTook . 's'),
+ 'timeSpent' => $timeTook,
+ ]
+ );
}
}
return $result;
@@ -732,7 +851,20 @@ class Connection extends PrimaryReadReplicaConnection {
$this->transactionBacktrace = null;
$this->transactionActiveSince = null;
if ($timeTook > 1) {
- $this->logger->debug('Transaction rollback took longer than 1s: ' . $timeTook, ['exception' => new \Exception('Long running transaction rollback')]);
+ $logLevel = match (true) {
+ $timeTook > 20 * 60 => ILogger::ERROR,
+ $timeTook > 5 * 60 => ILogger::WARN,
+ $timeTook > 10 => ILogger::INFO,
+ default => ILogger::DEBUG,
+ };
+ $this->logger->log(
+ $logLevel,
+ 'Transaction rollback took longer than 1s: ' . $timeTook,
+ [
+ 'exception' => new \Exception('Long running transaction rollback'),
+ 'timeSpent' => $timeTook,
+ ]
+ );
}
}
return $result;
@@ -802,4 +934,12 @@ class Connection extends PrimaryReadReplicaConnection {
}
}
}
+
+ public function getShardDefinition(string $name): ?ShardDefinition {
+ return $this->shards[$name] ?? null;
+ }
+
+ public function getCrossShardMoveHelper(): CrossShardMoveHelper {
+ return new CrossShardMoveHelper($this->shardConnectionManager);
+ }
}
diff --git a/lib/private/DB/ConnectionAdapter.php b/lib/private/DB/ConnectionAdapter.php
index 88083711195..2baeda9cfb7 100644
--- a/lib/private/DB/ConnectionAdapter.php
+++ b/lib/private/DB/ConnectionAdapter.php
@@ -12,6 +12,8 @@ use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Schema;
use OC\DB\Exceptions\DbalException;
+use OC\DB\QueryBuilder\Sharded\CrossShardMoveHelper;
+use OC\DB\QueryBuilder\Sharded\ShardDefinition;
use OCP\DB\IPreparedStatement;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -244,4 +246,12 @@ class ConnectionAdapter implements IDBConnection {
public function logDatabaseException(\Exception $exception) {
$this->inner->logDatabaseException($exception);
}
+
+ public function getShardDefinition(string $name): ?ShardDefinition {
+ return $this->inner->getShardDefinition($name);
+ }
+
+ public function getCrossShardMoveHelper(): CrossShardMoveHelper {
+ return $this->inner->getCrossShardMoveHelper();
+ }
}
diff --git a/lib/private/DB/ConnectionFactory.php b/lib/private/DB/ConnectionFactory.php
index dd041f1e41d..8d662b0508c 100644
--- a/lib/private/DB/ConnectionFactory.php
+++ b/lib/private/DB/ConnectionFactory.php
@@ -11,7 +11,11 @@ use Doctrine\Common\EventManager;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Event\Listeners\OracleSessionInit;
+use OC\DB\QueryBuilder\Sharded\AutoIncrementHandler;
+use OC\DB\QueryBuilder\Sharded\ShardConnectionManager;
use OC\SystemConfig;
+use OCP\ICacheFactory;
+use OCP\Server;
/**
* Takes care of creating and configuring Doctrine connections.
@@ -54,9 +58,12 @@ class ConnectionFactory {
],
];
+ private ShardConnectionManager $shardConnectionManager;
+ private ICacheFactory $cacheFactory;
public function __construct(
- private SystemConfig $config
+ private SystemConfig $config,
+ ?ICacheFactory $cacheFactory = null,
) {
if ($this->config->getValue('mysql.utf8mb4', false)) {
$this->defaultConnectionParams['mysql']['charset'] = 'utf8mb4';
@@ -65,6 +72,8 @@ class ConnectionFactory {
if ($collationOverride) {
$this->defaultConnectionParams['mysql']['collation'] = $collationOverride;
}
+ $this->shardConnectionManager = new ShardConnectionManager($this->config, $this);
+ $this->cacheFactory = $cacheFactory ?? Server::get(ICacheFactory::class);
}
/**
@@ -124,7 +133,7 @@ class ConnectionFactory {
if ($host === '') {
$connectionParams['dbname'] = $dbName; // use dbname as easy connect name
} else {
- $connectionParams['dbname'] = '//' . $host . (!empty($port) ? ":{$port}" : "") . '/' . $dbName;
+ $connectionParams['dbname'] = '//' . $host . (!empty($port) ? ":{$port}" : '') . '/' . $dbName;
}
unset($connectionParams['host']);
break;
@@ -180,7 +189,7 @@ class ConnectionFactory {
$name = $this->config->getValue($configPrefix . 'dbname', $this->config->getValue('dbname', self::DEFAULT_DBNAME));
if ($this->normalizeType($type) === 'sqlite3') {
- $dataDir = $this->config->getValue("datadirectory", \OC::$SERVERROOT . '/data');
+ $dataDir = $this->config->getValue('datadirectory', \OC::$SERVERROOT . '/data');
$connectionParams['path'] = $dataDir . '/' . $name . '.db';
} else {
$host = $this->config->getValue($configPrefix . 'dbhost', $this->config->getValue('dbhost', ''));
@@ -214,6 +223,19 @@ class ConnectionFactory {
if ($this->config->getValue('dbpersistent', false)) {
$connectionParams['persistent'] = true;
}
+
+ $connectionParams['sharding'] = $this->config->getValue('dbsharding', []);
+ if (!empty($connectionParams['sharding'])) {
+ $connectionParams['shard_connection_manager'] = $this->shardConnectionManager;
+ $connectionParams['auto_increment_handler'] = new AutoIncrementHandler(
+ $this->cacheFactory,
+ $this->shardConnectionManager,
+ );
+ } else {
+ // just in case only the presence could lead to funny behaviour
+ unset($connectionParams['sharding']);
+ }
+
$connectionParams = array_merge($connectionParams, $additionalConnectionParams);
$replica = $this->config->getValue($configPrefix . 'dbreplica', $this->config->getValue('dbreplica', [])) ?: [$connectionParams];
@@ -237,7 +259,7 @@ class ConnectionFactory {
// Host variable carries a port or socket.
$params['host'] = $matches[1];
if (is_numeric($matches[2])) {
- $params['port'] = (int) $matches[2];
+ $params['port'] = (int)$matches[2];
} else {
$params['unix_socket'] = $matches[2];
}
diff --git a/lib/private/DB/MigrationService.php b/lib/private/DB/MigrationService.php
index 0a3b0d1dcc7..61a6d2baf16 100644
--- a/lib/private/DB/MigrationService.php
+++ b/lib/private/DB/MigrationService.php
@@ -158,7 +158,7 @@ class MigrationService {
/**
* Returns all versions which have already been applied
*
- * @return string[]
+ * @return list<string>
* @codeCoverageIgnore - no need to test this
*/
public function getMigratedVersions() {
@@ -174,6 +174,8 @@ class MigrationService {
$rows = $result->fetchAll(\PDO::FETCH_COLUMN);
$result->closeCursor();
+ usort($rows, $this->sortMigrations(...));
+
return $rows;
}
@@ -183,7 +185,23 @@ class MigrationService {
*/
public function getAvailableVersions(): array {
$this->ensureMigrationsAreLoaded();
- return array_map('strval', array_keys($this->migrations));
+ $versions = array_map('strval', array_keys($this->migrations));
+ usort($versions, $this->sortMigrations(...));
+ return $versions;
+ }
+
+ protected function sortMigrations(string $a, string $b): int {
+ preg_match('/(\d+)Date(\d+)/', basename($a), $matchA);
+ preg_match('/(\d+)Date(\d+)/', basename($b), $matchB);
+ if (!empty($matchA) && !empty($matchB)) {
+ $versionA = (int)$matchA[1];
+ $versionB = (int)$matchB[1];
+ if ($versionA !== $versionB) {
+ return ($versionA < $versionB) ? -1 : 1;
+ }
+ return ($matchA[2] < $matchB[2]) ? -1 : 1;
+ }
+ return (basename($a) < basename($b)) ? -1 : 1;
}
/**
@@ -204,23 +222,13 @@ class MigrationService {
\RegexIterator::GET_MATCH);
$files = array_keys(iterator_to_array($iterator));
- uasort($files, function ($a, $b) {
- preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
- preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
- if (!empty($matchA) && !empty($matchB)) {
- if ($matchA[1] !== $matchB[1]) {
- return ($matchA[1] < $matchB[1]) ? -1 : 1;
- }
- return ($matchA[2] < $matchB[2]) ? -1 : 1;
- }
- return (basename($a) < basename($b)) ? -1 : 1;
- });
+ usort($files, $this->sortMigrations(...));
$migrations = [];
foreach ($files as $file) {
$className = basename($file, '.php');
- $version = (string) substr($className, 7);
+ $version = (string)substr($className, 7);
if ($version === '0') {
throw new \InvalidArgumentException(
"Cannot load a migrations with the name '$version' because it is a reserved number"
diff --git a/lib/private/DB/Migrator.php b/lib/private/DB/Migrator.php
index 4fd457b4bc6..40f8ad9676a 100644
--- a/lib/private/DB/Migrator.php
+++ b/lib/private/DB/Migrator.php
@@ -139,7 +139,7 @@ class Migrator {
$step = 0;
foreach ($sqls as $sql) {
$this->emit($sql, $step++, count($sqls));
- $connection->executeQuery($sql);
+ $connection->executeStatement($sql);
}
if (!$connection->getDatabasePlatform() instanceof MySQLPlatform) {
$connection->commit();
diff --git a/lib/private/DB/PreparedStatement.php b/lib/private/DB/PreparedStatement.php
index 5fdfa2b03e8..54561ed96cd 100644
--- a/lib/private/DB/PreparedStatement.php
+++ b/lib/private/DB/PreparedStatement.php
@@ -78,6 +78,6 @@ class PreparedStatement implements IPreparedStatement {
return $this->result;
}
- throw new Exception("You have to execute the prepared statement before accessing the results");
+ throw new Exception('You have to execute the prepared statement before accessing the results');
}
}
diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php
index b70e20e4d0d..b922c861630 100644
--- a/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php
+++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php
@@ -57,7 +57,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* $expr->andX('u.type = ?', 'u.role = ?'));
*
* @param mixed ...$x Optional clause. Defaults = null, but requires
- * at least one defined when converting to string.
+ * at least one defined when converting to string.
*
* @return \OCP\DB\QueryBuilder\ICompositeExpression
*/
@@ -78,7 +78,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* $qb->where($qb->expr()->orX('u.type = ?', 'u.role = ?'));
*
* @param mixed ...$x Optional clause. Defaults = null, but requires
- * at least one defined when converting to string.
+ * at least one defined when converting to string.
*
* @return \OCP\DB\QueryBuilder\ICompositeExpression
*/
@@ -96,7 +96,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @param string $operator One of the IExpressionBuilder::* constants.
* @param mixed $y The right expression.
* @param mixed|null $type one of the IQueryBuilder::PARAM_* constants
- * required when comparing text fields for oci compatibility
+ * required when comparing text fields for oci compatibility
*
* @return string
*/
@@ -119,7 +119,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @param mixed $x The left expression.
* @param mixed $y The right expression.
* @param mixed|null $type one of the IQueryBuilder::PARAM_* constants
- * required when comparing text fields for oci compatibility
+ * required when comparing text fields for oci compatibility
*
* @return string
*/
@@ -141,7 +141,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @param mixed $x The left expression.
* @param mixed $y The right expression.
* @param mixed|null $type one of the IQueryBuilder::PARAM_* constants
- * required when comparing text fields for oci compatibility
+ * required when comparing text fields for oci compatibility
*
* @return string
*/
@@ -163,7 +163,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @param mixed $x The left expression.
* @param mixed $y The right expression.
* @param mixed|null $type one of the IQueryBuilder::PARAM_* constants
- * required when comparing text fields for oci compatibility
+ * required when comparing text fields for oci compatibility
*
* @return string
*/
@@ -185,7 +185,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @param mixed $x The left expression.
* @param mixed $y The right expression.
* @param mixed|null $type one of the IQueryBuilder::PARAM_* constants
- * required when comparing text fields for oci compatibility
+ * required when comparing text fields for oci compatibility
*
* @return string
*/
@@ -207,7 +207,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @param mixed $x The left expression.
* @param mixed $y The right expression.
* @param mixed|null $type one of the IQueryBuilder::PARAM_* constants
- * required when comparing text fields for oci compatibility
+ * required when comparing text fields for oci compatibility
*
* @return string
*/
@@ -229,7 +229,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @param mixed $x The left expression.
* @param mixed $y The right expression.
* @param mixed|null $type one of the IQueryBuilder::PARAM_* constants
- * required when comparing text fields for oci compatibility
+ * required when comparing text fields for oci compatibility
*
* @return string
*/
@@ -269,7 +269,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @param ILiteral|IParameter|IQueryFunction|string $x Field in string format to be inspected by LIKE() comparison.
* @param mixed $y Argument to be used in LIKE() comparison.
* @param mixed|null $type one of the IQueryBuilder::PARAM_* constants
- * required when comparing text fields for oci compatibility
+ * required when comparing text fields for oci compatibility
*
* @return string
*/
@@ -285,7 +285,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @param string $x Field in string format to be inspected by ILIKE() comparison.
* @param mixed $y Argument to be used in ILIKE() comparison.
* @param mixed|null $type one of the IQueryBuilder::PARAM_* constants
- * required when comparing text fields for oci compatibility
+ * required when comparing text fields for oci compatibility
*
* @return string
* @since 9.0.0
@@ -300,7 +300,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @param ILiteral|IParameter|IQueryFunction|string $x Field in string format to be inspected by NOT LIKE() comparison.
* @param mixed $y Argument to be used in NOT LIKE() comparison.
* @param mixed|null $type one of the IQueryBuilder::PARAM_* constants
- * required when comparing text fields for oci compatibility
+ * required when comparing text fields for oci compatibility
*
* @return string
*/
@@ -316,7 +316,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @param ILiteral|IParameter|IQueryFunction|string $x The field in string format to be inspected by IN() comparison.
* @param ILiteral|IParameter|IQueryFunction|string|array $y The placeholder or the array of values to be used by IN() comparison.
* @param mixed|null $type one of the IQueryBuilder::PARAM_* constants
- * required when comparing text fields for oci compatibility
+ * required when comparing text fields for oci compatibility
*
* @return string
*/
@@ -332,7 +332,7 @@ class ExpressionBuilder implements IExpressionBuilder {
* @param ILiteral|IParameter|IQueryFunction|string $x The field in string format to be inspected by NOT IN() comparison.
* @param ILiteral|IParameter|IQueryFunction|string|array $y The placeholder or the array of values to be used by NOT IN() comparison.
* @param mixed|null $type one of the IQueryBuilder::PARAM_* constants
- * required when comparing text fields for oci compatibility
+ * required when comparing text fields for oci compatibility
*
* @return string
*/
diff --git a/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php b/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php
index bde6523567f..c40cadfbdb5 100644
--- a/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php
+++ b/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php
@@ -288,4 +288,22 @@ abstract class ExtendedQueryBuilder implements IQueryBuilder {
public function executeStatement(?IDBConnection $connection = null): int {
return $this->builder->executeStatement($connection);
}
+
+ public function hintShardKey(string $column, mixed $value, bool $overwrite = false) {
+ $this->builder->hintShardKey($column, $value, $overwrite);
+ return $this;
+ }
+
+ public function runAcrossAllShards() {
+ $this->builder->runAcrossAllShards();
+ return $this;
+ }
+
+ public function getOutputColumns(): array {
+ return $this->builder->getOutputColumns();
+ }
+
+ public function prefixTableName(string $table): string {
+ return $this->builder->prefixTableName($table);
+ }
}
diff --git a/lib/private/DB/QueryBuilder/Literal.php b/lib/private/DB/QueryBuilder/Literal.php
index f0accce1d93..3fb897328e5 100644
--- a/lib/private/DB/QueryBuilder/Literal.php
+++ b/lib/private/DB/QueryBuilder/Literal.php
@@ -18,6 +18,6 @@ class Literal implements ILiteral {
}
public function __toString(): string {
- return (string) $this->literal;
+ return (string)$this->literal;
}
}
diff --git a/lib/private/DB/QueryBuilder/Parameter.php b/lib/private/DB/QueryBuilder/Parameter.php
index dbd723639fc..a272c744d62 100644
--- a/lib/private/DB/QueryBuilder/Parameter.php
+++ b/lib/private/DB/QueryBuilder/Parameter.php
@@ -18,6 +18,6 @@ class Parameter implements IParameter {
}
public function __toString(): string {
- return (string) $this->name;
+ return (string)$this->name;
}
}
diff --git a/lib/private/DB/QueryBuilder/Partitioned/InvalidPartitionedQueryException.php b/lib/private/DB/QueryBuilder/Partitioned/InvalidPartitionedQueryException.php
new file mode 100644
index 00000000000..3a5aa2f3e0e
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Partitioned/InvalidPartitionedQueryException.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OC\DB\QueryBuilder\Partitioned;
+
+/**
+ * Partitioned queries impose limitations that queries have to follow:
+ *
+ * 1. Any reference to columns not in the "main table" (the table referenced by "FROM"), needs to explicitly include the
+ * table or alias the column belongs to.
+ *
+ * For example:
+ * ```
+ * $query->select("mount_point", "mimetype")
+ * ->from("mounts", "m")
+ * ->innerJoin("m", "filecache", "f", $query->expr()->eq("root_id", "fileid"));
+ * ```
+ * will not work, as the query builder doesn't know that the `mimetype` column belongs to the "filecache partition".
+ * Instead, you need to do
+ * ```
+ * $query->select("mount_point", "f.mimetype")
+ * ->from("mounts", "m")
+ * ->innerJoin("m", "filecache", "f", $query->expr()->eq("m.root_id", "f.fileid"));
+ * ```
+ *
+ * 2. The "ON" condition for the join can only perform a comparison between both sides of the join once.
+ *
+ * For example:
+ * ```
+ * $query->select("mount_point", "mimetype")
+ * ->from("mounts", "m")
+ * ->innerJoin("m", "filecache", "f", $query->expr()->andX($query->expr()->eq("m.root_id", "f.fileid"), $query->expr()->eq("m.storage_id", "f.storage")));
+ * ```
+ * will not work.
+ *
+ * 3. An "OR" expression in the "WHERE" cannot mention both sides of the join, this does not apply to "AND" expressions.
+ *
+ * For example:
+ * ```
+ * $query->select("mount_point", "mimetype")
+ * ->from("mounts", "m")
+ * ->innerJoin("m", "filecache", "f", $query->expr()->eq("m.root_id", "f.fileid")))
+ * ->where($query->expr()->orX(
+ * $query->expr()-eq("m.user_id", $query->createNamedParameter("test"))),
+ * $query->expr()-eq("f.name", $query->createNamedParameter("test"))),
+ * ));
+ * ```
+ * will not work, but.
+ * ```
+ * $query->select("mount_point", "mimetype")
+ * ->from("mounts", "m")
+ * ->innerJoin("m", "filecache", "f", $query->expr()->eq("m.root_id", "f.fileid")))
+ * ->where($query->expr()->andX(
+ * $query->expr()-eq("m.user_id", $query->createNamedParameter("test"))),
+ * $query->expr()-eq("f.name", $query->createNamedParameter("test"))),
+ * ));
+ * ```
+ * will.
+ *
+ * 4. Queries that join cross-partition cannot use position parameters, only named parameters are allowed
+ * 5. The "ON" condition of a join cannot contain and "OR" expression.
+ * 6. Right-joins are not allowed.
+ * 7. Update, delete and insert statements aren't allowed to contain cross-partition joins.
+ * 8. Queries that "GROUP BY" a column from the joined partition are not allowed.
+ * 9. Any `join` call needs to be made before any `where` call.
+ * 10. Queries that join cross-partition with an "INNER JOIN" or "LEFT JOIN" with a condition on the left side
+ * cannot use "LIMIT" or "OFFSET" in queries.
+ *
+ * The part of the query running on the sharded table has some additional limitations,
+ * see the `InvalidShardedQueryException` documentation for more information.
+ */
+class InvalidPartitionedQueryException extends \Exception {
+
+}
diff --git a/lib/private/DB/QueryBuilder/Partitioned/JoinCondition.php b/lib/private/DB/QueryBuilder/Partitioned/JoinCondition.php
new file mode 100644
index 00000000000..a08858d1d6b
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Partitioned/JoinCondition.php
@@ -0,0 +1,173 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\DB\QueryBuilder\Partitioned;
+
+use OC\DB\QueryBuilder\CompositeExpression;
+use OC\DB\QueryBuilder\QueryFunction;
+use OCP\DB\QueryBuilder\IQueryFunction;
+
+/**
+ * Utility class for working with join conditions
+ */
+class JoinCondition {
+ public function __construct(
+ public string|IQueryFunction $fromColumn,
+ public ?string $fromAlias,
+ public string|IQueryFunction $toColumn,
+ public ?string $toAlias,
+ public array $fromConditions,
+ public array $toConditions,
+ ) {
+ if (is_string($this->fromColumn) && str_starts_with($this->fromColumn, '(')) {
+ $this->fromColumn = new QueryFunction($this->fromColumn);
+ }
+ if (is_string($this->toColumn) && str_starts_with($this->toColumn, '(')) {
+ $this->toColumn = new QueryFunction($this->toColumn);
+ }
+ }
+
+ /**
+ * @param JoinCondition[] $conditions
+ * @return JoinCondition
+ */
+ public static function merge(array $conditions): JoinCondition {
+ $fromColumn = '';
+ $toColumn = '';
+ $fromAlias = null;
+ $toAlias = null;
+ $fromConditions = [];
+ $toConditions = [];
+ foreach ($conditions as $condition) {
+ if (($condition->fromColumn && $fromColumn) || ($condition->toColumn && $toColumn)) {
+ throw new InvalidPartitionedQueryException("Can't join from {$condition->fromColumn} to {$condition->toColumn} as it already join froms {$fromColumn} to {$toColumn}");
+ }
+ if ($condition->fromColumn) {
+ $fromColumn = $condition->fromColumn;
+ }
+ if ($condition->toColumn) {
+ $toColumn = $condition->toColumn;
+ }
+ if ($condition->fromAlias) {
+ $fromAlias = $condition->fromAlias;
+ }
+ if ($condition->toAlias) {
+ $toAlias = $condition->toAlias;
+ }
+ $fromConditions = array_merge($fromConditions, $condition->fromConditions);
+ $toConditions = array_merge($toConditions, $condition->toConditions);
+ }
+ return new JoinCondition($fromColumn, $fromAlias, $toColumn, $toAlias, $fromConditions, $toConditions);
+ }
+
+ /**
+ * @param null|string|CompositeExpression $condition
+ * @param string $join
+ * @param string $alias
+ * @param string $fromAlias
+ * @return JoinCondition
+ * @throws InvalidPartitionedQueryException
+ */
+ public static function parse($condition, string $join, string $alias, string $fromAlias): JoinCondition {
+ if ($condition === null) {
+ throw new InvalidPartitionedQueryException("Can't join on $join without a condition");
+ }
+
+ $result = self::parseSubCondition($condition, $join, $alias, $fromAlias);
+ if (!$result->fromColumn || !$result->toColumn) {
+ throw new InvalidPartitionedQueryException("No join condition found from $fromAlias to $alias");
+ }
+ return $result;
+ }
+
+ private static function parseSubCondition($condition, string $join, string $alias, string $fromAlias): JoinCondition {
+ if ($condition instanceof CompositeExpression) {
+ if ($condition->getType() === CompositeExpression::TYPE_OR) {
+ throw new InvalidPartitionedQueryException("Cannot join on $join with an OR expression");
+ }
+ return self::merge(array_map(function ($subCondition) use ($join, $alias, $fromAlias) {
+ return self::parseSubCondition($subCondition, $join, $alias, $fromAlias);
+ }, $condition->getParts()));
+ }
+
+ $condition = (string)$condition;
+ $isSubCondition = self::isExtraCondition($condition);
+ if ($isSubCondition) {
+ if (self::mentionsAlias($condition, $fromAlias)) {
+ return new JoinCondition('', null, '', null, [$condition], []);
+ } else {
+ return new JoinCondition('', null, '', null, [], [$condition]);
+ }
+ }
+
+ $condition = str_replace('`', '', $condition);
+
+ // expect a condition in the form of 'alias1.column1 = alias2.column2'
+ if (!str_contains($condition, ' = ')) {
+ throw new InvalidPartitionedQueryException("Can only join on $join with an `eq` condition");
+ }
+ $parts = explode(' = ', $condition, 2);
+ $parts = array_map(function (string $part) {
+ return self::clearConditionPart($part);
+ }, $parts);
+
+ if (!self::isSingleCondition($parts[0]) || !self::isSingleCondition($parts[1])) {
+ throw new InvalidPartitionedQueryException("Can only join on $join with a single condition");
+ }
+
+
+ if (self::mentionsAlias($parts[0], $fromAlias)) {
+ return new JoinCondition($parts[0], self::getAliasForPart($parts[0]), $parts[1], self::getAliasForPart($parts[1]), [], []);
+ } elseif (self::mentionsAlias($parts[1], $fromAlias)) {
+ return new JoinCondition($parts[1], self::getAliasForPart($parts[1]), $parts[0], self::getAliasForPart($parts[0]), [], []);
+ } else {
+ throw new InvalidPartitionedQueryException("join condition for $join needs to explicitly refer to the table by alias");
+ }
+ }
+
+ private static function isSingleCondition(string $condition): bool {
+ return !(str_contains($condition, ' OR ') || str_contains($condition, ' AND '));
+ }
+
+ private static function getAliasForPart(string $part): ?string {
+ if (str_contains($part, ' ')) {
+ return uniqid('join_alias_');
+ } else {
+ return null;
+ }
+ }
+
+ private static function clearConditionPart(string $part): string {
+ if (str_starts_with($part, 'CAST(')) {
+ // pgsql/mysql cast
+ $part = substr($part, strlen('CAST('));
+ [$part] = explode(' AS ', $part);
+ } elseif (str_starts_with($part, 'to_number(to_char(')) {
+ // oracle cast to int
+ $part = substr($part, strlen('to_number(to_char('), -2);
+ } elseif (str_starts_with($part, 'to_number(to_char(')) {
+ // oracle cast to string
+ $part = substr($part, strlen('to_char('), -1);
+ }
+ return $part;
+ }
+
+ /**
+ * Check that a condition is an extra limit on the from/to part, and not the join condition
+ *
+ * This is done by checking that only one of the halves of the condition references a column
+ */
+ private static function isExtraCondition(string $condition): bool {
+ $parts = explode(' ', $condition, 2);
+ return str_contains($parts[0], '`') xor str_contains($parts[1], '`');
+ }
+
+ private static function mentionsAlias(string $condition, string $alias): bool {
+ return str_contains($condition, "$alias.");
+ }
+}
diff --git a/lib/private/DB/QueryBuilder/Partitioned/PartitionQuery.php b/lib/private/DB/QueryBuilder/Partitioned/PartitionQuery.php
new file mode 100644
index 00000000000..a5024b478d3
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Partitioned/PartitionQuery.php
@@ -0,0 +1,75 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OC\DB\QueryBuilder\Partitioned;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+
+/**
+ * A sub-query from a partitioned join
+ */
+class PartitionQuery {
+ public const JOIN_MODE_INNER = 'inner';
+ public const JOIN_MODE_LEFT = 'left';
+ // left-join where the left side IS NULL
+ public const JOIN_MODE_LEFT_NULL = 'left_null';
+
+ public const JOIN_MODE_RIGHT = 'right';
+
+ public function __construct(
+ public IQueryBuilder $query,
+ public string $joinFromColumn,
+ public string $joinToColumn,
+ public string $joinMode,
+ ) {
+ if ($joinMode !== self::JOIN_MODE_LEFT && $joinMode !== self::JOIN_MODE_INNER) {
+ throw new InvalidPartitionedQueryException("$joinMode joins aren't allowed in partitioned queries");
+ }
+ }
+
+ public function mergeWith(array $rows): array {
+ if (empty($rows)) {
+ return [];
+ }
+ // strip table/alias from column names
+ $joinFromColumn = preg_replace('/\w+\./', '', $this->joinFromColumn);
+ $joinToColumn = preg_replace('/\w+\./', '', $this->joinToColumn);
+
+ $joinFromValues = array_map(function (array $row) use ($joinFromColumn) {
+ return $row[$joinFromColumn];
+ }, $rows);
+ $joinFromValues = array_filter($joinFromValues, function ($value) {
+ return $value !== null;
+ });
+ $this->query->andWhere($this->query->expr()->in($this->joinToColumn, $this->query->createNamedParameter($joinFromValues, IQueryBuilder::PARAM_STR_ARRAY, ':' . uniqid())));
+
+ $s = $this->query->getSQL();
+ $partitionedRows = $this->query->executeQuery()->fetchAll();
+
+ $columns = $this->query->getOutputColumns();
+ $nullResult = array_combine($columns, array_fill(0, count($columns), null));
+
+ $partitionedRowsByKey = [];
+ foreach ($partitionedRows as $partitionedRow) {
+ $partitionedRowsByKey[$partitionedRow[$joinToColumn]][] = $partitionedRow;
+ }
+ $result = [];
+ foreach ($rows as $row) {
+ if (isset($partitionedRowsByKey[$row[$joinFromColumn]])) {
+ if ($this->joinMode !== self::JOIN_MODE_LEFT_NULL) {
+ foreach ($partitionedRowsByKey[$row[$joinFromColumn]] as $partitionedRow) {
+ $result[] = array_merge($row, $partitionedRow);
+ }
+ }
+ } elseif ($this->joinMode === self::JOIN_MODE_LEFT || $this->joinMode === self::JOIN_MODE_LEFT_NULL) {
+ $result[] = array_merge($nullResult, $row);
+ }
+ }
+ return $result;
+ }
+}
diff --git a/lib/private/DB/QueryBuilder/Partitioned/PartitionSplit.php b/lib/private/DB/QueryBuilder/Partitioned/PartitionSplit.php
new file mode 100644
index 00000000000..ad4c0fab055
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Partitioned/PartitionSplit.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\DB\QueryBuilder\Partitioned;
+
+/**
+ * Information about a database partition, containing the tables in the partition and any active alias
+ */
+class PartitionSplit {
+ /** @var array<string, string> */
+ public array $aliases = [];
+
+ /**
+ * @param string[] $tables
+ */
+ public function __construct(
+ public string $name,
+ public array $tables,
+ ) {
+ }
+
+ public function addAlias(string $table, string $alias): void {
+ if ($this->containsTable($table)) {
+ $this->aliases[$alias] = $table;
+ }
+ }
+
+ public function addTable(string $table): void {
+ if (!$this->containsTable($table)) {
+ $this->tables[] = $table;
+ }
+ }
+
+ public function containsTable(string $table): bool {
+ return in_array($table, $this->tables);
+ }
+
+ public function containsAlias(string $alias): bool {
+ return array_key_exists($alias, $this->aliases);
+ }
+
+ private function getTablesAndAliases(): array {
+ return array_keys($this->aliases) + $this->tables;
+ }
+
+ /**
+ * Check if a query predicate mentions a table or alias from this partition
+ *
+ * @param string $predicate
+ * @return bool
+ */
+ public function checkPredicateForTable(string $predicate): bool {
+ foreach ($this->getTablesAndAliases() as $name) {
+ if (str_contains($predicate, "`$name`.`")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public function isColumnInPartition(string $column): bool {
+ foreach ($this->getTablesAndAliases() as $name) {
+ if (str_starts_with($column, "$name.")) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php b/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php
new file mode 100644
index 00000000000..175b7c1a42e
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php
@@ -0,0 +1,426 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OC\DB\QueryBuilder\Partitioned;
+
+use OC\DB\QueryBuilder\CompositeExpression;
+use OC\DB\QueryBuilder\QuoteHelper;
+use OC\DB\QueryBuilder\Sharded\AutoIncrementHandler;
+use OC\DB\QueryBuilder\Sharded\ShardConnectionManager;
+use OC\DB\QueryBuilder\Sharded\ShardedQueryBuilder;
+use OCP\DB\IResult;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\DB\QueryBuilder\IQueryFunction;
+use OCP\IDBConnection;
+
+/**
+ * A special query builder that automatically splits queries that span across multiple database partitions[1].
+ *
+ * This is done by inspecting the query as it's being built, and when a cross-partition join is detected,
+ * the part of the query that touches the partition is split of into a different sub-query.
+ * Then, when the query is executed, the results from the sub-queries are automatically merged.
+ *
+ * This whole process is intended to be transparent to any code using the query builder, however it does impose some extra
+ * limitation for queries that work cross-partition. See the documentation from `InvalidPartitionedQueryException` for more details.
+ *
+ * When a join is created in the query, this builder checks if it belongs to the same partition as the table from the
+ * original FROM/UPDATE/DELETE/INSERT and if not, creates a new "sub query" for the partition.
+ * Then for every part that is added the query, the part is analyzed to determine which partition the query part is referencing
+ * and the query part is added to the sub query for that partition.
+ *
+ * [1]: A set of tables which can't be queried together with the rest of the tables, such as when sharding is used.
+ */
+class PartitionedQueryBuilder extends ShardedQueryBuilder {
+ /** @var array<string, PartitionQuery> $splitQueries */
+ private array $splitQueries = [];
+ /** @var list<PartitionSplit> */
+ private array $partitions = [];
+
+ /** @var array{'select': string|array, 'alias': ?string}[] */
+ private array $selects = [];
+ private ?PartitionSplit $mainPartition = null;
+ private bool $hasPositionalParameter = false;
+ private QuoteHelper $quoteHelper;
+ private ?int $limit = null;
+ private ?int $offset = null;
+
+ public function __construct(
+ IQueryBuilder $builder,
+ array $shardDefinitions,
+ ShardConnectionManager $shardConnectionManager,
+ AutoIncrementHandler $autoIncrementHandler,
+ ) {
+ parent::__construct($builder, $shardDefinitions, $shardConnectionManager, $autoIncrementHandler);
+ $this->quoteHelper = new QuoteHelper();
+ }
+
+ private function newQuery(): IQueryBuilder {
+ // get a fresh, non-partitioning query builder
+ $builder = $this->builder->getConnection()->getQueryBuilder();
+ if ($builder instanceof PartitionedQueryBuilder) {
+ $builder = $builder->builder;
+ }
+
+ return new ShardedQueryBuilder(
+ $builder,
+ $this->shardDefinitions,
+ $this->shardConnectionManager,
+ $this->autoIncrementHandler,
+ );
+ }
+
+ // we need to save selects until we know all the table aliases
+ public function select(...$selects) {
+ $this->selects = [];
+ $this->addSelect(...$selects);
+ return $this;
+ }
+
+ public function addSelect(...$select) {
+ $select = array_map(function ($select) {
+ return ['select' => $select, 'alias' => null];
+ }, $select);
+ $this->selects = array_merge($this->selects, $select);
+ return $this;
+ }
+
+ public function selectAlias($select, $alias) {
+ $this->selects[] = ['select' => $select, 'alias' => $alias];
+ return $this;
+ }
+
+ /**
+ * Ensure that a column is being selected by the query
+ *
+ * This is mainly used to ensure that the returned rows from both sides of a partition contains the columns of the join predicate
+ *
+ * @param string $column
+ * @return void
+ */
+ private function ensureSelect(string|IQueryFunction $column, ?string $alias = null): void {
+ $checkColumn = $alias ?: $column;
+ if (str_contains($checkColumn, '.')) {
+ [, $checkColumn] = explode('.', $checkColumn);
+ }
+ foreach ($this->selects as $select) {
+ if ($select['select'] === $checkColumn || $select['select'] === '*' || str_ends_with($select['select'], '.' . $checkColumn)) {
+ return;
+ }
+ }
+ if ($alias) {
+ $this->selectAlias($column, $alias);
+ } else {
+ $this->addSelect($column);
+ }
+ }
+
+ /**
+ * Distribute the select statements to the correct partition
+ *
+ * This is done at the end instead of when the `select` call is made, because the `select` calls are generally done
+ * before we know what tables are involved in the query
+ *
+ * @return void
+ */
+ private function applySelects(): void {
+ foreach ($this->selects as $select) {
+ foreach ($this->partitions as $partition) {
+ if (is_string($select['select']) && (
+ $select['select'] === '*' ||
+ $partition->isColumnInPartition($select['select']))
+ ) {
+ if (isset($this->splitQueries[$partition->name])) {
+ if ($select['alias']) {
+ $this->splitQueries[$partition->name]->query->selectAlias($select['select'], $select['alias']);
+ } else {
+ $this->splitQueries[$partition->name]->query->addSelect($select['select']);
+ }
+ if ($select['select'] !== '*') {
+ continue 2;
+ }
+ }
+ }
+ }
+
+ if ($select['alias']) {
+ parent::selectAlias($select['select'], $select['alias']);
+ } else {
+ parent::addSelect($select['select']);
+ }
+ }
+ $this->selects = [];
+ }
+
+
+ public function addPartition(PartitionSplit $partition): void {
+ $this->partitions[] = $partition;
+ }
+
+ private function getPartition(string $table): ?PartitionSplit {
+ foreach ($this->partitions as $partition) {
+ if ($partition->containsTable($table) || $partition->containsAlias($table)) {
+ return $partition;
+ }
+ }
+ return null;
+ }
+
+ public function from($from, $alias = null) {
+ if (is_string($from) && $partition = $this->getPartition($from)) {
+ $this->mainPartition = $partition;
+ if ($alias) {
+ $this->mainPartition->addAlias($from, $alias);
+ }
+ }
+ return parent::from($from, $alias);
+ }
+
+ public function innerJoin($fromAlias, $join, $alias, $condition = null): self {
+ return $this->join($fromAlias, $join, $alias, $condition);
+ }
+
+ public function leftJoin($fromAlias, $join, $alias, $condition = null): self {
+ return $this->join($fromAlias, $join, $alias, $condition, PartitionQuery::JOIN_MODE_LEFT);
+ }
+
+ public function join($fromAlias, $join, $alias, $condition = null, $joinMode = PartitionQuery::JOIN_MODE_INNER): self {
+ $partition = $this->getPartition($join);
+ $fromPartition = $this->getPartition($fromAlias);
+ if ($partition && $partition !== $this->mainPartition) {
+ // join from the main db to a partition
+
+ $joinCondition = JoinCondition::parse($condition, $join, $alias, $fromAlias);
+ $partition->addAlias($join, $alias);
+
+ if (!isset($this->splitQueries[$partition->name])) {
+ $this->splitQueries[$partition->name] = new PartitionQuery(
+ $this->newQuery(),
+ $joinCondition->fromAlias ?? $joinCondition->fromColumn, $joinCondition->toAlias ?? $joinCondition->toColumn,
+ $joinMode
+ );
+ $this->splitQueries[$partition->name]->query->from($join, $alias);
+ $this->ensureSelect($joinCondition->fromColumn, $joinCondition->fromAlias);
+ $this->ensureSelect($joinCondition->toColumn, $joinCondition->toAlias);
+ } else {
+ $query = $this->splitQueries[$partition->name]->query;
+ if ($partition->containsAlias($fromAlias)) {
+ $query->innerJoin($fromAlias, $join, $alias, $condition);
+ } else {
+ throw new InvalidPartitionedQueryException("Can't join across partition boundaries more than once");
+ }
+ }
+ $this->splitQueries[$partition->name]->query->andWhere(...$joinCondition->toConditions);
+ parent::andWhere(...$joinCondition->fromConditions);
+ return $this;
+ } elseif ($fromPartition && $fromPartition !== $partition) {
+ // join from partition, to the main db
+
+ $joinCondition = JoinCondition::parse($condition, $join, $alias, $fromAlias);
+ if (str_starts_with($fromPartition->name, 'from_')) {
+ $partitionName = $fromPartition->name;
+ } else {
+ $partitionName = 'from_' . $fromPartition->name;
+ }
+
+ if (!isset($this->splitQueries[$partitionName])) {
+ $newPartition = new PartitionSplit($partitionName, [$join]);
+ $newPartition->addAlias($join, $alias);
+ $this->partitions[] = $newPartition;
+
+ $this->splitQueries[$partitionName] = new PartitionQuery(
+ $this->newQuery(),
+ $joinCondition->fromAlias ?? $joinCondition->fromColumn, $joinCondition->toAlias ?? $joinCondition->toColumn,
+ $joinMode
+ );
+ $this->ensureSelect($joinCondition->fromColumn, $joinCondition->fromAlias);
+ $this->ensureSelect($joinCondition->toColumn, $joinCondition->toAlias);
+ $this->splitQueries[$partitionName]->query->from($join, $alias);
+ $this->splitQueries[$partitionName]->query->andWhere(...$joinCondition->toConditions);
+ parent::andWhere(...$joinCondition->fromConditions);
+ } else {
+ $fromPartition->addTable($join);
+ $fromPartition->addAlias($join, $alias);
+
+ $query = $this->splitQueries[$partitionName]->query;
+ $query->innerJoin($fromAlias, $join, $alias, $condition);
+ }
+ return $this;
+ } else {
+ // join within the main db or a partition
+ if ($joinMode === PartitionQuery::JOIN_MODE_INNER) {
+ return parent::innerJoin($fromAlias, $join, $alias, $condition);
+ } elseif ($joinMode === PartitionQuery::JOIN_MODE_LEFT) {
+ return parent::leftJoin($fromAlias, $join, $alias, $condition);
+ } elseif ($joinMode === PartitionQuery::JOIN_MODE_RIGHT) {
+ return parent::rightJoin($fromAlias, $join, $alias, $condition);
+ } else {
+ throw new \InvalidArgumentException("Invalid join mode: $joinMode");
+ }
+ }
+ }
+
+ /**
+ * Flatten a list of predicates by merging the parts of any "AND" expression into the list of predicates
+ *
+ * @param array $predicates
+ * @return array
+ */
+ private function flattenPredicates(array $predicates): array {
+ $result = [];
+ foreach ($predicates as $predicate) {
+ if ($predicate instanceof CompositeExpression && $predicate->getType() === CompositeExpression::TYPE_AND) {
+ $result = array_merge($result, $this->flattenPredicates($predicate->getParts()));
+ } else {
+ $result[] = $predicate;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Split an array of predicates (WHERE query parts) by the partition they reference
+ * @param array $predicates
+ * @return array<string, array>
+ */
+ private function splitPredicatesByParts(array $predicates): array {
+ $predicates = $this->flattenPredicates($predicates);
+
+ $partitionPredicates = [];
+ foreach ($predicates as $predicate) {
+ $partition = $this->getPartitionForPredicate((string)$predicate);
+ if ($this->mainPartition === $partition) {
+ $partitionPredicates[''][] = $predicate;
+ } elseif ($partition) {
+ $partitionPredicates[$partition->name][] = $predicate;
+ } else {
+ $partitionPredicates[''][] = $predicate;
+ }
+ }
+ return $partitionPredicates;
+ }
+
+ public function where(...$predicates) {
+ return $this->andWhere(...$predicates);
+ }
+
+ public function andWhere(...$where) {
+ if ($where) {
+ foreach ($this->splitPredicatesByParts($where) as $alias => $predicates) {
+ if (isset($this->splitQueries[$alias])) {
+ // when there is a condition on a table being left-joined it starts to behave as if it's an inner join
+ // since any joined column that doesn't have the left part will not match the condition
+ // when there the condition is `$joinToColumn IS NULL` we instead mark the query as excluding the left half
+ if ($this->splitQueries[$alias]->joinMode === PartitionQuery::JOIN_MODE_LEFT) {
+ $this->splitQueries[$alias]->joinMode = PartitionQuery::JOIN_MODE_INNER;
+
+ $column = $this->quoteHelper->quoteColumnName($this->splitQueries[$alias]->joinToColumn);
+ foreach ($predicates as $predicate) {
+ if ((string)$predicate === "$column IS NULL") {
+ $this->splitQueries[$alias]->joinMode = PartitionQuery::JOIN_MODE_LEFT_NULL;
+ } else {
+ $this->splitQueries[$alias]->query->andWhere($predicate);
+ }
+ }
+ } else {
+ $this->splitQueries[$alias]->query->andWhere(...$predicates);
+ }
+ } else {
+ parent::andWhere(...$predicates);
+ }
+ }
+ }
+ return $this;
+ }
+
+
+ private function getPartitionForPredicate(string $predicate): ?PartitionSplit {
+ foreach ($this->partitions as $partition) {
+
+ if (str_contains($predicate, '?')) {
+ $this->hasPositionalParameter = true;
+ }
+ if ($partition->checkPredicateForTable($predicate)) {
+ return $partition;
+ }
+ }
+ return null;
+ }
+
+ public function update($update = null, $alias = null) {
+ return parent::update($update, $alias);
+ }
+
+ public function insert($insert = null) {
+ return parent::insert($insert);
+ }
+
+ public function delete($delete = null, $alias = null) {
+ return parent::delete($delete, $alias);
+ }
+
+ public function setMaxResults($maxResults) {
+ if ($maxResults > 0) {
+ $this->limit = (int)$maxResults;
+ }
+ return parent::setMaxResults($maxResults);
+ }
+
+ public function setFirstResult($firstResult) {
+ if ($firstResult > 0) {
+ $this->offset = (int)$firstResult;
+ }
+ return parent::setFirstResult($firstResult);
+ }
+
+ public function executeQuery(?IDBConnection $connection = null): IResult {
+ $this->applySelects();
+ if ($this->splitQueries && $this->hasPositionalParameter) {
+ throw new InvalidPartitionedQueryException("Partitioned queries aren't allowed to to positional arguments");
+ }
+ foreach ($this->splitQueries as $split) {
+ $split->query->setParameters($this->getParameters(), $this->getParameterTypes());
+ }
+ if (count($this->splitQueries) > 0) {
+ $hasNonLeftJoins = array_reduce($this->splitQueries, function (bool $hasNonLeftJoins, PartitionQuery $query) {
+ return $hasNonLeftJoins || $query->joinMode !== PartitionQuery::JOIN_MODE_LEFT;
+ }, false);
+ if ($hasNonLeftJoins) {
+ if (is_int($this->limit)) {
+ throw new InvalidPartitionedQueryException('Limit is not allowed in partitioned queries');
+ }
+ if (is_int($this->offset)) {
+ throw new InvalidPartitionedQueryException('Offset is not allowed in partitioned queries');
+ }
+ }
+ }
+
+ $s = $this->getSQL();
+ $result = parent::executeQuery($connection);
+ if (count($this->splitQueries) > 0) {
+ return new PartitionedResult($this->splitQueries, $result);
+ } else {
+ return $result;
+ }
+ }
+
+ public function executeStatement(?IDBConnection $connection = null): int {
+ if (count($this->splitQueries)) {
+ throw new InvalidPartitionedQueryException("Partitioning write queries isn't supported");
+ }
+ return parent::executeStatement($connection);
+ }
+
+ public function getSQL() {
+ $this->applySelects();
+ return parent::getSQL();
+ }
+
+ public function getPartitionCount(): int {
+ return count($this->splitQueries) + 1;
+ }
+}
diff --git a/lib/private/DB/QueryBuilder/Partitioned/PartitionedResult.php b/lib/private/DB/QueryBuilder/Partitioned/PartitionedResult.php
new file mode 100644
index 00000000000..aa9cc43b38b
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Partitioned/PartitionedResult.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OC\DB\QueryBuilder\Partitioned;
+
+use OC\DB\ArrayResult;
+use OCP\DB\IResult;
+use PDO;
+
+/**
+ * Combine the results of multiple join parts into a single result
+ */
+class PartitionedResult extends ArrayResult {
+ private bool $fetched = false;
+
+ /**
+ * @param PartitionQuery[] $splitOfParts
+ * @param IResult $result
+ */
+ public function __construct(
+ private array $splitOfParts,
+ private IResult $result
+ ) {
+ parent::__construct([]);
+ }
+
+ public function closeCursor(): bool {
+ return $this->result->closeCursor();
+ }
+
+ public function fetch(int $fetchMode = PDO::FETCH_ASSOC) {
+ $this->fetchRows();
+ return parent::fetch($fetchMode);
+ }
+
+ public function fetchAll(int $fetchMode = PDO::FETCH_ASSOC): array {
+ $this->fetchRows();
+ return parent::fetchAll($fetchMode);
+ }
+
+ public function rowCount(): int {
+ $this->fetchRows();
+ return parent::rowCount();
+ }
+
+ private function fetchRows(): void {
+ if (!$this->fetched) {
+ $this->fetched = true;
+ $this->rows = $this->result->fetchAll();
+ foreach ($this->splitOfParts as $part) {
+ $this->rows = $part->mergeWith($this->rows);
+ }
+ $this->count = count($this->rows);
+ }
+ }
+}
diff --git a/lib/private/DB/QueryBuilder/QueryBuilder.php b/lib/private/DB/QueryBuilder/QueryBuilder.php
index 4c4786f02b6..5c7e273c9ec 100644
--- a/lib/private/DB/QueryBuilder/QueryBuilder.php
+++ b/lib/private/DB/QueryBuilder/QueryBuilder.php
@@ -49,6 +49,7 @@ class QueryBuilder implements IQueryBuilder {
/** @var string */
protected $lastInsertedTable;
+ private array $selectedColumns = [];
/**
* Initializes a new QueryBuilder.
@@ -68,11 +69,11 @@ class QueryBuilder implements IQueryBuilder {
* Enable/disable automatic prefixing of table names with the oc_ prefix
*
* @param bool $enabled If set to true table names will be prefixed with the
- * owncloud database prefix automatically.
+ * owncloud database prefix automatically.
* @since 8.2.0
*/
public function automaticTablePrefix($enabled) {
- $this->automaticTablePrefix = (bool) $enabled;
+ $this->automaticTablePrefix = (bool)$enabled;
}
/**
@@ -405,7 +406,7 @@ class QueryBuilder implements IQueryBuilder {
* @return $this This QueryBuilder instance.
*/
public function setFirstResult($firstResult) {
- $this->queryBuilder->setFirstResult((int) $firstResult);
+ $this->queryBuilder->setFirstResult((int)$firstResult);
return $this;
}
@@ -435,7 +436,7 @@ class QueryBuilder implements IQueryBuilder {
if ($maxResults === null) {
$this->queryBuilder->setMaxResults($maxResults);
} else {
- $this->queryBuilder->setMaxResults((int) $maxResults);
+ $this->queryBuilder->setMaxResults((int)$maxResults);
}
return $this;
@@ -470,6 +471,7 @@ class QueryBuilder implements IQueryBuilder {
if (count($selects) === 1 && is_array($selects[0])) {
$selects = $selects[0];
}
+ $this->addOutputColumns($selects);
$this->queryBuilder->select(
$this->helper->quoteColumnNames($selects)
@@ -497,6 +499,7 @@ class QueryBuilder implements IQueryBuilder {
$this->queryBuilder->addSelect(
$this->helper->quoteColumnName($select) . ' AS ' . $this->helper->quoteColumnName($alias)
);
+ $this->addOutputColumns([$alias]);
return $this;
}
@@ -518,6 +521,7 @@ class QueryBuilder implements IQueryBuilder {
if (!is_array($select)) {
$select = [$select];
}
+ $this->addOutputColumns($select);
$quotedSelect = $this->helper->quoteColumnNames($select);
@@ -547,6 +551,7 @@ class QueryBuilder implements IQueryBuilder {
if (count($selects) === 1 && is_array($selects[0])) {
$selects = $selects[0];
}
+ $this->addOutputColumns($selects);
$this->queryBuilder->addSelect(
$this->helper->quoteColumnNames($selects)
@@ -555,6 +560,30 @@ class QueryBuilder implements IQueryBuilder {
return $this;
}
+ private function addOutputColumns(array $columns) {
+ foreach ($columns as $column) {
+ if (is_array($column)) {
+ $this->addOutputColumns($column);
+ } elseif (is_string($column) && !str_contains($column, '*')) {
+ if (str_contains($column, '.')) {
+ [, $column] = explode('.', $column);
+ }
+ $this->selectedColumns[] = $column;
+ }
+ }
+ }
+
+ public function getOutputColumns(): array {
+ return array_unique(array_map(function (string $column) {
+ if (str_contains($column, '.')) {
+ [, $column] = explode('.', $column);
+ return $column;
+ } else {
+ return $column;
+ }
+ }, $this->selectedColumns));
+ }
+
/**
* Turns the query being built into a bulk delete query that ranges over
* a certain table.
@@ -978,7 +1007,7 @@ class QueryBuilder implements IQueryBuilder {
public function setValue($column, $value) {
$this->queryBuilder->setValue(
$this->helper->quoteColumnName($column),
- (string) $value
+ (string)$value
);
return $this;
@@ -1287,7 +1316,7 @@ class QueryBuilder implements IQueryBuilder {
*/
public function getTableName($table) {
if ($table instanceof IQueryFunction) {
- return (string) $table;
+ return (string)$table;
}
$table = $this->prefixTableName($table);
@@ -1300,7 +1329,7 @@ class QueryBuilder implements IQueryBuilder {
* @param string $table
* @return string
*/
- protected function prefixTableName($table) {
+ public function prefixTableName(string $table): string {
if ($this->automaticTablePrefix === false || str_starts_with($table, '*PREFIX*')) {
return $table;
}
@@ -1336,4 +1365,18 @@ class QueryBuilder implements IQueryBuilder {
return $this->helper->quoteColumnName($alias);
}
+
+ public function escapeLikeParameter(string $parameter): string {
+ return $this->connection->escapeLikeParameter($parameter);
+ }
+
+ public function hintShardKey(string $column, mixed $value, bool $overwrite = false) {
+ return $this;
+ }
+
+ public function runAcrossAllShards() {
+ // noop
+ return $this;
+ }
+
}
diff --git a/lib/private/DB/QueryBuilder/QueryFunction.php b/lib/private/DB/QueryBuilder/QueryFunction.php
index 9cdd6c31c7b..7f2ab584a73 100644
--- a/lib/private/DB/QueryBuilder/QueryFunction.php
+++ b/lib/private/DB/QueryBuilder/QueryFunction.php
@@ -18,6 +18,6 @@ class QueryFunction implements IQueryFunction {
}
public function __toString(): string {
- return (string) $this->function;
+ return (string)$this->function;
}
}
diff --git a/lib/private/DB/QueryBuilder/QuoteHelper.php b/lib/private/DB/QueryBuilder/QuoteHelper.php
index a60a9731aa2..7ce4b359638 100644
--- a/lib/private/DB/QueryBuilder/QuoteHelper.php
+++ b/lib/private/DB/QueryBuilder/QuoteHelper.php
@@ -35,7 +35,7 @@ class QuoteHelper {
*/
public function quoteColumnName($string) {
if ($string instanceof IParameter || $string instanceof ILiteral || $string instanceof IQueryFunction) {
- return (string) $string;
+ return (string)$string;
}
if ($string === null || $string === 'null' || $string === '*') {
diff --git a/lib/private/DB/QueryBuilder/Sharded/AutoIncrementHandler.php b/lib/private/DB/QueryBuilder/Sharded/AutoIncrementHandler.php
new file mode 100644
index 00000000000..d40934669d7
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Sharded/AutoIncrementHandler.php
@@ -0,0 +1,152 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\DB\QueryBuilder\Sharded;
+
+use OCP\ICacheFactory;
+use OCP\IMemcache;
+use OCP\IMemcacheTTL;
+
+/**
+ * A helper to atomically determine the next auto increment value for a sharded table
+ *
+ * Since we can't use the database's auto-increment (since each db doesn't know about the keys in the other shards)
+ * we need external logic for doing the auto increment
+ */
+class AutoIncrementHandler {
+ public const MIN_VALID_KEY = 1000;
+ public const TTL = 365 * 24 * 60 * 60;
+
+ private ?IMemcache $cache = null;
+
+ public function __construct(
+ private ICacheFactory $cacheFactory,
+ private ShardConnectionManager $shardConnectionManager,
+ ) {
+ if (PHP_INT_SIZE < 8) {
+ throw new \Exception('sharding is only supported with 64bit php');
+ }
+ }
+
+ private function getCache(): IMemcache {
+ if(is_null($this->cache)) {
+ $cache = $this->cacheFactory->createDistributed('shared_autoincrement');
+ if ($cache instanceof IMemcache) {
+ $this->cache = $cache;
+ } else {
+ throw new \Exception('Distributed cache ' . get_class($cache) . ' is not suitable');
+ }
+ }
+ return $this->cache;
+ }
+
+ /**
+ * Get the next value for the given shard definition
+ *
+ * The returned key is unique and incrementing, but not sequential.
+ * The shard id is encoded in the first byte of the returned value
+ *
+ * @param ShardDefinition $shardDefinition
+ * @return int
+ * @throws \Exception
+ */
+ public function getNextPrimaryKey(ShardDefinition $shardDefinition, int $shard): int {
+ $retries = 0;
+ while ($retries < 5) {
+ $next = $this->getNextInner($shardDefinition);
+ if ($next !== null) {
+ if ($next > ShardDefinition::MAX_PRIMARY_KEY) {
+ throw new \Exception('Max primary key of ' . ShardDefinition::MAX_PRIMARY_KEY . ' exceeded');
+ }
+ // we encode the shard the primary key was originally inserted into to allow guessing the shard by primary key later on
+ return ($next << 8) | $shard;
+ } else {
+ $retries++;
+ }
+ }
+ throw new \Exception('Failed to get next primary key');
+ }
+
+ /**
+ * auto increment logic without retry
+ *
+ * @param ShardDefinition $shardDefinition
+ * @return int|null either the next primary key or null if the call needs to be retried
+ */
+ private function getNextInner(ShardDefinition $shardDefinition): ?int {
+ $cache = $this->getCache();
+ // because this function will likely be called concurrently from different requests
+ // the implementation needs to ensure that the cached value can be cleared, invalidated or re-calculated at any point between our cache calls
+ // care must be taken that the logic remains fully resilient against race conditions
+
+ // in the ideal case, the last primary key is stored in the cache and we can just do an `inc`
+ // if that is not the case we find the highest used id in the database increment it, and save it in the cache
+
+ // prevent inc from returning `1` if the key doesn't exist by setting it to a non-numeric value
+ $cache->add($shardDefinition->table, 'empty-placeholder', self::TTL);
+ $next = $cache->inc($shardDefinition->table);
+
+ if ($cache instanceof IMemcacheTTL) {
+ $cache->setTTL($shardDefinition->table, self::TTL);
+ }
+
+ // the "add + inc" trick above isn't strictly atomic, so as a safety we reject any result that to small
+ // to handle the edge case of the stored value disappearing between the add and inc
+ if (is_int($next) && $next >= self::MIN_VALID_KEY) {
+ return $next;
+ } elseif (is_int($next)) {
+ // we hit the edge case, so invalidate the cached value
+ if (!$cache->cas($shardDefinition->table, $next, 'empty-placeholder')) {
+ // someone else is changing the value concurrently, give up and retry
+ return null;
+ }
+ }
+
+ // discard the encoded initial shard
+ $current = $this->getMaxFromDb($shardDefinition) >> 8;
+ $next = max($current, self::MIN_VALID_KEY) + 1;
+ if ($cache->cas($shardDefinition->table, 'empty-placeholder', $next)) {
+ return $next;
+ }
+
+ // another request set the cached value before us, so we should just be able to inc
+ $next = $cache->inc($shardDefinition->table);
+ if (is_int($next) && $next >= self::MIN_VALID_KEY) {
+ return $next;
+ } elseif(is_int($next)) {
+ // key got cleared, invalidate and retry
+ $cache->cas($shardDefinition->table, $next, 'empty-placeholder');
+ return null;
+ } else {
+ // cleanup any non-numeric value other than the placeholder if that got stored somehow
+ $cache->ncad($shardDefinition->table, 'empty-placeholder');
+ // retry
+ return null;
+ }
+ }
+
+ /**
+ * Get the maximum primary key value from the shards
+ */
+ private function getMaxFromDb(ShardDefinition $shardDefinition): int {
+ $max = 0;
+ foreach ($shardDefinition->getAllShards() as $shard) {
+ $connection = $this->shardConnectionManager->getConnection($shardDefinition, $shard);
+ $query = $connection->getQueryBuilder();
+ $query->select($shardDefinition->primaryKey)
+ ->from($shardDefinition->table)
+ ->orderBy($shardDefinition->primaryKey, 'DESC')
+ ->setMaxResults(1);
+ $result = $query->executeQuery()->fetchOne();
+ if ($result) {
+ $max = max($max, $result);
+ }
+ }
+ return $max;
+ }
+}
diff --git a/lib/private/DB/QueryBuilder/Sharded/CrossShardMoveHelper.php b/lib/private/DB/QueryBuilder/Sharded/CrossShardMoveHelper.php
new file mode 100644
index 00000000000..45f24e32685
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Sharded/CrossShardMoveHelper.php
@@ -0,0 +1,162 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\DB\QueryBuilder\Sharded;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+/**
+ * Utility methods for implementing logic that moves data across shards
+ */
+class CrossShardMoveHelper {
+ public function __construct(
+ private ShardConnectionManager $connectionManager
+ ) {
+ }
+
+ public function getConnection(ShardDefinition $shardDefinition, int $shardKey): IDBConnection {
+ return $this->connectionManager->getConnection($shardDefinition, $shardDefinition->getShardForKey($shardKey));
+ }
+
+ /**
+ * Update the shard key of a set of rows, moving them to a different shard if needed
+ *
+ * @param ShardDefinition $shardDefinition
+ * @param string $table
+ * @param string $shardColumn
+ * @param int $sourceShardKey
+ * @param int $targetShardKey
+ * @param string $primaryColumn
+ * @param int[] $primaryKeys
+ * @return void
+ */
+ public function moveCrossShards(ShardDefinition $shardDefinition, string $table, string $shardColumn, int $sourceShardKey, int $targetShardKey, string $primaryColumn, array $primaryKeys): void {
+ $sourceShard = $shardDefinition->getShardForKey($sourceShardKey);
+ $targetShard = $shardDefinition->getShardForKey($targetShardKey);
+ $sourceConnection = $this->connectionManager->getConnection($shardDefinition, $sourceShard);
+ if ($sourceShard === $targetShard) {
+ $this->updateItems($sourceConnection, $table, $shardColumn, $targetShardKey, $primaryColumn, $primaryKeys);
+
+ return;
+ }
+ $targetConnection = $this->connectionManager->getConnection($shardDefinition, $targetShard);
+
+ $sourceItems = $this->loadItems($sourceConnection, $table, $primaryColumn, $primaryKeys);
+ foreach ($sourceItems as &$sourceItem) {
+ $sourceItem[$shardColumn] = $targetShardKey;
+ }
+ if (!$sourceItems) {
+ return;
+ }
+
+ $sourceConnection->beginTransaction();
+ $targetConnection->beginTransaction();
+ try {
+ $this->saveItems($targetConnection, $table, $sourceItems);
+ $this->deleteItems($sourceConnection, $table, $primaryColumn, $primaryKeys);
+
+ $targetConnection->commit();
+ $sourceConnection->commit();
+ } catch (\Exception $e) {
+ $sourceConnection->rollback();
+ $targetConnection->rollback();
+ throw $e;
+ }
+ }
+
+ /**
+ * Load rows from a table to move
+ *
+ * @param IDBConnection $connection
+ * @param string $table
+ * @param string $primaryColumn
+ * @param int[] $primaryKeys
+ * @return array[]
+ */
+ public function loadItems(IDBConnection $connection, string $table, string $primaryColumn, array $primaryKeys): array {
+ $query = $connection->getQueryBuilder();
+ $query->select('*')
+ ->from($table)
+ ->where($query->expr()->in($primaryColumn, $query->createParameter('keys')));
+
+ $chunks = array_chunk($primaryKeys, 1000);
+
+ $results = [];
+ foreach ($chunks as $chunk) {
+ $query->setParameter('keys', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
+ $results = array_merge($results, $query->execute()->fetchAll());
+ }
+
+ return $results;
+ }
+
+ /**
+ * Save modified rows
+ *
+ * @param IDBConnection $connection
+ * @param string $table
+ * @param array[] $items
+ * @return void
+ */
+ public function saveItems(IDBConnection $connection, string $table, array $items): void {
+ if (count($items) === 0) {
+ return;
+ }
+ $query = $connection->getQueryBuilder();
+ $query->insert($table);
+ foreach ($items[0] as $column => $value) {
+ $query->setValue($column, $query->createParameter($column));
+ }
+
+ foreach ($items as $item) {
+ foreach ($item as $column => $value) {
+ if (is_int($column)) {
+ $query->setParameter($column, $value, IQueryBuilder::PARAM_INT);
+ } else {
+ $query->setParameter($column, $value);
+ }
+ }
+ $query->executeStatement();
+ }
+ }
+
+ /**
+ * @param IDBConnection $connection
+ * @param string $table
+ * @param string $primaryColumn
+ * @param int[] $primaryKeys
+ * @return void
+ */
+ public function updateItems(IDBConnection $connection, string $table, string $shardColumn, int $targetShardKey, string $primaryColumn, array $primaryKeys): void {
+ $query = $connection->getQueryBuilder();
+ $query->update($table)
+ ->set($shardColumn, $query->createNamedParameter($targetShardKey, IQueryBuilder::PARAM_INT))
+ ->where($query->expr()->in($primaryColumn, $query->createNamedParameter($primaryKeys, IQueryBuilder::PARAM_INT_ARRAY)));
+ $query->executeQuery()->fetchAll();
+ }
+
+ /**
+ * @param IDBConnection $connection
+ * @param string $table
+ * @param string $primaryColumn
+ * @param int[] $primaryKeys
+ * @return void
+ */
+ public function deleteItems(IDBConnection $connection, string $table, string $primaryColumn, array $primaryKeys): void {
+ $query = $connection->getQueryBuilder();
+ $query->delete($table)
+ ->where($query->expr()->in($primaryColumn, $query->createParameter('keys')));
+ $chunks = array_chunk($primaryKeys, 1000);
+
+ foreach ($chunks as $chunk) {
+ $query->setParameter('keys', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
+ $query->executeStatement();
+ }
+ }
+}
diff --git a/lib/private/DB/QueryBuilder/Sharded/HashShardMapper.php b/lib/private/DB/QueryBuilder/Sharded/HashShardMapper.php
new file mode 100644
index 00000000000..af778489a2d
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Sharded/HashShardMapper.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\DB\QueryBuilder\Sharded;
+
+use OCP\DB\QueryBuilder\Sharded\IShardMapper;
+
+/**
+ * Map string key to an int-range by hashing the key
+ */
+class HashShardMapper implements IShardMapper {
+ public function getShardForKey(int $key, int $count): int {
+ $int = unpack('L', substr(md5((string)$key, true), 0, 4))[1];
+ return $int % $count;
+ }
+}
diff --git a/lib/private/DB/QueryBuilder/Sharded/InvalidShardedQueryException.php b/lib/private/DB/QueryBuilder/Sharded/InvalidShardedQueryException.php
new file mode 100644
index 00000000000..733a6acaf9d
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Sharded/InvalidShardedQueryException.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OC\DB\QueryBuilder\Sharded;
+
+/**
+ * Queries on sharded table has the following limitations:
+ *
+ * 1. Either the shard key (e.g. "storage") or primary key (e.g. "fileid") must be mentioned in the query.
+ * Or the query must be explicitly marked as running across all shards.
+ *
+ * For queries where it isn't possible to set one of these keys in the query normally, you can set it using `hintShardKey`
+ *
+ * 2. Insert statements must always explicitly set the shard key
+ * 3. A query on a sharded table is not allowed to join on the same table
+ * 4. Right joins are not allowed on sharded tables
+ * 5. Updating the shard key where the new shard key maps to a different shard is not allowed
+ *
+ * Moving rows to a different shard needs to be implemented manually. `CrossShardMoveHelper` provides
+ * some tools to help make this easier.
+ */
+class InvalidShardedQueryException extends \Exception {
+
+}
diff --git a/lib/private/DB/QueryBuilder/Sharded/RoundRobinShardMapper.php b/lib/private/DB/QueryBuilder/Sharded/RoundRobinShardMapper.php
new file mode 100644
index 00000000000..a5694b06507
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Sharded/RoundRobinShardMapper.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\DB\QueryBuilder\Sharded;
+
+use OCP\DB\QueryBuilder\Sharded\IShardMapper;
+
+/**
+ * Map string key to an int-range by hashing the key
+ */
+class RoundRobinShardMapper implements IShardMapper {
+ public function getShardForKey(int $key, int $count): int {
+ return $key % $count;
+ }
+}
diff --git a/lib/private/DB/QueryBuilder/Sharded/ShardConnectionManager.php b/lib/private/DB/QueryBuilder/Sharded/ShardConnectionManager.php
new file mode 100644
index 00000000000..87cac58bc57
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Sharded/ShardConnectionManager.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\DB\QueryBuilder\Sharded;
+
+use OC\DB\ConnectionAdapter;
+use OC\DB\ConnectionFactory;
+use OC\SystemConfig;
+use OCP\IDBConnection;
+
+/**
+ * Keeps track of the db connections to the various shards
+ */
+class ShardConnectionManager {
+ /** @var array<string, IDBConnection> */
+ private array $connections = [];
+
+ public function __construct(
+ private SystemConfig $config,
+ private ConnectionFactory $factory,
+ ) {
+ }
+
+ public function getConnection(ShardDefinition $shardDefinition, int $shard): IDBConnection {
+ $connectionKey = $shardDefinition->table . '_' . $shard;
+ if (!isset($this->connections[$connectionKey])) {
+ $this->connections[$connectionKey] = $this->createConnection($shardDefinition->shards[$shard]);
+ }
+
+ return $this->connections[$connectionKey];
+ }
+
+ private function createConnection(array $shardConfig): IDBConnection {
+ $shardConfig['sharding'] = [];
+ $type = $this->config->getValue('dbtype', 'sqlite');
+ return new ConnectionAdapter($this->factory->getConnection($type, $shardConfig));
+ }
+}
diff --git a/lib/private/DB/QueryBuilder/Sharded/ShardDefinition.php b/lib/private/DB/QueryBuilder/Sharded/ShardDefinition.php
new file mode 100644
index 00000000000..ebccbb639a6
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Sharded/ShardDefinition.php
@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\DB\QueryBuilder\Sharded;
+
+use OCP\DB\QueryBuilder\Sharded\IShardMapper;
+
+/**
+ * Configuration for a shard setup
+ */
+class ShardDefinition {
+ // we reserve the bottom byte of the primary key for the initial shard, so the total shard count is limited to what we can fit there
+ public const MAX_SHARDS = 256;
+
+ public const PRIMARY_KEY_MASK = 0x7F_FF_FF_FF_FF_FF_FF_00;
+ public const PRIMARY_KEY_SHARD_MASK = 0x00_00_00_00_00_00_00_FF;
+ // since we reserve 1 byte for the shard index, we only have 56 bits of primary key space
+ public const MAX_PRIMARY_KEY = PHP_INT_MAX >> 8;
+
+ /**
+ * @param string $table
+ * @param string $primaryKey
+ * @param string $shardKey
+ * @param string[] $companionKeys
+ * @param IShardMapper $shardMapper
+ * @param string[] $companionTables
+ * @param array $shards
+ */
+ public function __construct(
+ public string $table,
+ public string $primaryKey,
+ public array $companionKeys,
+ public string $shardKey,
+ public IShardMapper $shardMapper,
+ public array $companionTables = [],
+ public array $shards = [],
+ ) {
+ if (count($this->shards) >= self::MAX_SHARDS) {
+ throw new \Exception('Only allowed maximum of ' . self::MAX_SHARDS . ' shards allowed');
+ }
+ }
+
+ public function hasTable(string $table): bool {
+ if ($this->table === $table) {
+ return true;
+ }
+ return in_array($table, $this->companionTables);
+ }
+
+ public function getShardForKey(int $key): int {
+ return $this->shardMapper->getShardForKey($key, count($this->shards));
+ }
+
+ public function getAllShards(): array {
+ return array_keys($this->shards);
+ }
+
+ public function isKey(string $column): bool {
+ return $column === $this->primaryKey || in_array($column, $this->companionKeys);
+ }
+}
diff --git a/lib/private/DB/QueryBuilder/Sharded/ShardQueryRunner.php b/lib/private/DB/QueryBuilder/Sharded/ShardQueryRunner.php
new file mode 100644
index 00000000000..51cd055e801
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Sharded/ShardQueryRunner.php
@@ -0,0 +1,197 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\DB\QueryBuilder\Sharded;
+
+use OC\DB\ArrayResult;
+use OCP\DB\IResult;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+
+/**
+ * Logic for running a query across a number of shards, combining the results
+ */
+class ShardQueryRunner {
+ public function __construct(
+ private ShardConnectionManager $shardConnectionManager,
+ private ShardDefinition $shardDefinition,
+ ) {
+ }
+
+ /**
+ * Get the shards for a specific query or null if the shards aren't known in advance
+ *
+ * @param bool $allShards
+ * @param int[] $shardKeys
+ * @return null|int[]
+ */
+ public function getShards(bool $allShards, array $shardKeys): ?array {
+ if ($allShards) {
+ return $this->shardDefinition->getAllShards();
+ }
+ $allConfiguredShards = $this->shardDefinition->getAllShards();
+ if (count($allConfiguredShards) === 1) {
+ return $allConfiguredShards;
+ }
+ if (empty($shardKeys)) {
+ return null;
+ }
+ $shards = array_map(function ($shardKey) {
+ return $this->shardDefinition->getShardForKey((int)$shardKey);
+ }, $shardKeys);
+ return array_values(array_unique($shards));
+ }
+
+ /**
+ * Try to get the shards that the keys are likely to be in, based on the shard the row was created
+ *
+ * @param int[] $primaryKeys
+ * @return int[]
+ */
+ private function getLikelyShards(array $primaryKeys): array {
+ $shards = [];
+ foreach ($primaryKeys as $primaryKey) {
+ $encodedShard = $primaryKey & ShardDefinition::PRIMARY_KEY_SHARD_MASK;
+ if ($encodedShard < count($this->shardDefinition->shards) && !in_array($encodedShard, $shards)) {
+ $shards[] = $encodedShard;
+ }
+ }
+ return $shards;
+ }
+
+ /**
+ * Execute a SELECT statement across the configured shards
+ *
+ * @param IQueryBuilder $query
+ * @param bool $allShards
+ * @param int[] $shardKeys
+ * @param int[] $primaryKeys
+ * @param array{column: string, order: string}[] $sortList
+ * @param int|null $limit
+ * @param int|null $offset
+ * @return IResult
+ */
+ public function executeQuery(
+ IQueryBuilder $query,
+ bool $allShards,
+ array $shardKeys,
+ array $primaryKeys,
+ ?array $sortList = null,
+ ?int $limit = null,
+ ?int $offset = null,
+ ): IResult {
+ $shards = $this->getShards($allShards, $shardKeys);
+ $results = [];
+ if ($shards && count($shards) === 1) {
+ // trivial case
+ return $query->executeQuery($this->shardConnectionManager->getConnection($this->shardDefinition, $shards[0]));
+ }
+ // we have to emulate limit and offset, so we select offset+limit from all shards to ensure we have enough rows
+ // and then filter them down after we merged the results
+ if ($limit !== null && $offset !== null) {
+ $query->setMaxResults($limit + $offset);
+ }
+
+ if ($shards) {
+ // we know exactly what shards we need to query
+ foreach ($shards as $shard) {
+ $shardConnection = $this->shardConnectionManager->getConnection($this->shardDefinition, $shard);
+ $subResult = $query->executeQuery($shardConnection);
+ $results = array_merge($results, $subResult->fetchAll());
+ $subResult->closeCursor();
+ }
+ } else {
+ // we don't know for sure what shards we need to query,
+ // we first try the shards that are "likely" to have the rows we want, based on the shard that the row was
+ // originally created in. If we then still haven't found all rows we try the rest of the shards
+ $likelyShards = $this->getLikelyShards($primaryKeys);
+ $unlikelyShards = array_diff($this->shardDefinition->getAllShards(), $likelyShards);
+ $shards = array_merge($likelyShards, $unlikelyShards);
+
+ foreach ($shards as $shard) {
+ $shardConnection = $this->shardConnectionManager->getConnection($this->shardDefinition, $shard);
+ $subResult = $query->executeQuery($shardConnection);
+ $rows = $subResult->fetchAll();
+ $results = array_merge($results, $rows);
+ $subResult->closeCursor();
+
+ if (count($rows) >= count($primaryKeys)) {
+ // we have all the rows we're looking for
+ break;
+ }
+ }
+ }
+
+ if ($sortList) {
+ usort($results, function ($a, $b) use ($sortList) {
+ foreach ($sortList as $sort) {
+ $valueA = $a[$sort['column']] ?? null;
+ $valueB = $b[$sort['column']] ?? null;
+ $cmp = $valueA <=> $valueB;
+ if ($cmp === 0) {
+ continue;
+ }
+ if ($sort['order'] === 'DESC') {
+ $cmp = -$cmp;
+ }
+ return $cmp;
+ }
+ });
+ }
+
+ if ($limit !== null && $offset !== null) {
+ $results = array_slice($results, $offset, $limit);
+ } elseif ($limit !== null) {
+ $results = array_slice($results, 0, $limit);
+ } elseif ($offset !== null) {
+ $results = array_slice($results, $offset);
+ }
+
+ return new ArrayResult($results);
+ }
+
+ /**
+ * Execute an UPDATE or DELETE statement
+ *
+ * @param IQueryBuilder $query
+ * @param bool $allShards
+ * @param int[] $shardKeys
+ * @param int[] $primaryKeys
+ * @return int
+ * @throws \OCP\DB\Exception
+ */
+ public function executeStatement(IQueryBuilder $query, bool $allShards, array $shardKeys, array $primaryKeys): int {
+ if ($query->getType() === \Doctrine\DBAL\Query\QueryBuilder::INSERT) {
+ throw new \Exception('insert queries need special handling');
+ }
+
+ $shards = $this->getShards($allShards, $shardKeys);
+ $maxCount = count($primaryKeys);
+ if ($shards && count($shards) === 1) {
+ return $query->executeStatement($this->shardConnectionManager->getConnection($this->shardDefinition, $shards[0]));
+ } elseif ($shards) {
+ $maxCount = PHP_INT_MAX;
+ } else {
+ // sort the likely shards before the rest, similar logic to `self::executeQuery`
+ $likelyShards = $this->getLikelyShards($primaryKeys);
+ $unlikelyShards = array_diff($this->shardDefinition->getAllShards(), $likelyShards);
+ $shards = array_merge($likelyShards, $unlikelyShards);
+ }
+
+ $count = 0;
+
+ foreach ($shards as $shard) {
+ $shardConnection = $this->shardConnectionManager->getConnection($this->shardDefinition, $shard);
+ $count += $query->executeStatement($shardConnection);
+
+ if ($count >= $maxCount) {
+ break;
+ }
+ }
+ return $count;
+ }
+}
diff --git a/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php b/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php
new file mode 100644
index 00000000000..e7bc70ce440
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php
@@ -0,0 +1,407 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\DB\QueryBuilder\Sharded;
+
+use OC\DB\QueryBuilder\CompositeExpression;
+use OC\DB\QueryBuilder\ExtendedQueryBuilder;
+use OC\DB\QueryBuilder\Parameter;
+use OCP\DB\IResult;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+/**
+ * A special query builder that automatically distributes queries over multiple database shards.
+ *
+ * This relies on `PartitionedQueryBuilder` to handle splitting of parts of the query that touch the sharded tables
+ * from the non-sharded tables. So the query build here should only either touch only sharded table or only non-sharded tables.
+ *
+ * Most of the logic in this class is concerned with extracting either the shard key (e.g. "storage") or primary key (e.g. "fileid")
+ * from the query. The logic for actually running the query across the shards is mostly delegated to `ShardQueryRunner`.
+ */
+class ShardedQueryBuilder extends ExtendedQueryBuilder {
+ private array $shardKeys = [];
+ private array $primaryKeys = [];
+ private ?ShardDefinition $shardDefinition = null;
+ /** @var bool Run the query across all shards */
+ private bool $allShards = false;
+ private ?string $insertTable = null;
+ private mixed $lastInsertId = null;
+ private ?IDBConnection $lastInsertConnection = null;
+ private ?int $updateShardKey = null;
+ private ?int $limit = null;
+ private ?int $offset = null;
+ /** @var array{column: string, order: string}[] */
+ private array $sortList = [];
+ private string $mainTable = '';
+
+ public function __construct(
+ IQueryBuilder $builder,
+ protected array $shardDefinitions,
+ protected ShardConnectionManager $shardConnectionManager,
+ protected AutoIncrementHandler $autoIncrementHandler,
+ ) {
+ parent::__construct($builder);
+ }
+
+ public function getShardKeys(): array {
+ return $this->getKeyValues($this->shardKeys);
+ }
+
+ public function getPrimaryKeys(): array {
+ return $this->getKeyValues($this->primaryKeys);
+ }
+
+ private function getKeyValues(array $keys): array {
+ $values = [];
+ foreach ($keys as $key) {
+ $values = array_merge($values, $this->getKeyValue($key));
+ }
+ return array_values(array_unique($values));
+ }
+
+ private function getKeyValue($value): array {
+ if ($value instanceof Parameter) {
+ $value = (string)$value;
+ }
+ if (is_string($value) && str_starts_with($value, ':')) {
+ $param = $this->getParameter(substr($value, 1));
+ if (is_array($param)) {
+ return $param;
+ } else {
+ return [$param];
+ }
+ } elseif ($value !== null) {
+ return [$value];
+ } else {
+ return [];
+ }
+ }
+
+ public function where(...$predicates) {
+ return $this->andWhere(...$predicates);
+ }
+
+ public function andWhere(...$where) {
+ if ($where) {
+ foreach ($where as $predicate) {
+ $this->tryLoadShardKey($predicate);
+ }
+ parent::andWhere(...$where);
+ }
+ return $this;
+ }
+
+ private function tryLoadShardKey($predicate): void {
+ if (!$this->shardDefinition) {
+ return;
+ }
+ if ($keys = $this->tryExtractShardKeys($predicate, $this->shardDefinition->shardKey)) {
+ $this->shardKeys += $keys;
+ }
+ if ($keys = $this->tryExtractShardKeys($predicate, $this->shardDefinition->primaryKey)) {
+ $this->primaryKeys += $keys;
+ }
+ foreach ($this->shardDefinition->companionKeys as $companionKey) {
+ if ($keys = $this->tryExtractShardKeys($predicate, $companionKey)) {
+ $this->primaryKeys += $keys;
+ }
+ }
+ }
+
+ /**
+ * @param $predicate
+ * @param string $column
+ * @return string[]
+ */
+ private function tryExtractShardKeys($predicate, string $column): array {
+ if ($predicate instanceof CompositeExpression) {
+ $values = [];
+ foreach ($predicate->getParts() as $part) {
+ $partValues = $this->tryExtractShardKeys($part, $column);
+ // for OR expressions, we can only rely on the predicate if all parts contain the comparison
+ if ($predicate->getType() === CompositeExpression::TYPE_OR && !$partValues) {
+ return [];
+ }
+ $values = array_merge($values, $partValues);
+ }
+ return $values;
+ }
+ $predicate = (string)$predicate;
+ // expect a condition in the form of 'alias1.column1 = placeholder' or 'alias1.column1 in placeholder'
+ if (substr_count($predicate, ' ') > 2) {
+ return [];
+ }
+ if (str_contains($predicate, ' = ')) {
+ $parts = explode(' = ', $predicate);
+ if ($parts[0] === "`{$column}`" || str_ends_with($parts[0], "`.`{$column}`")) {
+ return [$parts[1]];
+ } else {
+ return [];
+ }
+ }
+
+ if (str_contains($predicate, ' IN ')) {
+ $parts = explode(' IN ', $predicate);
+ if ($parts[0] === "`{$column}`" || str_ends_with($parts[0], "`.`{$column}`")) {
+ return [trim(trim($parts[1], '('), ')')];
+ } else {
+ return [];
+ }
+ }
+
+ return [];
+ }
+
+ public function set($key, $value) {
+ if ($this->shardDefinition && $key === $this->shardDefinition->shardKey) {
+ $updateShardKey = $value;
+ }
+ return parent::set($key, $value);
+ }
+
+ public function setValue($column, $value) {
+ if ($this->shardDefinition) {
+ if ($this->shardDefinition->isKey($column)) {
+ $this->primaryKeys[] = $value;
+ }
+ if ($column === $this->shardDefinition->shardKey) {
+ $this->shardKeys[] = $value;
+ }
+ }
+ return parent::setValue($column, $value);
+ }
+
+ public function values(array $values) {
+ foreach ($values as $column => $value) {
+ $this->setValue($column, $value);
+ }
+ return $this;
+ }
+
+ private function actOnTable(string $table): void {
+ $this->mainTable = $table;
+ foreach ($this->shardDefinitions as $shardDefinition) {
+ if ($shardDefinition->hasTable($table)) {
+ $this->shardDefinition = $shardDefinition;
+ }
+ }
+ }
+
+ public function from($from, $alias = null) {
+ if (is_string($from) && $from) {
+ $this->actOnTable($from);
+ }
+ return parent::from($from, $alias);
+ }
+
+ public function update($update = null, $alias = null) {
+ if (is_string($update) && $update) {
+ $this->actOnTable($update);
+ }
+ return parent::update($update, $alias);
+ }
+
+ public function insert($insert = null) {
+ if (is_string($insert) && $insert) {
+ $this->insertTable = $insert;
+ $this->actOnTable($insert);
+ }
+ return parent::insert($insert);
+ }
+
+ public function delete($delete = null, $alias = null) {
+ if (is_string($delete) && $delete) {
+ $this->actOnTable($delete);
+ }
+ return parent::delete($delete, $alias);
+ }
+
+ private function checkJoin(string $table): void {
+ if ($this->shardDefinition) {
+ if ($table === $this->mainTable) {
+ throw new InvalidShardedQueryException("Sharded query on {$this->mainTable} isn't allowed to join on itself");
+ }
+ if (!$this->shardDefinition->hasTable($table)) {
+ // this generally shouldn't happen as the partitioning logic should prevent this
+ // but the check is here just in case
+ throw new InvalidShardedQueryException("Sharded query on {$this->shardDefinition->table} isn't allowed to join on $table");
+ }
+ }
+ }
+
+ public function innerJoin($fromAlias, $join, $alias, $condition = null) {
+ $this->checkJoin($join);
+ return parent::innerJoin($fromAlias, $join, $alias, $condition);
+ }
+
+ public function leftJoin($fromAlias, $join, $alias, $condition = null) {
+ $this->checkJoin($join);
+ return parent::leftJoin($fromAlias, $join, $alias, $condition);
+ }
+
+ public function rightJoin($fromAlias, $join, $alias, $condition = null) {
+ if ($this->shardDefinition) {
+ throw new InvalidShardedQueryException("Sharded query on {$this->shardDefinition->table} isn't allowed to right join");
+ }
+ return parent::rightJoin($fromAlias, $join, $alias, $condition);
+ }
+
+ public function join($fromAlias, $join, $alias, $condition = null) {
+ return $this->innerJoin($fromAlias, $join, $alias, $condition);
+ }
+
+ public function setMaxResults($maxResults) {
+ if ($maxResults > 0) {
+ $this->limit = (int)$maxResults;
+ }
+ return parent::setMaxResults($maxResults);
+ }
+
+ public function setFirstResult($firstResult) {
+ if ($firstResult > 0) {
+ $this->offset = (int)$firstResult;
+ }
+ if ($this->shardDefinition && count($this->shardDefinition->shards) > 1) {
+ // we have to emulate offset
+ return $this;
+ } else {
+ return parent::setFirstResult($firstResult);
+ }
+ }
+
+ public function addOrderBy($sort, $order = null) {
+ $this->registerOrder((string)$sort, (string)$order ?? 'ASC');
+ return parent::orderBy($sort, $order);
+ }
+
+ public function orderBy($sort, $order = null) {
+ $this->sortList = [];
+ $this->registerOrder((string)$sort, (string)$order ?? 'ASC');
+ return parent::orderBy($sort, $order);
+ }
+
+ private function registerOrder(string $column, string $order): void {
+ // handle `mime + 0` and similar by just sorting on the first part of the expression
+ [$column] = explode(' ', $column);
+ $column = trim($column, '`');
+ $this->sortList[] = [
+ 'column' => $column,
+ 'order' => strtoupper($order),
+ ];
+ }
+
+ public function hintShardKey(string $column, mixed $value, bool $overwrite = false) {
+ if ($overwrite) {
+ $this->primaryKeys = [];
+ $this->shardKeys = [];
+ }
+ if ($this->shardDefinition?->isKey($column)) {
+ $this->primaryKeys[] = $value;
+ }
+ if ($column === $this->shardDefinition?->shardKey) {
+ $this->shardKeys[] = $value;
+ }
+ return $this;
+ }
+
+ public function runAcrossAllShards() {
+ $this->allShards = true;
+ return $this;
+ }
+
+ /**
+ * @throws InvalidShardedQueryException
+ */
+ public function validate(): void {
+ if ($this->shardDefinition && $this->insertTable) {
+ if ($this->allShards) {
+ throw new InvalidShardedQueryException("Can't insert across all shards");
+ }
+ if (empty($this->getShardKeys())) {
+ throw new InvalidShardedQueryException("Can't insert without shard key");
+ }
+ }
+ if ($this->shardDefinition && !$this->allShards) {
+ if (empty($this->getShardKeys()) && empty($this->getPrimaryKeys())) {
+ throw new InvalidShardedQueryException('No shard key or primary key set for query');
+ }
+ }
+ if ($this->shardDefinition && $this->updateShardKey) {
+ $newShardKey = $this->getKeyValue($this->updateShardKey);
+ $oldShardKeys = $this->getShardKeys();
+ if (count($newShardKey) !== 1) {
+ throw new InvalidShardedQueryException("Can't set shard key to an array");
+ }
+ $newShardKey = current($newShardKey);
+ if (empty($oldShardKeys)) {
+ throw new InvalidShardedQueryException("Can't update without shard key");
+ }
+ $oldShards = array_values(array_unique(array_map(function ($shardKey) {
+ return $this->shardDefinition->getShardForKey((int)$shardKey);
+ }, $oldShardKeys)));
+ $newShard = $this->shardDefinition->getShardForKey((int)$newShardKey);
+ if ($oldShards === [$newShard]) {
+ throw new InvalidShardedQueryException('Update statement would move rows to a different shard');
+ }
+ }
+ }
+
+ public function executeQuery(?IDBConnection $connection = null): IResult {
+ $this->validate();
+ if ($this->shardDefinition) {
+ $runner = new ShardQueryRunner($this->shardConnectionManager, $this->shardDefinition);
+ return $runner->executeQuery($this->builder, $this->allShards, $this->getShardKeys(), $this->getPrimaryKeys(), $this->sortList, $this->limit, $this->offset);
+ }
+ return parent::executeQuery($connection);
+ }
+
+ public function executeStatement(?IDBConnection $connection = null): int {
+ $this->validate();
+ if ($this->shardDefinition) {
+ $runner = new ShardQueryRunner($this->shardConnectionManager, $this->shardDefinition);
+ if ($this->insertTable) {
+ $shards = $runner->getShards($this->allShards, $this->getShardKeys());
+ if (!$shards) {
+ throw new InvalidShardedQueryException("Can't insert without shard key");
+ }
+ $count = 0;
+ foreach ($shards as $shard) {
+ $shardConnection = $this->shardConnectionManager->getConnection($this->shardDefinition, $shard);
+ if (!$this->primaryKeys && $this->shardDefinition->table === $this->insertTable) {
+ $id = $this->autoIncrementHandler->getNextPrimaryKey($this->shardDefinition, $shard);
+ parent::setValue($this->shardDefinition->primaryKey, $this->createParameter('__generated_primary_key'));
+ $this->setParameter('__generated_primary_key', $id, self::PARAM_INT);
+ $this->lastInsertId = $id;
+ }
+ $count += parent::executeStatement($shardConnection);
+
+ $this->lastInsertConnection = $shardConnection;
+ }
+ return $count;
+ } else {
+ return $runner->executeStatement($this->builder, $this->allShards, $this->getShardKeys(), $this->getPrimaryKeys());
+ }
+ }
+ return parent::executeStatement($connection);
+ }
+
+ public function getLastInsertId(): int {
+ if ($this->lastInsertId) {
+ return $this->lastInsertId;
+ }
+ if ($this->lastInsertConnection) {
+ $table = $this->builder->prefixTableName($this->insertTable);
+ return $this->lastInsertConnection->lastInsertId($table);
+ } else {
+ return parent::getLastInsertId();
+ }
+ }
+
+
+}
diff --git a/lib/private/DB/SchemaWrapper.php b/lib/private/DB/SchemaWrapper.php
index 5720e10fbdb..473c0009237 100644
--- a/lib/private/DB/SchemaWrapper.php
+++ b/lib/private/DB/SchemaWrapper.php
@@ -36,6 +36,9 @@ class SchemaWrapper implements ISchemaWrapper {
public function performDropTableCalls() {
foreach ($this->tablesToDelete as $tableName => $true) {
$this->connection->dropTable($tableName);
+ foreach ($this->connection->getShardConnections() as $shardConnection) {
+ $shardConnection->dropTable($tableName);
+ }
unset($this->tablesToDelete[$tableName]);
}
}
diff --git a/lib/private/DateTimeFormatter.php b/lib/private/DateTimeFormatter.php
index cd765a2a14b..2882a7d8cd7 100644
--- a/lib/private/DateTimeFormatter.php
+++ b/lib/private/DateTimeFormatter.php
@@ -28,8 +28,8 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter {
/**
* Get TimeZone to use
*
- * @param \DateTimeZone $timeZone The timezone to use
- * @return \DateTimeZone The timezone to use, falling back to the current user's timezone
+ * @param \DateTimeZone $timeZone The timezone to use
+ * @return \DateTimeZone The timezone to use, falling back to the current user's timezone
*/
protected function getTimeZone($timeZone = null) {
if ($timeZone === null) {
@@ -42,8 +42,8 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter {
/**
* Get \OCP\IL10N to use
*
- * @param \OCP\IL10N $l The locale to use
- * @return \OCP\IL10N The locale to use, falling back to the current user's locale
+ * @param \OCP\IL10N $l The locale to use
+ * @return \OCP\IL10N The locale to use, falling back to the current user's locale
*/
protected function getLocale($l = null) {
if ($l === null) {
@@ -57,7 +57,7 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter {
* Generates a DateTime object with the given timestamp and TimeZone
*
* @param mixed $timestamp
- * @param \DateTimeZone $timeZone The timezone to use
+ * @param \DateTimeZone $timeZone The timezone to use
* @return \DateTime
*/
protected function getDateTime($timestamp, ?\DateTimeZone $timeZone = null) {
@@ -77,15 +77,15 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter {
/**
* Formats the date of the given timestamp
*
- * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
- * @param string $format Either 'full', 'long', 'medium' or 'short'
- * full: e.g. 'EEEE, MMMM d, y' => 'Wednesday, August 20, 2014'
- * long: e.g. 'MMMM d, y' => 'August 20, 2014'
- * medium: e.g. 'MMM d, y' => 'Aug 20, 2014'
- * short: e.g. 'M/d/yy' => '8/20/14'
- * The exact format is dependent on the language
- * @param \DateTimeZone $timeZone The timezone to use
- * @param \OCP\IL10N $l The locale to use
+ * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
+ * @param string $format Either 'full', 'long', 'medium' or 'short'
+ * full: e.g. 'EEEE, MMMM d, y' => 'Wednesday, August 20, 2014'
+ * long: e.g. 'MMMM d, y' => 'August 20, 2014'
+ * medium: e.g. 'MMM d, y' => 'Aug 20, 2014'
+ * short: e.g. 'M/d/yy' => '8/20/14'
+ * The exact format is dependent on the language
+ * @param \DateTimeZone $timeZone The timezone to use
+ * @param \OCP\IL10N $l The locale to use
* @return string Formatted date string
*/
public function formatDate($timestamp, $format = 'long', ?\DateTimeZone $timeZone = null, ?\OCP\IL10N $l = null) {
@@ -95,16 +95,16 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter {
/**
* Formats the date of the given timestamp
*
- * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
- * @param string $format Either 'full', 'long', 'medium' or 'short'
- * full: e.g. 'EEEE, MMMM d, y' => 'Wednesday, August 20, 2014'
- * long: e.g. 'MMMM d, y' => 'August 20, 2014'
- * medium: e.g. 'MMM d, y' => 'Aug 20, 2014'
- * short: e.g. 'M/d/yy' => '8/20/14'
- * The exact format is dependent on the language
- * Uses 'Today', 'Yesterday' and 'Tomorrow' when applicable
- * @param \DateTimeZone $timeZone The timezone to use
- * @param \OCP\IL10N $l The locale to use
+ * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
+ * @param string $format Either 'full', 'long', 'medium' or 'short'
+ * full: e.g. 'EEEE, MMMM d, y' => 'Wednesday, August 20, 2014'
+ * long: e.g. 'MMMM d, y' => 'August 20, 2014'
+ * medium: e.g. 'MMM d, y' => 'Aug 20, 2014'
+ * short: e.g. 'M/d/yy' => '8/20/14'
+ * The exact format is dependent on the language
+ * Uses 'Today', 'Yesterday' and 'Tomorrow' when applicable
+ * @param \DateTimeZone $timeZone The timezone to use
+ * @param \OCP\IL10N $l The locale to use
* @return string Formatted relative date string
*/
public function formatDateRelativeDay($timestamp, $format = 'long', ?\DateTimeZone $timeZone = null, ?\OCP\IL10N $l = null) {
@@ -119,13 +119,13 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter {
* Gives the relative date of the timestamp
* Only works for past dates
*
- * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
- * @param int|\DateTime $baseTimestamp Timestamp to compare $timestamp against, defaults to current time
- * @param \OCP\IL10N $l The locale to use
+ * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
+ * @param int|\DateTime $baseTimestamp Timestamp to compare $timestamp against, defaults to current time
+ * @param \OCP\IL10N $l The locale to use
* @return string Formatted date span. Dates returned are:
- * < 1 month => Today, Yesterday, n days ago
- * < 13 month => last month, n months ago
- * >= 13 month => last year, n years ago
+ * < 1 month => Today, Yesterday, n days ago
+ * < 13 month => last month, n months ago
+ * >= 13 month => last year, n years ago
*/
public function formatDateSpan($timestamp, $baseTimestamp = null, ?\OCP\IL10N $l = null) {
$l = $this->getLocale($l);
@@ -182,15 +182,15 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter {
/**
* Formats the time of the given timestamp
*
- * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
- * @param string $format Either 'full', 'long', 'medium' or 'short'
- * full: e.g. 'h:mm:ss a zzzz' => '11:42:13 AM GMT+0:00'
- * long: e.g. 'h:mm:ss a z' => '11:42:13 AM GMT'
- * medium: e.g. 'h:mm:ss a' => '11:42:13 AM'
- * short: e.g. 'h:mm a' => '11:42 AM'
- * The exact format is dependent on the language
- * @param \DateTimeZone $timeZone The timezone to use
- * @param \OCP\IL10N $l The locale to use
+ * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
+ * @param string $format Either 'full', 'long', 'medium' or 'short'
+ * full: e.g. 'h:mm:ss a zzzz' => '11:42:13 AM GMT+0:00'
+ * long: e.g. 'h:mm:ss a z' => '11:42:13 AM GMT'
+ * medium: e.g. 'h:mm:ss a' => '11:42:13 AM'
+ * short: e.g. 'h:mm a' => '11:42 AM'
+ * The exact format is dependent on the language
+ * @param \DateTimeZone $timeZone The timezone to use
+ * @param \OCP\IL10N $l The locale to use
* @return string Formatted time string
*/
public function formatTime($timestamp, $format = 'medium', ?\DateTimeZone $timeZone = null, ?\OCP\IL10N $l = null) {
@@ -200,16 +200,16 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter {
/**
* Gives the relative past time of the timestamp
*
- * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
- * @param int|\DateTime $baseTimestamp Timestamp to compare $timestamp against, defaults to current time
- * @param \OCP\IL10N $l The locale to use
+ * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
+ * @param int|\DateTime $baseTimestamp Timestamp to compare $timestamp against, defaults to current time
+ * @param \OCP\IL10N $l The locale to use
* @return string Formatted time span. Dates returned are:
- * < 60 sec => seconds ago
- * < 1 hour => n minutes ago
- * < 1 day => n hours ago
- * < 1 month => Yesterday, n days ago
- * < 13 month => last month, n months ago
- * >= 13 month => last year, n years ago
+ * < 60 sec => seconds ago
+ * < 1 hour => n minutes ago
+ * < 1 day => n hours ago
+ * < 1 month => Yesterday, n days ago
+ * < 13 month => last month, n months ago
+ * >= 13 month => last year, n years ago
*/
public function formatTimeSpan($timestamp, $baseTimestamp = null, ?\OCP\IL10N $l = null) {
$l = $this->getLocale($l);
@@ -247,11 +247,11 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter {
/**
* Formats the date and time of the given timestamp
*
- * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
- * @param string $formatDate See formatDate() for description
- * @param string $formatTime See formatTime() for description
- * @param \DateTimeZone $timeZone The timezone to use
- * @param \OCP\IL10N $l The locale to use
+ * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
+ * @param string $formatDate See formatDate() for description
+ * @param string $formatTime See formatTime() for description
+ * @param \DateTimeZone $timeZone The timezone to use
+ * @param \OCP\IL10N $l The locale to use
* @return string Formatted date and time string
*/
public function formatDateTime($timestamp, $formatDate = 'long', $formatTime = 'medium', ?\DateTimeZone $timeZone = null, ?\OCP\IL10N $l = null) {
@@ -261,12 +261,12 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter {
/**
* Formats the date and time of the given timestamp
*
- * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
- * @param string $formatDate See formatDate() for description
- * Uses 'Today', 'Yesterday' and 'Tomorrow' when applicable
- * @param string $formatTime See formatTime() for description
- * @param \DateTimeZone $timeZone The timezone to use
- * @param \OCP\IL10N $l The locale to use
+ * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
+ * @param string $formatDate See formatDate() for description
+ * Uses 'Today', 'Yesterday' and 'Tomorrow' when applicable
+ * @param string $formatTime See formatTime() for description
+ * @param \DateTimeZone $timeZone The timezone to use
+ * @param \OCP\IL10N $l The locale to use
* @return string Formatted relative date and time string
*/
public function formatDateTimeRelativeDay($timestamp, $formatDate = 'long', $formatTime = 'medium', ?\DateTimeZone $timeZone = null, ?\OCP\IL10N $l = null) {
@@ -280,11 +280,11 @@ class DateTimeFormatter implements \OCP\IDateTimeFormatter {
/**
* Formats the date and time of the given timestamp
*
- * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
- * @param string $type One of 'date', 'datetime' or 'time'
- * @param string $format Format string
- * @param \DateTimeZone $timeZone The timezone to use
- * @param \OCP\IL10N $l The locale to use
+ * @param int|\DateTime $timestamp Either a Unix timestamp or DateTime object
+ * @param string $type One of 'date', 'datetime' or 'time'
+ * @param string $format Format string
+ * @param \DateTimeZone $timeZone The timezone to use
+ * @param \OCP\IL10N $l The locale to use
* @return string Formatted date and time string
*/
protected function format($timestamp, $type, $format, ?\DateTimeZone $timeZone = null, ?\OCP\IL10N $l = null) {
diff --git a/lib/private/DirectEditing/Token.php b/lib/private/DirectEditing/Token.php
index 594cef98086..12ad9411216 100644
--- a/lib/private/DirectEditing/Token.php
+++ b/lib/private/DirectEditing/Token.php
@@ -42,7 +42,7 @@ class Token implements IToken {
}
public function hasBeenAccessed(): bool {
- return (bool) $this->data['accessed'];
+ return (bool)$this->data['accessed'];
}
public function getEditor(): string {
diff --git a/lib/private/Encryption/DecryptAll.php b/lib/private/Encryption/DecryptAll.php
index f9a92d07d20..0007467298c 100644
--- a/lib/private/Encryption/DecryptAll.php
+++ b/lib/private/Encryption/DecryptAll.php
@@ -17,13 +17,13 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DecryptAll {
- /** @var OutputInterface */
+ /** @var OutputInterface */
protected $output;
- /** @var InputInterface */
+ /** @var InputInterface */
protected $input;
- /** @var array files which couldn't be decrypted */
+ /** @var array files which couldn't be decrypted */
protected $failed;
public function __construct(
@@ -114,7 +114,7 @@ class DecryptAll {
$fetchUsersProgress = new ProgressBar($this->output);
$fetchUsersProgress->setFormat(" %message% \n [%bar%]");
$fetchUsersProgress->start();
- $fetchUsersProgress->setMessage("Fetch list of users...");
+ $fetchUsersProgress->setMessage('Fetch list of users...');
$fetchUsersProgress->advance();
foreach ($this->userManager->getBackends() as $backend) {
@@ -128,7 +128,7 @@ class DecryptAll {
$offset += $limit;
$fetchUsersProgress->advance();
} while (count($users) >= $limit);
- $fetchUsersProgress->setMessage("Fetch list of users... finished");
+ $fetchUsersProgress->setMessage('Fetch list of users... finished');
$fetchUsersProgress->finish();
}
} else {
@@ -140,7 +140,7 @@ class DecryptAll {
$progress = new ProgressBar($this->output);
$progress->setFormat(" %message% \n [%bar%]");
$progress->start();
- $progress->setMessage("starting to decrypt files...");
+ $progress->setMessage('starting to decrypt files...');
$progress->advance();
$numberOfUsers = count($userList);
@@ -151,7 +151,7 @@ class DecryptAll {
$userNo++;
}
- $progress->setMessage("starting to decrypt files... finished");
+ $progress->setMessage('starting to decrypt files... finished');
$progress->finish();
$this->output->writeln("\n\n");
diff --git a/lib/private/Encryption/EncryptionWrapper.php b/lib/private/Encryption/EncryptionWrapper.php
index d3bf0aeb4d8..aec93a3ce4d 100644
--- a/lib/private/Encryption/EncryptionWrapper.php
+++ b/lib/private/Encryption/EncryptionWrapper.php
@@ -26,10 +26,10 @@ use Psr\Log\LoggerInterface;
* @package OC\Encryption
*/
class EncryptionWrapper {
- /** @var ArrayCache */
+ /** @var ArrayCache */
private $arrayCache;
- /** @var Manager */
+ /** @var Manager */
private $manager;
private LoggerInterface $logger;
diff --git a/lib/private/Encryption/File.php b/lib/private/Encryption/File.php
index a29d62946c4..26e643d1006 100644
--- a/lib/private/Encryption/File.php
+++ b/lib/private/Encryption/File.php
@@ -92,7 +92,7 @@ class File implements \OCP\Encryption\IFile {
}
// check if it is a group mount
- if ($this->getAppManager()->isEnabledForUser("files_external")) {
+ if ($this->getAppManager()->isEnabledForUser('files_external')) {
/** @var GlobalStoragesService $storageService */
$storageService = \OC::$server->get(GlobalStoragesService::class);
$storages = $storageService->getAllStorages();
diff --git a/lib/private/Encryption/HookManager.php b/lib/private/Encryption/HookManager.php
index 5ce51229e4e..39e7edabb95 100644
--- a/lib/private/Encryption/HookManager.php
+++ b/lib/private/Encryption/HookManager.php
@@ -42,7 +42,7 @@ class HookManager {
$user = \OC::$server->getUserManager()->get($owner);
}
if (!$user) {
- throw new \Exception("Inconsistent data, File unshared, but owner not found. Should not happen");
+ throw new \Exception('Inconsistent data, File unshared, but owner not found. Should not happen');
}
$uid = '';
diff --git a/lib/private/EventSource.php b/lib/private/EventSource.php
index dbeda25049e..18af6e35832 100644
--- a/lib/private/EventSource.php
+++ b/lib/private/EventSource.php
@@ -45,10 +45,10 @@ class EventSource implements IEventSource {
* @link https://github.com/owncloud/core/issues/14286
*/
header("Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'");
- header("Content-Type: text/html");
+ header('Content-Type: text/html');
echo str_repeat('<span></span>' . PHP_EOL, 10); //dummy data to keep IE happy
} else {
- header("Content-Type: text/event-stream");
+ header('Content-Type: text/event-stream');
}
if (!$this->request->passesStrictCookieCheck()) {
header('Location: '.\OC::$WEBROOT);
@@ -69,7 +69,7 @@ class EventSource implements IEventSource {
* @param mixed $data
*
* @throws \BadMethodCallException
- * if only one parameter is given, a typeless message will be send with that parameter as data
+ * if only one parameter is given, a typeless message will be send with that parameter as data
* @suppress PhanDeprecatedFunction
*/
public function send($type, $data = null) {
diff --git a/lib/private/Federation/CloudIdManager.php b/lib/private/Federation/CloudIdManager.php
index 3528d06a167..69d48a148b3 100644
--- a/lib/private/Federation/CloudIdManager.php
+++ b/lib/private/Federation/CloudIdManager.php
@@ -59,7 +59,7 @@ class CloudIdManager implements ICloudIdManager {
if ($event instanceof CardUpdatedEvent) {
$data = $event->getCardData()['carddata'];
foreach (explode("\r\n", $data) as $line) {
- if (str_starts_with($line, "CLOUD;")) {
+ if (str_starts_with($line, 'CLOUD;')) {
$parts = explode(':', $line, 2);
if (isset($parts[1])) {
$key = $parts[1];
diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php
index a4290549dd9..a8d9067050d 100644
--- a/lib/private/Files/Cache/Cache.php
+++ b/lib/private/Files/Cache/Cache.php
@@ -9,6 +9,7 @@ namespace OC\Files\Cache;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use OC\DB\Exceptions\DbalException;
+use OC\DB\QueryBuilder\Sharded\ShardDefinition;
use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchQuery;
use OC\Files\Storage\Wrapper\Encryption;
@@ -284,6 +285,7 @@ class Cache implements ICache {
if (count($extensionValues)) {
$query = $this->getQueryBuilder();
$query->insert('filecache_extended');
+ $query->hintShardKey('storage', $storageId);
$query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
foreach ($extensionValues as $column => $value) {
@@ -357,6 +359,7 @@ class Cache implements ICache {
try {
$query = $this->getQueryBuilder();
$query->insert('filecache_extended');
+ $query->hintShardKey('storage', $this->getNumericStorageId());
$query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
foreach ($extensionValues as $column => $value) {
@@ -368,6 +371,7 @@ class Cache implements ICache {
$query = $this->getQueryBuilder();
$query->update('filecache_extended')
->whereFileId($id)
+ ->hintShardKey('storage', $this->getNumericStorageId())
->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
return $query->expr()->orX(
$query->expr()->neq($key, $query->createNamedParameter($value)),
@@ -520,7 +524,8 @@ class Cache implements ICache {
$query = $this->getQueryBuilder();
$query->delete('filecache_extended')
- ->whereFileId($entry->getId());
+ ->whereFileId($entry->getId())
+ ->hintShardKey('storage', $this->getNumericStorageId());
$query->execute();
if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
@@ -564,7 +569,8 @@ class Cache implements ICache {
$query = $this->getQueryBuilder();
$query->delete('filecache_extended')
- ->where($query->expr()->in('fileid', $query->createParameter('childIds')));
+ ->where($query->expr()->in('fileid', $query->createParameter('childIds')))
+ ->hintShardKey('storage', $this->getNumericStorageId());
foreach (array_chunk($childIds, 1000) as $childIdChunk) {
$query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
@@ -652,6 +658,15 @@ class Cache implements ICache {
throw new \Exception('Invalid source storage path: ' . $sourcePath);
}
+ $shardDefinition = $this->connection->getShardDefinition('filecache');
+ if (
+ $shardDefinition &&
+ $shardDefinition->getShardForKey($sourceCache->getNumericStorageId()) !== $shardDefinition->getShardForKey($this->getNumericStorageId())
+ ) {
+ $this->moveFromStorageSharded($shardDefinition, $sourceCache, $sourceData, $targetPath);
+ return;
+ }
+
$sourceId = $sourceData['fileid'];
$newParentId = $this->getParentId($targetPath);
@@ -673,7 +688,7 @@ class Cache implements ICache {
$childChunks = array_chunk($childIds, 1000);
- $query = $this->connection->getQueryBuilder();
+ $query = $this->getQueryBuilder();
$fun = $query->func();
$newPathFunction = $fun->concat(
@@ -681,12 +696,15 @@ class Cache implements ICache {
$fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
);
$query->update('filecache')
- ->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
->set('path_hash', $fun->md5($newPathFunction))
->set('path', $newPathFunction)
- ->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
+ ->whereStorageId($sourceStorageId)
->andWhere($query->expr()->in('fileid', $query->createParameter('files')));
+ if ($sourceStorageId !== $targetStorageId) {
+ $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
+ }
+
// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
$query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
@@ -728,13 +746,17 @@ class Cache implements ICache {
$query = $this->getQueryBuilder();
$query->update('filecache')
- ->set('storage', $query->createNamedParameter($targetStorageId))
->set('path', $query->createNamedParameter($targetPath))
->set('path_hash', $query->createNamedParameter(md5($targetPath)))
->set('name', $query->createNamedParameter(basename($targetPath)))
->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
+ ->whereStorageId($sourceStorageId)
->whereFileId($sourceId);
+ if ($sourceStorageId !== $targetStorageId) {
+ $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
+ }
+
// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
$query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
@@ -839,7 +861,7 @@ class Cache implements ICache {
* search for files by mimetype
*
* @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
- * where it will search for all mimetypes in the group ('image/*')
+ * where it will search for all mimetypes in the group ('image/*')
* @return ICacheEntry[] an array of cache entries where the mimetype matches the search
*/
public function searchByMime($mimetype) {
@@ -891,6 +913,7 @@ class Cache implements ICache {
$query->select($query->func()->count())
->from('filecache')
->whereParent($fileId)
+ ->whereStorageId($this->getNumericStorageId())
->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$result = $query->execute();
@@ -1133,7 +1156,7 @@ class Cache implements ICache {
*/
public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
if ($sourceEntry->getId() < 0) {
- throw new \RuntimeException("Invalid source cache entry on copyFromCache");
+ throw new \RuntimeException('Invalid source cache entry on copyFromCache');
}
$data = $this->cacheEntryToArray($sourceEntry);
@@ -1144,7 +1167,7 @@ class Cache implements ICache {
$fileId = $this->put($targetPath, $data);
if ($fileId <= 0) {
- throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " ");
+ throw new \RuntimeException('Failed to copy to ' . $targetPath . ' from cache with source data ' . json_encode($data) . ' ');
}
if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
$folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
@@ -1183,4 +1206,72 @@ class Cache implements ICache {
return null;
}
}
+
+ private function moveFromStorageSharded(ShardDefinition $shardDefinition, ICache $sourceCache, ICacheEntry $sourceEntry, $targetPath) {
+ if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
+ $fileIds = $this->getChildIds($sourceCache->getNumericStorageId(), $sourceEntry->getPath());
+ } else {
+ $fileIds = [];
+ }
+ $fileIds[] = $sourceEntry->getId();
+
+ $helper = $this->connection->getCrossShardMoveHelper();
+
+ $sourceConnection = $helper->getConnection($shardDefinition, $sourceCache->getNumericStorageId());
+ $targetConnection = $helper->getConnection($shardDefinition, $this->getNumericStorageId());
+
+ $cacheItems = $helper->loadItems($sourceConnection, 'filecache', 'fileid', $fileIds);
+ $extendedItems = $helper->loadItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
+ $metadataItems = $helper->loadItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
+
+ // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
+ $removeEncryptedFlag = ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper()) && !$this->hasEncryptionWrapper();
+
+ $sourcePathLength = strlen($sourceEntry->getPath());
+ foreach ($cacheItems as &$cacheItem) {
+ if ($cacheItem['path'] === $sourceEntry->getPath()) {
+ $cacheItem['path'] = $targetPath;
+ $cacheItem['parent'] = $this->getParentId($targetPath);
+ $cacheItem['name'] = basename($cacheItem['path']);
+ } else {
+ $cacheItem['path'] = $targetPath . '/' . substr($cacheItem['path'], $sourcePathLength + 1); // +1 for the leading slash
+ }
+ $cacheItem['path_hash'] = md5($cacheItem['path']);
+ $cacheItem['storage'] = $this->getNumericStorageId();
+ if ($removeEncryptedFlag) {
+ $cacheItem['encrypted'] = 0;
+ }
+ }
+
+ $targetConnection->beginTransaction();
+
+ try {
+ $helper->saveItems($targetConnection, 'filecache', $cacheItems);
+ $helper->saveItems($targetConnection, 'filecache_extended', $extendedItems);
+ $helper->saveItems($targetConnection, 'files_metadata', $metadataItems);
+ } catch (\Exception $e) {
+ $targetConnection->rollback();
+ throw $e;
+ }
+
+ $sourceConnection->beginTransaction();
+
+ try {
+ $helper->deleteItems($sourceConnection, 'filecache', 'fileid', $fileIds);
+ $helper->deleteItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
+ $helper->deleteItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
+ } catch (\Exception $e) {
+ $targetConnection->rollback();
+ $sourceConnection->rollBack();
+ throw $e;
+ }
+
+ try {
+ $sourceConnection->commit();
+ } catch (\Exception $e) {
+ $targetConnection->rollback();
+ throw $e;
+ }
+ $targetConnection->commit();
+ }
}
diff --git a/lib/private/Files/Cache/FailedCache.php b/lib/private/Files/Cache/FailedCache.php
index 8ba2ac491bf..44c1016ca8e 100644
--- a/lib/private/Files/Cache/FailedCache.php
+++ b/lib/private/Files/Cache/FailedCache.php
@@ -125,7 +125,7 @@ class FailedCache implements ICache {
}
public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
- throw new \Exception("Invalid cache");
+ throw new \Exception('Invalid cache');
}
public function getQueryFilterForStorage(): ISearchOperator {
diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php
index 0b164912301..82f9ec3be66 100644
--- a/lib/private/Files/Cache/QuerySearchHelper.php
+++ b/lib/private/Files/Cache/QuerySearchHelper.php
@@ -194,7 +194,7 @@ class QuerySearchHelper {
protected function requireUser(ISearchQuery $searchQuery): IUser {
$user = $searchQuery->getUser();
if ($user === null) {
- throw new \InvalidArgumentException("This search operation requires the user to be set in the query");
+ throw new \InvalidArgumentException('This search operation requires the user to be set in the query');
}
return $user;
}
diff --git a/lib/private/Files/Cache/Storage.php b/lib/private/Files/Cache/Storage.php
index 0929907fcff..8d99a268dc0 100644
--- a/lib/private/Files/Cache/Storage.php
+++ b/lib/private/Files/Cache/Storage.php
@@ -80,7 +80,7 @@ class Storage {
* Adjusts the storage id to use md5 if too long
* @param string $storageId storage id
* @return string unchanged $storageId if its length is less than 64 characters,
- * else returns the md5 of $storageId
+ * else returns the md5 of $storageId
*/
public static function adjustStorageId($storageId) {
if (strlen($storageId) > 64) {
diff --git a/lib/private/Files/Cache/Updater.php b/lib/private/Files/Cache/Updater.php
index e8c6d32599e..eab68b4f545 100644
--- a/lib/private/Files/Cache/Updater.php
+++ b/lib/private/Files/Cache/Updater.php
@@ -114,7 +114,7 @@ class Updater implements IUpdater {
}
// encryption is a pita and touches the cache itself
- if (isset($data['encrypted']) && !!$data['encrypted']) {
+ if (isset($data['encrypted']) && (bool)$data['encrypted']) {
$sizeDifference = null;
}
@@ -246,7 +246,7 @@ class Updater implements IUpdater {
// ignore the failure.
// with failures concurrent updates, someone else would have already done it.
// in the worst case the `storage_mtime` isn't updated, which should at most only trigger an extra rescan
- $this->logger->warning("Error while updating parent storage_mtime, should be safe to ignore", ['exception' => $e]);
+ $this->logger->warning('Error while updating parent storage_mtime, should be safe to ignore', ['exception' => $e]);
}
}
}
diff --git a/lib/private/Files/Config/MountProviderCollection.php b/lib/private/Files/Config/MountProviderCollection.php
index 0e103690b6b..1dbc469c8c3 100644
--- a/lib/private/Files/Config/MountProviderCollection.php
+++ b/lib/private/Files/Config/MountProviderCollection.php
@@ -131,9 +131,9 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
}
$lateMounts = $this->filterMounts($user, $lateMounts);
- $this->eventLogger->start("fs:setup:add-mounts", "Add mounts to the filesystem");
+ $this->eventLogger->start('fs:setup:add-mounts', 'Add mounts to the filesystem');
array_walk($lateMounts, [$mountManager, 'addMount']);
- $this->eventLogger->end("fs:setup:add-mounts");
+ $this->eventLogger->end('fs:setup:add-mounts');
return array_merge($lateMounts, $firstMounts);
}
@@ -223,7 +223,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
}, []);
if (count($mounts) === 0) {
- throw new \Exception("No root mounts provided by any provider");
+ throw new \Exception('No root mounts provided by any provider');
}
return $mounts;
diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php
index 67b2cad7ea2..94da770b63f 100644
--- a/lib/private/Files/Config/UserMountCache.php
+++ b/lib/private/Files/Config/UserMountCache.php
@@ -477,7 +477,7 @@ class UserMountCache implements IUserMountCache {
}
}
- throw new NotFoundException("No cached mount for path " . $path);
+ throw new NotFoundException('No cached mount for path ' . $path);
}
public function getMountsInPath(IUser $user, string $path): array {
diff --git a/lib/private/Files/FileInfo.php b/lib/private/Files/FileInfo.php
index c3dcb531342..c47b8b1d1a7 100644
--- a/lib/private/Files/FileInfo.php
+++ b/lib/private/Files/FileInfo.php
@@ -134,7 +134,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess {
* @return int|null
*/
public function getId() {
- return isset($this->data['fileid']) ? (int) $this->data['fileid'] : null;
+ return isset($this->data['fileid']) ? (int)$this->data['fileid'] : null;
}
/**
@@ -196,7 +196,7 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess {
*/
public function getMTime() {
$this->updateEntryfromSubMounts();
- return (int) $this->data['mtime'];
+ return (int)$this->data['mtime'];
}
/**
@@ -210,14 +210,14 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess {
* Return the current version used for the HMAC in the encryption app
*/
public function getEncryptedVersion(): int {
- return isset($this->data['encryptedVersion']) ? (int) $this->data['encryptedVersion'] : 1;
+ return isset($this->data['encryptedVersion']) ? (int)$this->data['encryptedVersion'] : 1;
}
/**
* @return int
*/
public function getPermissions() {
- return (int) $this->data['permissions'];
+ return (int)$this->data['permissions'];
}
/**
@@ -379,11 +379,11 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess {
}
public function getCreationTime(): int {
- return (int) $this->data['creation_time'];
+ return (int)$this->data['creation_time'];
}
public function getUploadTime(): int {
- return (int) $this->data['upload_time'];
+ return (int)$this->data['upload_time'];
}
public function getParentId(): int {
diff --git a/lib/private/Files/FilenameValidator.php b/lib/private/Files/FilenameValidator.php
index b1ce8e02b13..fde45068df7 100644
--- a/lib/private/Files/FilenameValidator.php
+++ b/lib/private/Files/FilenameValidator.php
@@ -25,6 +25,8 @@ use Psr\Log\LoggerInterface;
*/
class FilenameValidator implements IFilenameValidator {
+ public const INVALID_FILE_TYPE = 100;
+
private IL10N $l10n;
/**
@@ -198,9 +200,7 @@ class FilenameValidator implements IFilenameValidator {
}
}
- if ($this->isForbidden($filename)) {
- throw new ReservedWordException();
- }
+ $this->checkForbiddenName($filename);
$this->checkForbiddenExtension($filename);
@@ -227,18 +227,25 @@ class FilenameValidator implements IFilenameValidator {
return true;
}
+ // Filename is not forbidden
+ return false;
+ }
+
+ protected function checkForbiddenName($filename): void {
+ if ($this->isForbidden($filename)) {
+ throw new ReservedWordException($this->l10n->t('"%1$s" is a forbidden file or folder name.', [$filename]));
+ }
+
// Check for forbidden basenames - basenames are the part of the file until the first dot
// (except if the dot is the first character as this is then part of the basename "hidden files")
$basename = substr($filename, 0, strpos($filename, '.', 1) ?: null);
$forbiddenNames = $this->getForbiddenBasenames();
if (in_array($basename, $forbiddenNames)) {
- return true;
+ throw new ReservedWordException($this->l10n->t('"%1$s" is a forbidden prefix for file or folder names.', [$filename]));
}
-
- // Filename is not forbidden
- return false;
}
+
/**
* Check if a filename contains any of the forbidden characters
* @param string $filename
@@ -252,7 +259,7 @@ class FilenameValidator implements IFilenameValidator {
foreach ($this->getForbiddenCharacters() as $char) {
if (str_contains($filename, $char)) {
- throw new InvalidCharacterInPathException($this->l10n->t('Invalid character "%1$s" in filename', [$char]));
+ throw new InvalidCharacterInPathException($this->l10n->t('"%1$s" is not allowed inside a file or folder name.', [$char]));
}
}
}
@@ -264,11 +271,15 @@ class FilenameValidator implements IFilenameValidator {
*/
protected function checkForbiddenExtension(string $filename): void {
$filename = mb_strtolower($filename);
- // Check for forbidden filename exten<sions
+ // Check for forbidden filename extensions
$forbiddenExtensions = $this->getForbiddenExtensions();
foreach ($forbiddenExtensions as $extension) {
if (str_ends_with($filename, $extension)) {
- throw new InvalidPathException($this->l10n->t('Invalid filename extension "%1$s"', [$extension]));
+ if (str_starts_with($extension, '.')) {
+ throw new InvalidPathException($this->l10n->t('"%1$s" is a forbidden file type.', [$extension]), self::INVALID_FILE_TYPE);
+ } else {
+ throw new InvalidPathException($this->l10n->t('Filenames must not end with "%1$s".', [$extension]));
+ }
}
}
}
diff --git a/lib/private/Files/Filesystem.php b/lib/private/Files/Filesystem.php
index db7420c3c4c..48c069de0b9 100644
--- a/lib/private/Files/Filesystem.php
+++ b/lib/private/Files/Filesystem.php
@@ -674,7 +674,7 @@ class Filesystem {
*
* @param string $path
* @param bool|string $includeMountPoints whether to add mountpoint sizes,
- * defaults to true
+ * defaults to true
* @return \OC\Files\FileInfo|false False if file does not exist
*/
public static function getFileInfo($path, $includeMountPoints = true) {
diff --git a/lib/private/Files/Mount/Manager.php b/lib/private/Files/Mount/Manager.php
index c2267af3c96..d118021afa2 100644
--- a/lib/private/Files/Mount/Manager.php
+++ b/lib/private/Files/Mount/Manager.php
@@ -84,7 +84,7 @@ class Manager implements IMountManager {
if (count($this->mounts) === 0) {
$this->setupManager->setupRoot();
if (count($this->mounts) === 0) {
- throw new \Exception("No mounts even after explicitly setting up the root mounts");
+ throw new \Exception('No mounts even after explicitly setting up the root mounts');
}
}
@@ -104,7 +104,7 @@ class Manager implements IMountManager {
}
}
- throw new NotFoundException("No mount for path " . $path . " existing mounts (" . count($this->mounts) ."): " . implode(",", array_keys($this->mounts)));
+ throw new NotFoundException('No mount for path ' . $path . ' existing mounts (' . count($this->mounts) .'): ' . implode(',', array_keys($this->mounts)));
}
/**
diff --git a/lib/private/Files/Mount/RootMountProvider.php b/lib/private/Files/Mount/RootMountProvider.php
index 7ad7a7f059c..86f8188978f 100644
--- a/lib/private/Files/Mount/RootMountProvider.php
+++ b/lib/private/Files/Mount/RootMountProvider.php
@@ -56,7 +56,7 @@ class RootMountProvider implements IRootMountProvider {
}
private function getLocalRootMount(IStorageFactory $loader): MountPoint {
- $configDataDirectory = $this->config->getSystemValue("datadirectory", OC::$SERVERROOT . "/data");
+ $configDataDirectory = $this->config->getSystemValue('datadirectory', OC::$SERVERROOT . '/data');
return new MountPoint(LocalRootStorage::class, '/', ['datadir' => $configDataDirectory], $loader, null, null, self::class);
}
diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php
index 9936487abb3..ca256b09d33 100644
--- a/lib/private/Files/Node/Folder.php
+++ b/lib/private/Files/Node/Folder.php
@@ -422,7 +422,7 @@ class Folder extends Node implements \OCP\Files\Folder {
$filterNonRecentFiles = new SearchComparison(
ISearchComparison::COMPARE_GREATER_THAN,
'mtime',
- strtotime("-2 week")
+ strtotime('-2 week')
);
if ($offset === 0 && $limit <= 100) {
$query = new SearchQuery(
diff --git a/lib/private/Files/Node/LazyUserFolder.php b/lib/private/Files/Node/LazyUserFolder.php
index f9908b9b7b6..77479c2fa5e 100644
--- a/lib/private/Files/Node/LazyUserFolder.php
+++ b/lib/private/Files/Node/LazyUserFolder.php
@@ -59,7 +59,7 @@ class LazyUserFolder extends LazyFolder {
}
$mountPoint = $this->mountManager->find('/' . $this->user->getUID());
if (is_null($mountPoint)) {
- throw new \Exception("No mountpoint for user folder");
+ throw new \Exception('No mountpoint for user folder');
}
return $mountPoint;
}
diff --git a/lib/private/Files/Node/Node.php b/lib/private/Files/Node/Node.php
index e7f533e73a9..5dbdc4054bf 100644
--- a/lib/private/Files/Node/Node.php
+++ b/lib/private/Files/Node/Node.php
@@ -158,7 +158,7 @@ class Node implements INode {
public function getStorage() {
$storage = $this->getMountPoint()->getStorage();
if (!$storage) {
- throw new \Exception("No storage for node");
+ throw new \Exception('No storage for node');
}
return $storage;
}
@@ -432,11 +432,14 @@ class Node implements INode {
$targetPath = $this->normalizePath($targetPath);
$parent = $this->root->get(dirname($targetPath));
if (
- $parent instanceof Folder and
- $this->isValidPath($targetPath) and
- (
- $parent->isCreatable() ||
- ($parent->getInternalPath() === '' && $parent->getMountPoint() instanceof MoveableMount)
+ ($parent instanceof Folder)
+ && $this->isValidPath($targetPath)
+ && (
+ $parent->isCreatable()
+ || (
+ $parent->getInternalPath() === ''
+ && ($parent->getMountPoint() instanceof MoveableMount)
+ )
)
) {
$nonExisting = $this->createNonExistingNode($targetPath);
diff --git a/lib/private/Files/Node/NonExistingFile.php b/lib/private/Files/Node/NonExistingFile.php
index d154876432e..66ec2e6c040 100644
--- a/lib/private/Files/Node/NonExistingFile.php
+++ b/lib/private/Files/Node/NonExistingFile.php
@@ -38,6 +38,14 @@ class NonExistingFile extends File {
}
}
+ public function getInternalPath() {
+ if ($this->fileInfo) {
+ return parent::getInternalPath();
+ } else {
+ return $this->getParent()->getMountPoint()->getInternalPath($this->getPath());
+ }
+ }
+
public function stat() {
throw new NotFoundException();
}
diff --git a/lib/private/Files/Node/NonExistingFolder.php b/lib/private/Files/Node/NonExistingFolder.php
index 5650c99fe73..4489fdaf010 100644
--- a/lib/private/Files/Node/NonExistingFolder.php
+++ b/lib/private/Files/Node/NonExistingFolder.php
@@ -38,6 +38,14 @@ class NonExistingFolder extends Folder {
}
}
+ public function getInternalPath() {
+ if ($this->fileInfo) {
+ return parent::getInternalPath();
+ } else {
+ return $this->getParent()->getMountPoint()->getInternalPath($this->getPath());
+ }
+ }
+
public function stat() {
throw new NotFoundException();
}
diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php
index 5fb4013cfc9..416adc7f374 100644
--- a/lib/private/Files/Node/Root.php
+++ b/lib/private/Files/Node/Root.php
@@ -459,7 +459,7 @@ class Root extends Folder implements IRootFolder {
if ($folder instanceof Folder) {
return $folder->getByIdInRootMount($id);
} else {
- throw new \Exception("getByIdInPath with non folder");
+ throw new \Exception('getByIdInPath with non folder');
}
}
return [];
diff --git a/lib/private/Files/ObjectStore/Azure.php b/lib/private/Files/ObjectStore/Azure.php
index 55400d4131c..2dacdac1f8d 100644
--- a/lib/private/Files/ObjectStore/Azure.php
+++ b/lib/private/Files/ObjectStore/Azure.php
@@ -21,7 +21,7 @@ class Azure implements IObjectStore {
private $blobClient = null;
/** @var string|null */
private $endpoint = null;
- /** @var bool */
+ /** @var bool */
private $autoCreate = false;
/**
@@ -45,7 +45,7 @@ class Azure implements IObjectStore {
private function getBlobClient() {
if (!$this->blobClient) {
$protocol = $this->endpoint ? substr($this->endpoint, 0, strpos($this->endpoint, ':')) : 'https';
- $connectionString = "DefaultEndpointsProtocol=" . $protocol . ";AccountName=" . $this->accountName . ";AccountKey=" . $this->accountKey;
+ $connectionString = 'DefaultEndpointsProtocol=' . $protocol . ';AccountName=' . $this->accountName . ';AccountKey=' . $this->accountKey;
if ($this->endpoint) {
$connectionString .= ';BlobEndpoint=' . $this->endpoint;
}
diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php
index 389f744eab4..228fc516677 100644
--- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php
+++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php
@@ -594,6 +594,31 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil
return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
}
+ public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, ?ICacheEntry $sourceCacheEntry = null): bool {
+ $sourceCache = $sourceStorage->getCache();
+ if (!$sourceCacheEntry) {
+ $sourceCacheEntry = $sourceCache->get($sourceInternalPath);
+ }
+ if ($sourceCacheEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
+ foreach ($sourceCache->getFolderContents($sourceInternalPath) as $child) {
+ $this->moveFromStorage($sourceStorage, $child->getPath(), $targetInternalPath . '/' . $child->getName());
+ }
+ $sourceStorage->rmdir($sourceInternalPath);
+ } else {
+ // move the cache entry before the contents so that we have the correct fileid/urn for the target
+ $this->getCache()->moveFromCache($sourceCache, $sourceInternalPath, $targetInternalPath);
+ try {
+ $this->writeStream($targetInternalPath, $sourceStorage->fopen($sourceInternalPath, 'r'), $sourceCacheEntry->getSize());
+ } catch (\Exception $e) {
+ // restore the cache entry
+ $sourceCache->moveFromCache($this->getCache(), $targetInternalPath, $sourceInternalPath);
+ throw $e;
+ }
+ $sourceStorage->unlink($sourceInternalPath);
+ }
+ return true;
+ }
+
public function copy($source, $target) {
$source = $this->normalizePath($source);
$target = $this->normalizePath($target);
@@ -632,7 +657,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFil
$sourceUrn = $this->getURN($sourceEntry->getId());
if (!$cache instanceof Cache) {
- throw new \Exception("Invalid source cache for object store copy");
+ throw new \Exception('Invalid source cache for object store copy');
}
$targetId = $cache->copyFromCache($cache, $sourceEntry, $to);
diff --git a/lib/private/Files/ObjectStore/S3ConnectionTrait.php b/lib/private/Files/ObjectStore/S3ConnectionTrait.php
index 0506eb35353..9de85f00620 100644
--- a/lib/private/Files/ObjectStore/S3ConnectionTrait.php
+++ b/lib/private/Files/ObjectStore/S3ConnectionTrait.php
@@ -28,7 +28,7 @@ trait S3ConnectionTrait {
protected function parseParams($params) {
if (empty($params['bucket'])) {
- throw new \Exception("Bucket has to be configured.");
+ throw new \Exception('Bucket has to be configured.');
}
$this->id = 'amazon::' . $params['bucket'];
@@ -132,7 +132,7 @@ trait S3ConnectionTrait {
try {
$logger->info('Bucket "' . $this->bucket . '" does not exist - creating it.', ['app' => 'objectstore']);
if (!$this->connection::isBucketDnsCompatible($this->bucket)) {
- throw new \Exception("The bucket will not be created because the name is not dns compatible, please correct it: " . $this->bucket);
+ throw new \Exception('The bucket will not be created because the name is not dns compatible, please correct it: ' . $this->bucket);
}
$this->connection->createBucket(['Bucket' => $this->bucket]);
$this->testTimeout();
@@ -197,7 +197,7 @@ trait S3ConnectionTrait {
}
protected function getCertificateBundlePath(): ?string {
- if ((int)($this->params['use_nextcloud_bundle'] ?? "0")) {
+ if ((int)($this->params['use_nextcloud_bundle'] ?? '0')) {
// since we store the certificate bundles on the primary storage, we can't get the bundle while setting up the primary storage
if (!isset($this->params['primary_storage'])) {
/** @var ICertificateManager $certManager */
diff --git a/lib/private/Files/ObjectStore/S3ObjectTrait.php b/lib/private/Files/ObjectStore/S3ObjectTrait.php
index 5d00c184ca7..2e625033751 100644
--- a/lib/private/Files/ObjectStore/S3ObjectTrait.php
+++ b/lib/private/Files/ObjectStore/S3ObjectTrait.php
@@ -127,7 +127,7 @@ trait S3ObjectTrait {
if ($e->getState()->isInitiated() && (array_key_exists('UploadId', $uploadInfo))) {
$this->getConnection()->abortMultipartUpload($uploadInfo);
}
- throw new \OCA\DAV\Connector\Sabre\Exception\BadGateway("Error while uploading to S3 bucket", 0, $e);
+ throw new \OCA\DAV\Connector\Sabre\Exception\BadGateway('Error while uploading to S3 bucket', 0, $e);
}
}
@@ -144,7 +144,7 @@ trait S3ObjectTrait {
// ($psrStream->isSeekable() && $psrStream->getSize() !== null) evaluates to true for a On-Seekable stream
// so the optimisation does not apply
- $buffer = new Psr7\Stream(fopen("php://memory", 'rwb+'));
+ $buffer = new Psr7\Stream(fopen('php://memory', 'rwb+'));
Utils::copyToStream($psrStream, $buffer, $this->putSizeLimit);
$buffer->seek(0);
if ($buffer->getSize() < $this->putSizeLimit) {
@@ -183,14 +183,14 @@ trait S3ObjectTrait {
if ($this->useMultipartCopy && $size > $this->copySizeLimit) {
$copy = new MultipartCopy($this->getConnection(), [
- "source_bucket" => $this->getBucket(),
- "source_key" => $from
+ 'source_bucket' => $this->getBucket(),
+ 'source_key' => $from
], array_merge([
- "bucket" => $this->getBucket(),
- "key" => $to,
- "acl" => "private",
- "params" => $this->getSSECParameters() + $this->getSSECParameters(true),
- "source_metadata" => $sourceMetadata
+ 'bucket' => $this->getBucket(),
+ 'key' => $to,
+ 'acl' => 'private',
+ 'params' => $this->getSSECParameters() + $this->getSSECParameters(true),
+ 'source_metadata' => $sourceMetadata
], $options));
$copy->copy();
} else {
diff --git a/lib/private/Files/ObjectStore/S3Signature.php b/lib/private/Files/ObjectStore/S3Signature.php
index e3a522b6581..4e9784ee81a 100644
--- a/lib/private/Files/ObjectStore/S3Signature.php
+++ b/lib/private/Files/ObjectStore/S3Signature.php
@@ -99,7 +99,7 @@ class S3Signature implements SignatureInterface {
}
/**
- * @param RequestInterface $request
+ * @param RequestInterface $request
* @param CredentialsInterface $creds
*
* @return RequestInterface
diff --git a/lib/private/Files/ObjectStore/SwiftFactory.php b/lib/private/Files/ObjectStore/SwiftFactory.php
index 0db5c9762d2..a2e22e45de2 100644
--- a/lib/private/Files/ObjectStore/SwiftFactory.php
+++ b/lib/private/Files/ObjectStore/SwiftFactory.php
@@ -170,7 +170,7 @@ class SwiftFactory {
try {
/** @var \OpenStack\Identity\v2\Models\Token $token */
$token = $authService->model(\OpenStack\Identity\v2\Models\Token::class, $cachedToken['token']);
- $now = new \DateTimeImmutable("now");
+ $now = new \DateTimeImmutable('now');
if ($token->expires > $now) {
$hasValidCachedToken = true;
$this->params['v2cachedToken'] = $token;
diff --git a/lib/private/Files/Search/SearchQuery.php b/lib/private/Files/Search/SearchQuery.php
index e7cb031da3e..3c8711facd8 100644
--- a/lib/private/Files/Search/SearchQuery.php
+++ b/lib/private/Files/Search/SearchQuery.php
@@ -11,13 +11,13 @@ use OCP\Files\Search\ISearchQuery;
use OCP\IUser;
class SearchQuery implements ISearchQuery {
- /** @var ISearchOperator */
+ /** @var ISearchOperator */
private $searchOperation;
- /** @var integer */
+ /** @var integer */
private $limit;
- /** @var integer */
+ /** @var integer */
private $offset;
- /** @var ISearchOrder[] */
+ /** @var ISearchOrder[] */
private $order;
/** @var ?IUser */
private $user;
diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php
index 53befe57e36..e74dd1042d6 100644
--- a/lib/private/Files/SetupManager.php
+++ b/lib/private/Files/SetupManager.php
@@ -107,8 +107,9 @@ class SetupManager {
$prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
Filesystem::addStorageWrapper('mount_options', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
- if ($mount->getOptions() && $storage->instanceOfStorage(Common::class)) {
- $storage->setMountOptions($mount->getOptions());
+ if ($storage->instanceOfStorage(Common::class)) {
+ $options = array_merge($mount->getOptions(), ['mount_point' => $mountPoint]);
+ $storage->setMountOptions($options);
}
return $storage;
});
@@ -381,7 +382,7 @@ class SetupManager {
}
// for the user's home folder, and includes children we need everything always
- if (rtrim($path) === "/" . $user->getUID() . "/files" && $includeChildren) {
+ if (rtrim($path) === '/' . $user->getUID() . '/files' && $includeChildren) {
$this->setupForUser($user);
return;
}
@@ -411,7 +412,7 @@ class SetupManager {
$setupProviders[] = $cachedMount->getMountProvider();
$mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]);
} else {
- $this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup");
+ $this->logger->debug('mount at ' . $cachedMount->getMountPoint() . ' has no provider set, performing full setup');
$this->eventLogger->end('fs:setup:user:path:find');
$this->setupForUser($user);
$this->eventLogger->end('fs:setup:user:path');
@@ -428,7 +429,7 @@ class SetupManager {
}, false);
if ($needsFullSetup) {
- $this->logger->debug("mount has no provider set, performing full setup");
+ $this->logger->debug('mount has no provider set, performing full setup');
$this->setupForUser($user);
$this->eventLogger->end('fs:setup:user:path');
return;
@@ -490,7 +491,7 @@ class SetupManager {
return;
}
- $this->eventLogger->start('fs:setup:user:providers', "Setup filesystem for " . implode(', ', $providers));
+ $this->eventLogger->start('fs:setup:user:providers', 'Setup filesystem for ' . implode(', ', $providers));
$this->oneTimeUserSetup($user);
diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php
index a8f8c05ab54..9541ba7c746 100644
--- a/lib/private/Files/Storage/Common.php
+++ b/lib/private/Files/Storage/Common.php
@@ -13,6 +13,7 @@ use OC\Files\Cache\Propagator;
use OC\Files\Cache\Scanner;
use OC\Files\Cache\Updater;
use OC\Files\Cache\Watcher;
+use OC\Files\FilenameValidator;
use OC\Files\Filesystem;
use OC\Files\Storage\Wrapper\Jail;
use OC\Files\Storage\Wrapper\Wrapper;
@@ -159,7 +160,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage {
}
public function file_get_contents($path) {
- $handle = $this->fopen($path, "r");
+ $handle = $this->fopen($path, 'r');
if (!$handle) {
return false;
}
@@ -169,7 +170,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage {
}
public function file_put_contents($path, $data) {
- $handle = $this->fopen($path, "w");
+ $handle = $this->fopen($path, 'w');
if (!$handle) {
return false;
}
@@ -191,7 +192,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage {
$this->remove($target);
$dir = $this->opendir($source);
$this->mkdir($target);
- while ($file = readdir($dir)) {
+ while (($file = readdir($dir)) !== false) {
if (!Filesystem::isIgnoredDir($file)) {
if (!$this->copy($source . '/' . $file, $target . '/' . $file)) {
closedir($dir);
@@ -430,11 +431,11 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage {
if ($this->stat('')) {
return true;
}
- \OC::$server->get(LoggerInterface::class)->info("External storage not available: stat() failed");
+ \OC::$server->get(LoggerInterface::class)->info('External storage not available: stat() failed');
return false;
} catch (\Exception $e) {
\OC::$server->get(LoggerInterface::class)->warning(
- "External storage not available: " . $e->getMessage(),
+ 'External storage not available: ' . $e->getMessage(),
['exception' => $e]
);
return false;
@@ -494,7 +495,18 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage {
$this->getFilenameValidator()
->validateFilename($fileName);
- // NOTE: $path will remain unverified for now
+ // verify also the path is valid
+ if ($path && $path !== '/' && $path !== '.') {
+ try {
+ $this->verifyPath(dirname($path), basename($path));
+ } catch (InvalidPathException $e) {
+ // Ignore invalid file type exceptions on directories
+ if ($e->getCode() !== FilenameValidator::INVALID_FILE_TYPE) {
+ $l = \OCP\Util::getL10N('lib');
+ throw new InvalidPathException($l->t('Invalid parent path'), previous: $e);
+ }
+ }
+ }
}
/**
@@ -816,7 +828,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage {
try {
[$count, $result] = \OC_Helper::streamCopy($stream, $target);
if (!$result) {
- throw new GenericFileException("Failed to copy stream");
+ throw new GenericFileException('Failed to copy stream');
}
} finally {
fclose($target);
diff --git a/lib/private/Files/Storage/DAV.php b/lib/private/Files/Storage/DAV.php
index 63ef1399a69..7abc0ccc182 100644
--- a/lib/private/Files/Storage/DAV.php
+++ b/lib/private/Files/Storage/DAV.php
@@ -90,9 +90,9 @@ class DAV extends Common {
if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
$host = $params['host'];
//remove leading http[s], will be generated in createBaseUri()
- if (str_starts_with($host, "https://")) {
+ if (str_starts_with($host, 'https://')) {
$host = substr($host, 8);
- } elseif (str_starts_with($host, "http://")) {
+ } elseif (str_starts_with($host, 'http://')) {
$host = substr($host, 7);
}
$this->host = $host;
@@ -162,13 +162,13 @@ class DAV extends Common {
$lastRequestStart = 0;
$this->client->on('beforeRequest', function (RequestInterface $request) use (&$lastRequestStart) {
- $this->logger->debug("sending dav " . $request->getMethod() . " request to external storage: " . $request->getAbsoluteUrl(), ['app' => 'dav']);
+ $this->logger->debug('sending dav ' . $request->getMethod() . ' request to external storage: ' . $request->getAbsoluteUrl(), ['app' => 'dav']);
$lastRequestStart = microtime(true);
- $this->eventLogger->start('fs:storage:dav:request', "Sending dav request to external storage");
+ $this->eventLogger->start('fs:storage:dav:request', 'Sending dav request to external storage');
});
$this->client->on('afterRequest', function (RequestInterface $request) use (&$lastRequestStart) {
$elapsed = microtime(true) - $lastRequestStart;
- $this->logger->debug("dav " . $request->getMethod() . " request to external storage: " . $request->getAbsoluteUrl() . " took " . round($elapsed * 1000, 1) . "ms", ['app' => 'dav']);
+ $this->logger->debug('dav ' . $request->getMethod() . ' request to external storage: ' . $request->getAbsoluteUrl() . ' took ' . round($elapsed * 1000, 1) . 'ms', ['app' => 'dav']);
$this->eventLogger->end('fs:storage:dav:request');
});
}
@@ -283,11 +283,11 @@ class DAV extends Common {
return false;
}
$responseType = [];
- if (isset($response["{DAV:}resourcetype"])) {
+ if (isset($response['{DAV:}resourcetype'])) {
/** @var ResourceType[] $response */
- $responseType = $response["{DAV:}resourcetype"]->getValue();
+ $responseType = $response['{DAV:}resourcetype']->getValue();
}
- return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
+ return (count($responseType) > 0 and $responseType[0] == '{DAV:}collection') ? 'dir' : 'file';
} catch (\Exception $e) {
$this->convertException($e, $path);
}
@@ -582,11 +582,11 @@ class DAV extends Common {
}
$responseType = [];
- if (isset($response["{DAV:}resourcetype"])) {
+ if (isset($response['{DAV:}resourcetype'])) {
/** @var ResourceType[] $response */
- $responseType = $response["{DAV:}resourcetype"]->getValue();
+ $responseType = $response['{DAV:}resourcetype']->getValue();
}
- $type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
+ $type = (count($responseType) > 0 and $responseType[0] == '{DAV:}collection') ? 'dir' : 'file';
if ($type === 'dir') {
$mimeType = 'httpd/unix-directory';
} elseif (isset($response['{DAV:}getcontenttype'])) {
@@ -832,9 +832,9 @@ class DAV extends Common {
* @param string $path optional path from the operation
*
* @throws StorageInvalidException if the storage is invalid, for example
- * when the authentication expired or is invalid
+ * when the authentication expired or is invalid
* @throws StorageNotAvailableException if the storage is not available,
- * which might be temporary
+ * which might be temporary
* @throws ForbiddenException if the action is not allowed
*/
protected function convertException(Exception $e, $path = '') {
diff --git a/lib/private/Files/Storage/Home.php b/lib/private/Files/Storage/Home.php
index 9a336d2efcc..a8d1f82b987 100644
--- a/lib/private/Files/Storage/Home.php
+++ b/lib/private/Files/Storage/Home.php
@@ -28,7 +28,7 @@ class Home extends Local implements \OCP\Files\IHomeStorage {
* Construct a Home storage instance
*
* @param array $arguments array with "user" containing the
- * storage owner
+ * storage owner
*/
public function __construct($arguments) {
$this->user = $arguments['user'];
diff --git a/lib/private/Files/Storage/PolyFill/CopyDirectory.php b/lib/private/Files/Storage/PolyFill/CopyDirectory.php
index 4b3e367da78..5fe396d97e1 100644
--- a/lib/private/Files/Storage/PolyFill/CopyDirectory.php
+++ b/lib/private/Files/Storage/PolyFill/CopyDirectory.php
@@ -70,7 +70,7 @@ trait CopyDirectory {
protected function copyRecursive($source, $target) {
$dh = $this->opendir($source);
$result = true;
- while ($file = readdir($dh)) {
+ while (($file = readdir($dh)) !== false) {
if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
if ($this->is_dir($source . '/' . $file)) {
$this->mkdir($target . '/' . $file);
diff --git a/lib/private/Files/Storage/StorageFactory.php b/lib/private/Files/Storage/StorageFactory.php
index 18fca1d28e3..612592e2d3a 100644
--- a/lib/private/Files/Storage/StorageFactory.php
+++ b/lib/private/Files/Storage/StorageFactory.php
@@ -26,7 +26,7 @@ class StorageFactory implements IStorageFactory {
* @param int $priority wrappers with the lower priority are applied last (meaning they get called first)
* @param \OCP\Files\Mount\IMountPoint[] $existingMounts existing mount points to apply the wrapper to
* @return bool true if the wrapper was added, false if there was already a wrapper with this
- * name registered
+ * name registered
*/
public function addStorageWrapper($wrapperName, $callback, $priority = 50, $existingMounts = []) {
if (isset($this->storageWrappers[$wrapperName])) {
diff --git a/lib/private/Files/Storage/Wrapper/Encryption.php b/lib/private/Files/Storage/Wrapper/Encryption.php
index 7d45c356bfe..1ead1c342b0 100644
--- a/lib/private/Files/Storage/Wrapper/Encryption.php
+++ b/lib/private/Files/Storage/Wrapper/Encryption.php
@@ -14,6 +14,7 @@ use OC\Files\Cache\CacheEntry;
use OC\Files\Filesystem;
use OC\Files\Mount\Manager;
use OC\Files\ObjectStore\ObjectStoreStorage;
+use OC\Files\Storage\Common;
use OC\Files\Storage\LocalTempFileTrait;
use OC\Memcache\ArrayCache;
use OCP\Cache\CappedMemoryCache;
@@ -64,7 +65,7 @@ class Encryption extends Wrapper {
/** @var array remember for which path we execute the repair step to avoid recursions */
private $fixUnencryptedSizeOf = [];
- /** @var ArrayCache */
+ /** @var ArrayCache */
private $arrayCache;
/** @var CappedMemoryCache<bool> */
@@ -203,7 +204,7 @@ class Encryption extends Wrapper {
$encryptionModule = $this->getEncryptionModule($path);
if ($encryptionModule) {
- $handle = $this->fopen($path, "r");
+ $handle = $this->fopen($path, 'r');
if (!$handle) {
return false;
}
@@ -776,9 +777,8 @@ class Encryption extends Wrapper {
// first copy the keys that we reuse the existing file key on the target location
// and don't create a new one which would break versions for example.
- $mount = $this->mountManager->findByStorageId($sourceStorage->getId());
- if (count($mount) >= 1) {
- $mountPoint = $mount[0]->getMountPoint();
+ if ($sourceStorage->instanceOfStorage(Common::class) && $sourceStorage->getMountOption('mount_point')) {
+ $mountPoint = $sourceStorage->getMountOption('mount_point');
$source = $mountPoint . '/' . $sourceInternalPath;
$target = $this->getFullPath($targetInternalPath);
$this->copyKeys($source, $target);
diff --git a/lib/private/Files/Storage/Wrapper/Quota.php b/lib/private/Files/Storage/Wrapper/Quota.php
index 8c6799fdd2d..b642c438266 100644
--- a/lib/private/Files/Storage/Wrapper/Quota.php
+++ b/lib/private/Files/Storage/Wrapper/Quota.php
@@ -40,7 +40,7 @@ class Quota extends Wrapper {
if ($this->quota === null) {
$quotaCallback = $this->quotaCallback;
if ($quotaCallback === null) {
- throw new \Exception("No quota or quota callback provider");
+ throw new \Exception('No quota or quota callback provider');
}
$this->quota = $quotaCallback();
}
diff --git a/lib/private/Files/Storage/Wrapper/Wrapper.php b/lib/private/Files/Storage/Wrapper/Wrapper.php
index 29acc9ad1c2..f8aa9d963dc 100644
--- a/lib/private/Files/Storage/Wrapper/Wrapper.php
+++ b/lib/private/Files/Storage/Wrapper/Wrapper.php
@@ -40,7 +40,7 @@ class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage, IWriteStrea
*/
public function getWrapperStorage() {
if (!$this->storage) {
- $message = "storage wrapper " . get_class($this) . " doesn't have a wrapped storage set";
+ $message = 'storage wrapper ' . get_class($this) . " doesn't have a wrapped storage set";
$logger = Server::get(LoggerInterface::class);
$logger->error($message);
$this->storage = new FailedStorage(['exception' => new \Exception($message)]);
diff --git a/lib/private/Files/Stream/Encryption.php b/lib/private/Files/Stream/Encryption.php
index 32c0021cd23..8f08f925da0 100644
--- a/lib/private/Files/Stream/Encryption.php
+++ b/lib/private/Files/Stream/Encryption.php
@@ -55,7 +55,7 @@ class Encryption extends Wrapper {
/** @var string */
protected $fullPath;
- /** @var bool */
+ /** @var bool */
protected $signed;
/**
diff --git a/lib/private/Files/Stream/SeekableHttpStream.php b/lib/private/Files/Stream/SeekableHttpStream.php
index 02ed1470fbd..5ed04ed9066 100644
--- a/lib/private/Files/Stream/SeekableHttpStream.php
+++ b/lib/private/Files/Stream/SeekableHttpStream.php
@@ -90,7 +90,7 @@ class SeekableHttpStream implements File {
continue 2;
}
}
- throw new \Exception("Failed to get source stream from stream wrapper of " . get_class($responseHead));
+ throw new \Exception('Failed to get source stream from stream wrapper of ' . get_class($responseHead));
}
$rangeHeaders = array_values(array_filter($responseHead, function ($v) {
diff --git a/lib/private/Files/Type/Loader.php b/lib/private/Files/Type/Loader.php
index 247acf0141a..407df59b2e2 100644
--- a/lib/private/Files/Type/Loader.php
+++ b/lib/private/Files/Type/Loader.php
@@ -115,7 +115,7 @@ class Loader implements IMimeTypeLoader {
throw new \Exception("Database threw an unique constraint on inserting a new mimetype, but couldn't return the ID for this very mimetype");
}
- $mimetypeId = (int) $id;
+ $mimetypeId = (int)$id;
}
$this->mimetypes[$mimetypeId] = $mimetype;
@@ -136,8 +136,8 @@ class Loader implements IMimeTypeLoader {
$result->closeCursor();
foreach ($results as $row) {
- $this->mimetypes[(int) $row['id']] = $row['mimetype'];
- $this->mimetypeIds[$row['mimetype']] = (int) $row['id'];
+ $this->mimetypes[(int)$row['id']] = $row['mimetype'];
+ $this->mimetypeIds[$row['mimetype']] = (int)$row['id'];
}
}
diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php
index 0e5e433ccb6..64c7f744dd9 100644
--- a/lib/private/Files/View.php
+++ b/lib/private/Files/View.php
@@ -221,7 +221,7 @@ class View {
$relPath = '/' . $pathParts[3];
$this->lockFile($relPath, ILockingProvider::LOCK_SHARED, true);
\OC_Hook::emit(
- Filesystem::CLASSNAME, "umount",
+ Filesystem::CLASSNAME, 'umount',
[Filesystem::signal_param_path => $relPath]
);
$this->changeLock($relPath, ILockingProvider::LOCK_EXCLUSIVE, true);
@@ -229,7 +229,7 @@ class View {
$this->changeLock($relPath, ILockingProvider::LOCK_SHARED, true);
if ($result) {
\OC_Hook::emit(
- Filesystem::CLASSNAME, "post_umount",
+ Filesystem::CLASSNAME, 'post_umount',
[Filesystem::signal_param_path => $relPath]
);
}
@@ -697,7 +697,7 @@ class View {
$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
if (str_starts_with($absolutePath2, $absolutePath1 . '/')) {
- throw new ForbiddenException("Moving a folder into a child folder is forbidden", false);
+ throw new ForbiddenException('Moving a folder into a child folder is forbidden', false);
}
$targetParts = explode('/', $absolutePath2);
@@ -1330,7 +1330,7 @@ class View {
*
* @param string $path
* @param bool|string $includeMountPoints true to add mountpoint sizes,
- * 'ext' to add only ext storage mount point sizes. Defaults to true.
+ * 'ext' to add only ext storage mount point sizes. Defaults to true.
* @return \OC\Files\FileInfo|false False if file does not exist
*/
public function getFileInfo($path, $includeMountPoints = true) {
@@ -1826,28 +1826,39 @@ class View {
/**
* @param string $path
* @param string $fileName
+ * @param bool $readonly Check only if the path is allowed for read-only access
* @throws InvalidPathException
*/
- public function verifyPath($path, $fileName): void {
+ public function verifyPath($path, $fileName, $readonly = false): void {
// All of the view's functions disallow '..' in the path so we can short cut if the path is invalid
if (!Filesystem::isValidPath($path ?: '/')) {
$l = \OCP\Util::getL10N('lib');
throw new InvalidPathException($l->t('Path contains invalid segments'));
}
+ // Short cut for read-only validation
+ if ($readonly) {
+ $validator = \OCP\Server::get(FilenameValidator::class);
+ if ($validator->isForbidden($fileName)) {
+ $l = \OCP\Util::getL10N('lib');
+ throw new InvalidPathException($l->t('Filename is a reserved word'));
+ }
+ return;
+ }
+
try {
/** @type \OCP\Files\Storage $storage */
[$storage, $internalPath] = $this->resolvePath($path);
$storage->verifyPath($internalPath, $fileName);
} catch (ReservedWordException $ex) {
$l = \OCP\Util::getL10N('lib');
- throw new InvalidPathException($l->t('File name is a reserved word'));
+ throw new InvalidPathException($ex->getMessage() ?: $l->t('Filename is a reserved word'));
} catch (InvalidCharacterInPathException $ex) {
$l = \OCP\Util::getL10N('lib');
- throw new InvalidPathException($l->t('File name contains at least one invalid character'));
+ throw new InvalidPathException($ex->getMessage() ?: $l->t('Filename contains at least one invalid character'));
} catch (FileNameTooLongException $ex) {
$l = \OCP\Util::getL10N('lib');
- throw new InvalidPathException($l->t('File name is too long'));
+ throw new InvalidPathException($l->t('Filename is too long'));
} catch (InvalidDirectoryException $ex) {
$l = \OCP\Util::getL10N('lib');
throw new InvalidPathException($l->t('Dot files are not allowed'));
@@ -1889,7 +1900,7 @@ class View {
*
* @param string $absolutePath absolute path
* @param bool $useParentMount true to return parent mount instead of whatever
- * is mounted directly on the given path, false otherwise
+ * is mounted directly on the given path, false otherwise
* @return IMountPoint mount point for which to apply locks
*/
private function getMountForLock(string $absolutePath, bool $useParentMount = false): IMountPoint {
@@ -2103,7 +2114,7 @@ class View {
* @param string $absolutePath absolute path which is under "files"
*
* @return string path relative to "files" with trimmed slashes or null
- * if the path was NOT relative to files
+ * if the path was NOT relative to files
*
* @throws \InvalidArgumentException if the given path was not under "files"
* @since 8.1.0
diff --git a/lib/private/FilesMetadata/Service/IndexRequestService.php b/lib/private/FilesMetadata/Service/IndexRequestService.php
index 32248ff5c24..b50fb378325 100644
--- a/lib/private/FilesMetadata/Service/IndexRequestService.php
+++ b/lib/private/FilesMetadata/Service/IndexRequestService.php
@@ -82,9 +82,9 @@ class IndexRequestService {
private function insertIndexString(int $fileId, string $key, string $value): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert(self::TABLE_METADATA_INDEX)
- ->setValue('meta_key', $qb->createNamedParameter($key))
- ->setValue('meta_value_string', $qb->createNamedParameter($value))
- ->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
+ ->setValue('meta_key', $qb->createNamedParameter($key))
+ ->setValue('meta_value_string', $qb->createNamedParameter($value))
+ ->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
$qb->executeStatement();
}
@@ -100,9 +100,9 @@ class IndexRequestService {
public function insertIndexInt(int $fileId, string $key, int $value): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert(self::TABLE_METADATA_INDEX)
- ->setValue('meta_key', $qb->createNamedParameter($key))
- ->setValue('meta_value_int', $qb->createNamedParameter($value, IQueryBuilder::PARAM_INT))
- ->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
+ ->setValue('meta_key', $qb->createNamedParameter($key))
+ ->setValue('meta_value_int', $qb->createNamedParameter($value, IQueryBuilder::PARAM_INT))
+ ->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
$qb->executeStatement();
}
@@ -118,9 +118,9 @@ class IndexRequestService {
public function insertIndexBool(int $fileId, string $key, bool $value): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert(self::TABLE_METADATA_INDEX)
- ->setValue('meta_key', $qb->createNamedParameter($key))
- ->setValue('meta_value_int', $qb->createNamedParameter(($value) ? '1' : '0', IQueryBuilder::PARAM_INT))
- ->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
+ ->setValue('meta_key', $qb->createNamedParameter($key))
+ ->setValue('meta_value_int', $qb->createNamedParameter(($value) ? '1' : '0', IQueryBuilder::PARAM_INT))
+ ->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
$qb->executeStatement();
}
@@ -167,7 +167,7 @@ class IndexRequestService {
$qb = $this->dbConnection->getQueryBuilder();
$expr = $qb->expr();
$qb->delete(self::TABLE_METADATA_INDEX)
- ->where($expr->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
+ ->where($expr->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
if ($key !== '') {
$qb->andWhere($expr->eq('meta_key', $qb->createNamedParameter($key)));
diff --git a/lib/private/FilesMetadata/Service/MetadataRequestService.php b/lib/private/FilesMetadata/Service/MetadataRequestService.php
index 08982a2a659..b58912b0216 100644
--- a/lib/private/FilesMetadata/Service/MetadataRequestService.php
+++ b/lib/private/FilesMetadata/Service/MetadataRequestService.php
@@ -38,10 +38,10 @@ class MetadataRequestService {
public function store(IFilesMetadata $filesMetadata): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert(self::TABLE_METADATA)
- ->setValue('file_id', $qb->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT))
- ->setValue('json', $qb->createNamedParameter(json_encode($filesMetadata->jsonSerialize())))
- ->setValue('sync_token', $qb->createNamedParameter($this->generateSyncToken()))
- ->setValue('last_update', (string) $qb->createFunction('NOW()'));
+ ->setValue('file_id', $qb->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT))
+ ->setValue('json', $qb->createNamedParameter(json_encode($filesMetadata->jsonSerialize())))
+ ->setValue('sync_token', $qb->createNamedParameter($this->generateSyncToken()))
+ ->setValue('last_update', (string)$qb->createFunction('NOW()'));
$qb->executeStatement();
}
@@ -92,7 +92,7 @@ class MetadataRequestService {
$list = [];
$result = $qb->executeQuery();
while ($data = $result->fetch()) {
- $fileId = (int) $data['file_id'];
+ $fileId = (int)$data['file_id'];
$metadata = new FilesMetadata($fileId);
try {
$metadata->importFromDatabase($data);
@@ -117,7 +117,7 @@ class MetadataRequestService {
public function dropMetadata(int $fileId): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->delete(self::TABLE_METADATA)
- ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
+ ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
$qb->executeStatement();
}
@@ -134,15 +134,15 @@ class MetadataRequestService {
$expr = $qb->expr();
$qb->update(self::TABLE_METADATA)
- ->set('json', $qb->createNamedParameter(json_encode($filesMetadata->jsonSerialize())))
- ->set('sync_token', $qb->createNamedParameter($this->generateSyncToken()))
- ->set('last_update', $qb->createFunction('NOW()'))
- ->where(
- $expr->andX(
- $expr->eq('file_id', $qb->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT)),
- $expr->eq('sync_token', $qb->createNamedParameter($filesMetadata->getSyncToken()))
- )
- );
+ ->set('json', $qb->createNamedParameter(json_encode($filesMetadata->jsonSerialize())))
+ ->set('sync_token', $qb->createNamedParameter($this->generateSyncToken()))
+ ->set('last_update', $qb->createFunction('NOW()'))
+ ->where(
+ $expr->andX(
+ $expr->eq('file_id', $qb->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT)),
+ $expr->eq('sync_token', $qb->createNamedParameter($filesMetadata->getSyncToken()))
+ )
+ );
return $qb->executeStatement();
}
diff --git a/lib/private/FullTextSearch/Model/IndexDocument.php b/lib/private/FullTextSearch/Model/IndexDocument.php
index 6f9da9416fa..8bd20bad1e0 100644
--- a/lib/private/FullTextSearch/Model/IndexDocument.php
+++ b/lib/private/FullTextSearch/Model/IndexDocument.php
@@ -402,7 +402,7 @@ class IndexDocument implements IIndexDocument, JsonSerializable {
return $this;
}
- $this->hash = hash("md5", $this->getContent());
+ $this->hash = hash('md5', $this->getContent());
return $this;
}
@@ -556,9 +556,9 @@ class IndexDocument implements IIndexDocument, JsonSerializable {
* @since 16.0.0
*/
private function cleanExcerpt(string $excerpt): string {
- $excerpt = str_replace("\\n", ' ', $excerpt);
- $excerpt = str_replace("\\r", ' ', $excerpt);
- $excerpt = str_replace("\\t", ' ', $excerpt);
+ $excerpt = str_replace('\\n', ' ', $excerpt);
+ $excerpt = str_replace('\\r', ' ', $excerpt);
+ $excerpt = str_replace('\\t', ' ', $excerpt);
$excerpt = str_replace("\n", ' ', $excerpt);
$excerpt = str_replace("\r", ' ', $excerpt);
$excerpt = str_replace("\t", ' ', $excerpt);
diff --git a/lib/private/Group/Database.php b/lib/private/Group/Database.php
index 5827a31172d..72cd0ea4a91 100644
--- a/lib/private/Group/Database.php
+++ b/lib/private/Group/Database.php
@@ -325,8 +325,8 @@ class Database extends ABackend implements
$qb = $this->dbConn->getQueryBuilder();
$qb->select('gid', 'displayname')
- ->from('groups')
- ->where($qb->expr()->in('gid', $qb->createParameter('ids')));
+ ->from('groups')
+ ->where($qb->expr()->in('gid', $qb->createParameter('ids')));
foreach (array_chunk($notFoundGids, 1000) as $chunk) {
$qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
$result = $qb->executeQuery();
@@ -488,7 +488,7 @@ class Database extends ABackend implements
$displayName = $result->fetchOne();
$result->closeCursor();
- return (string) $displayName;
+ return (string)$displayName;
}
public function getGroupDetails(string $gid): array {
diff --git a/lib/private/Group/Group.php b/lib/private/Group/Group.php
index dcda7c29bb5..147c5baf543 100644
--- a/lib/private/Group/Group.php
+++ b/lib/private/Group/Group.php
@@ -30,7 +30,7 @@ use OCP\IUser;
use OCP\IUserManager;
class Group implements IGroup {
- /** @var null|string */
+ /** @var null|string */
protected $displayName;
/** @var string */
@@ -46,7 +46,7 @@ class Group implements IGroup {
private $backends;
/** @var IEventDispatcher */
private $dispatcher;
- /** @var \OC\User\Manager|IUserManager */
+ /** @var \OC\User\Manager|IUserManager */
private $userManager;
/** @var PublicEmitter */
private $emitter;
diff --git a/lib/private/Group/Manager.php b/lib/private/Group/Manager.php
index cad8e76fbbd..d18c7796805 100644
--- a/lib/private/Group/Manager.php
+++ b/lib/private/Group/Manager.php
@@ -452,7 +452,7 @@ class Manager extends PublicEmitter implements IGroupManager {
$matchingUsers = [];
foreach ($groupUsers as $groupUser) {
- $matchingUsers[(string) $groupUser->getUID()] = $groupUser->getDisplayName();
+ $matchingUsers[(string)$groupUser->getUID()] = $groupUser->getDisplayName();
}
return $matchingUsers;
}
diff --git a/lib/private/Group/MetaData.php b/lib/private/Group/MetaData.php
index da553c89a7b..fe0d931cb09 100644
--- a/lib/private/Group/MetaData.php
+++ b/lib/private/Group/MetaData.php
@@ -41,7 +41,7 @@ class MetaData {
* [0] array containing meta data about admin groups
* [1] array containing meta data about unprivileged groups
* @param string $groupSearch only effective when instance was created with
- * isAdmin being true
+ * isAdmin being true
* @param string $userSearch the pattern users are search for
*/
public function get(string $groupSearch = '', string $userSearch = ''): array {
diff --git a/lib/private/Http/Client/Client.php b/lib/private/Http/Client/Client.php
index 7cadf3fdf6e..0b72522c218 100644
--- a/lib/private/Http/Client/Client.php
+++ b/lib/private/Http/Client/Client.php
@@ -176,27 +176,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'query' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'query' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -212,22 +212,22 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -242,27 +242,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -283,27 +283,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -318,27 +318,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -353,27 +353,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -388,27 +388,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -440,27 +440,27 @@ class Client implements IClient {
* @param string $method The HTTP method to use
* @param string $uri
* @param array $options Array such as
- * 'query' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'query' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IResponse
* @throws \Exception If the request could not get completed
*/
@@ -483,27 +483,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'query' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'query' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IPromise
*/
public function getAsync(string $uri, array $options = []): IPromise {
@@ -517,22 +517,22 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IPromise
*/
public function headAsync(string $uri, array $options = []): IPromise {
@@ -546,27 +546,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IPromise
*/
public function postAsync(string $uri, array $options = []): IPromise {
@@ -585,27 +585,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IPromise
*/
public function putAsync(string $uri, array $options = []): IPromise {
@@ -619,27 +619,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IPromise
*/
public function deleteAsync(string $uri, array $options = []): IPromise {
@@ -653,27 +653,27 @@ class Client implements IClient {
*
* @param string $uri
* @param array $options Array such as
- * 'body' => [
- * 'field' => 'abc',
- * 'other_field' => '123',
- * 'file_name' => fopen('/path/to/file', 'r'),
- * ],
- * 'headers' => [
- * 'foo' => 'bar',
- * ],
- * 'cookies' => [
- * 'foo' => 'bar',
- * ],
- * 'allow_redirects' => [
- * 'max' => 10, // allow at most 10 redirects.
- * 'strict' => true, // use "strict" RFC compliant redirects.
- * 'referer' => true, // add a Referer header
- * 'protocols' => ['https'] // only allow https URLs
- * ],
- * 'sink' => '/path/to/file', // save to a file or a stream
- * 'verify' => true, // bool or string to CA file
- * 'debug' => true,
- * 'timeout' => 5,
+ * 'body' => [
+ * 'field' => 'abc',
+ * 'other_field' => '123',
+ * 'file_name' => fopen('/path/to/file', 'r'),
+ * ],
+ * 'headers' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'cookies' => [
+ * 'foo' => 'bar',
+ * ],
+ * 'allow_redirects' => [
+ * 'max' => 10, // allow at most 10 redirects.
+ * 'strict' => true, // use "strict" RFC compliant redirects.
+ * 'referer' => true, // add a Referer header
+ * 'protocols' => ['https'] // only allow https URLs
+ * ],
+ * 'sink' => '/path/to/file', // save to a file or a stream
+ * 'verify' => true, // bool or string to CA file
+ * 'debug' => true,
+ * 'timeout' => 5,
* @return IPromise
*/
public function optionsAsync(string $uri, array $options = []): IPromise {
diff --git a/lib/private/Http/Client/ClientService.php b/lib/private/Http/Client/ClientService.php
index 9a170be8752..b719f3d369d 100644
--- a/lib/private/Http/Client/ClientService.php
+++ b/lib/private/Http/Client/ClientService.php
@@ -61,7 +61,7 @@ class ClientService implements IClientService {
$stack->push($this->dnsPinMiddleware->addDnsPinning());
}
$stack->push(Middleware::tap(function (RequestInterface $request) {
- $this->eventLogger->start('http:request', $request->getMethod() . " request to " . $request->getRequestTarget());
+ $this->eventLogger->start('http:request', $request->getMethod() . ' request to ' . $request->getRequestTarget());
}, function () {
$this->eventLogger->end('http:request');
}), 'event logger');
diff --git a/lib/private/Http/Client/GuzzlePromiseAdapter.php b/lib/private/Http/Client/GuzzlePromiseAdapter.php
index dc8be9bd2e0..03a9ed9a599 100644
--- a/lib/private/Http/Client/GuzzlePromiseAdapter.php
+++ b/lib/private/Http/Client/GuzzlePromiseAdapter.php
@@ -36,7 +36,7 @@ class GuzzlePromiseAdapter implements IPromise {
* a new promise resolving to the return value of the called handler.
*
* @param ?callable(IResponse): void $onFulfilled Invoked when the promise fulfills. Gets an \OCP\Http\Client\IResponse passed in as argument
- * @param ?callable(Exception): void $onRejected Invoked when the promise is rejected. Gets an \Exception passed in as argument
+ * @param ?callable(Exception): void $onRejected Invoked when the promise is rejected. Gets an \Exception passed in as argument
*
* @return IPromise
* @since 28.0.0
@@ -115,7 +115,7 @@ class GuzzlePromiseAdapter implements IPromise {
* @return mixed
*
* @throws LogicException if the promise has no wait function or if the
- * promise does not settle after waiting.
+ * promise does not settle after waiting.
* @since 28.0.0
*/
public function wait(bool $unwrap = true): mixed {
diff --git a/lib/private/Http/Client/NegativeDnsCache.php b/lib/private/Http/Client/NegativeDnsCache.php
index d5e32fa7c2d..ca8a477d6be 100644
--- a/lib/private/Http/Client/NegativeDnsCache.php
+++ b/lib/private/Http/Client/NegativeDnsCache.php
@@ -20,11 +20,11 @@ class NegativeDnsCache {
}
private function createCacheKey(string $domain, int $type) : string {
- return $domain . "-" . (string)$type;
+ return $domain . '-' . (string)$type;
}
public function setNegativeCacheForDnsType(string $domain, int $type, int $ttl) : void {
- $this->cache->set($this->createCacheKey($domain, $type), "true", $ttl);
+ $this->cache->set($this->createCacheKey($domain, $type), 'true', $ttl);
}
public function isNegativeCached(string $domain, int $type) : bool {
diff --git a/lib/private/Http/WellKnown/RequestManager.php b/lib/private/Http/WellKnown/RequestManager.php
index 38dde0eade2..3624bf73962 100644
--- a/lib/private/Http/WellKnown/RequestManager.php
+++ b/lib/private/Http/WellKnown/RequestManager.php
@@ -74,11 +74,11 @@ class RequestManager {
$context = $this->coordinator->getRegistrationContext();
if ($context === null) {
- throw new RuntimeException("Well known handlers requested before the apps had been fully registered");
+ throw new RuntimeException('Well known handlers requested before the apps had been fully registered');
}
$registrations = $context->getWellKnownHandlers();
- $this->logger->debug(count($registrations) . " well known handlers registered");
+ $this->logger->debug(count($registrations) . ' well known handlers registered');
return array_filter(
array_map(function (ServiceRegistration $registration) {
diff --git a/lib/private/legacy/OC_Image.php b/lib/private/Image.php
index 1d2326dcce9..3dd0bc49662 100644
--- a/lib/private/legacy/OC_Image.php
+++ b/lib/private/Image.php
@@ -1,17 +1,27 @@
<?php
declare(strict_types=1);
+
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
+
+namespace OC;
+
+use finfo;
+use GdImage;
+use OCP\IAppConfig;
+use OCP\IConfig;
use OCP\IImage;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
/**
* Class for basic image manipulation
*/
-class OC_Image implements \OCP\IImage {
+class Image implements IImage {
// Default memory limit for images to load (256 MBytes).
protected const DEFAULT_MEMORY_LIMIT = 256;
@@ -21,49 +31,34 @@ class OC_Image implements \OCP\IImage {
// Default quality for webp images
protected const DEFAULT_WEBP_QUALITY = 80;
- /** @var false|\GdImage */
- protected $resource = false; // tmp resource.
- /** @var int */
- protected $imageType = IMAGETYPE_PNG; // Default to png if file type isn't evident.
- /** @var null|string */
- protected $mimeType = 'image/png'; // Default to png
- /** @var null|string */
- protected $filePath = null;
- /** @var ?finfo */
- private $fileInfo = null;
- /** @var \OCP\ILogger */
- private $logger;
- /** @var \OCP\IConfig */
- private $config;
- /** @var ?array */
- private $exif = null;
+ // tmp resource.
+ protected GdImage|false $resource = false;
+ // Default to png if file type isn't evident.
+ protected int $imageType = IMAGETYPE_PNG;
+ // Default to png
+ protected ?string $mimeType = 'image/png';
+ protected ?string $filePath = null;
+ private ?finfo $fileInfo = null;
+ private LoggerInterface $logger;
+ private IAppConfig $appConfig;
+ private IConfig $config;
+ private ?array $exif = null;
/**
- * Constructor.
- *
- * @param mixed $imageRef Deprecated, should be null
- * @psalm-assert null $imageRef
- * @param \OCP\ILogger $logger
- * @param \OCP\IConfig $config
* @throws \InvalidArgumentException in case the $imageRef parameter is not null
*/
- public function __construct($imageRef = null, ?\OCP\ILogger $logger = null, ?\OCP\IConfig $config = null) {
- $this->logger = $logger;
- if ($logger === null) {
- $this->logger = \OC::$server->getLogger();
- }
- $this->config = $config;
- if ($config === null) {
- $this->config = \OC::$server->getConfig();
- }
+ public function __construct(
+ ?LoggerInterface $logger = null,
+ ?IAppConfig $appConfig = null,
+ ?IConfig $config = null,
+ ) {
+ $this->logger = $logger ?? Server::get(LoggerInterface::class);
+ $this->appConfig = $appConfig ?? Server::get(IAppConfig::class);
+ $this->config = $config ?? Server::get(IConfig::class);
if (\OC_Util::fileInfoLoaded()) {
$this->fileInfo = new finfo(FILEINFO_MIME_TYPE);
}
-
- if ($imageRef !== null) {
- throw new \InvalidArgumentException('The first parameter in the constructor is not supported anymore. Please use any of the load* methods of the image object to load an image.');
- }
}
/**
@@ -120,7 +115,7 @@ class OC_Image implements \OCP\IImage {
*/
public function widthTopLeft(): int {
$o = $this->getOrientation();
- $this->logger->debug('OC_Image->widthTopLeft() Orientation: ' . $o, ['app' => 'core']);
+ $this->logger->debug('Image->widthTopLeft() Orientation: ' . $o, ['app' => 'core']);
switch ($o) {
case -1:
case 1:
@@ -144,7 +139,7 @@ class OC_Image implements \OCP\IImage {
*/
public function heightTopLeft(): int {
$o = $this->getOrientation();
- $this->logger->debug('OC_Image->heightTopLeft() Orientation: ' . $o, ['app' => 'core']);
+ $this->logger->debug('Image->heightTopLeft() Orientation: ' . $o, ['app' => 'core']);
switch ($o) {
case -1:
case 1:
@@ -171,7 +166,9 @@ class OC_Image implements \OCP\IImage {
if ($mimeType === null) {
$mimeType = $this->mimeType();
}
- header('Content-Type: ' . $mimeType);
+ if ($mimeType !== null) {
+ header('Content-Type: ' . $mimeType);
+ }
return $this->_output(null, $mimeType);
}
@@ -201,13 +198,10 @@ class OC_Image implements \OCP\IImage {
/**
* Outputs/saves the image.
*
- * @param string $filePath
- * @param string $mimeType
- * @return bool
- * @throws Exception
+ * @throws \Exception
*/
private function _output(?string $filePath = null, ?string $mimeType = null): bool {
- if ($filePath) {
+ if ($filePath !== null && $filePath !== '') {
if (!file_exists(dirname($filePath))) {
mkdir(dirname($filePath), 0777, true);
}
@@ -247,7 +241,7 @@ class OC_Image implements \OCP\IImage {
$imageType = IMAGETYPE_WEBP;
break;
default:
- throw new Exception('\OC_Image::_output(): "' . $mimeType . '" is not supported when forcing a specific output format');
+ throw new \Exception('Image::_output(): "' . $mimeType . '" is not supported when forcing a specific output format');
}
}
@@ -266,7 +260,7 @@ class OC_Image implements \OCP\IImage {
if (function_exists('imagexbm')) {
$retVal = imagexbm($this->resource, $filePath);
} else {
- throw new Exception('\OC_Image::_output(): imagexbm() is not supported.');
+ throw new \Exception('Image::_output(): imagexbm() is not supported.');
}
break;
@@ -334,27 +328,27 @@ class OC_Image implements \OCP\IImage {
}
ob_start();
switch ($this->mimeType) {
- case "image/png":
+ case 'image/png':
$res = imagepng($this->resource);
break;
- case "image/jpeg":
+ case 'image/jpeg':
imageinterlace($this->resource, true);
$quality = $this->getJpegQuality();
$res = imagejpeg($this->resource, null, $quality);
break;
- case "image/gif":
+ case 'image/gif':
$res = imagegif($this->resource);
break;
- case "image/webp":
+ case 'image/webp':
$res = imagewebp($this->resource, null, $this->getWebpQuality());
break;
default:
$res = imagepng($this->resource);
- $this->logger->info('OC_Image->data. Could not guess mime-type, defaulting to png', ['app' => 'core']);
+ $this->logger->info('Image->data. Could not guess mime-type, defaulting to png', ['app' => 'core']);
break;
}
if (!$res) {
- $this->logger->error('OC_Image->data. Error getting image data.', ['app' => 'core']);
+ $this->logger->error('Image->data. Error getting image data.', ['app' => 'core']);
}
return ob_get_clean();
}
@@ -363,31 +357,34 @@ class OC_Image implements \OCP\IImage {
* @return string - base64 encoded, which is suitable for embedding in a VCard.
*/
public function __toString(): string {
- return base64_encode($this->data());
+ $data = $this->data();
+ if ($data === null) {
+ return '';
+ } else {
+ return base64_encode($data);
+ }
}
- /**
- * @return int
- */
protected function getJpegQuality(): int {
- $quality = $this->config->getAppValue('preview', 'jpeg_quality', (string) self::DEFAULT_JPEG_QUALITY);
- // TODO: remove when getAppValue is type safe
- if ($quality === null) {
- $quality = self::DEFAULT_JPEG_QUALITY;
- }
- return min(100, max(10, (int) $quality));
+ $quality = $this->appConfig->getValueInt('preview', 'jpeg_quality', self::DEFAULT_JPEG_QUALITY);
+ return min(100, max(10, $quality));
}
- /**
- * @return int
- */
protected function getWebpQuality(): int {
- $quality = $this->config->getAppValue('preview', 'webp_quality', (string) self::DEFAULT_WEBP_QUALITY);
- // TODO: remove when getAppValue is type safe
- if ($quality === null) {
- $quality = self::DEFAULT_WEBP_QUALITY;
+ $quality = $this->appConfig->getValueInt('preview', 'webp_quality', self::DEFAULT_WEBP_QUALITY);
+ return min(100, max(10, $quality));
+ }
+
+ private function isValidExifData(array $exif): bool {
+ if (!isset($exif['Orientation'])) {
+ return false;
}
- return min(100, max(10, (int) $quality));
+
+ if (!is_numeric($exif['Orientation'])) {
+ return false;
+ }
+
+ return true;
}
/**
@@ -402,47 +399,41 @@ class OC_Image implements \OCP\IImage {
}
if ($this->imageType !== IMAGETYPE_JPEG) {
- $this->logger->debug('OC_Image->fixOrientation() Image is not a JPEG.', ['app' => 'core']);
+ $this->logger->debug('Image->fixOrientation() Image is not a JPEG.', ['app' => 'core']);
return -1;
}
if (!is_callable('exif_read_data')) {
- $this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
+ $this->logger->debug('Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
return -1;
}
if (!$this->valid()) {
- $this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']);
+ $this->logger->debug('Image->fixOrientation() No image loaded.', ['app' => 'core']);
return -1;
}
if (is_null($this->filePath) || !is_readable($this->filePath)) {
- $this->logger->debug('OC_Image->fixOrientation() No readable file path set.', ['app' => 'core']);
+ $this->logger->debug('Image->fixOrientation() No readable file path set.', ['app' => 'core']);
return -1;
}
$exif = @exif_read_data($this->filePath, 'IFD0');
- if (!$exif) {
- return -1;
- }
- if (!isset($exif['Orientation'])) {
+ if ($exif === false || !$this->isValidExifData($exif)) {
return -1;
}
$this->exif = $exif;
- return $exif['Orientation'];
+ return (int)$exif['Orientation'];
}
- public function readExif($data): void {
+ public function readExif(string $data): void {
if (!is_callable('exif_read_data')) {
- $this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
+ $this->logger->debug('Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
return;
}
if (!$this->valid()) {
- $this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']);
+ $this->logger->debug('Image->fixOrientation() No image loaded.', ['app' => 'core']);
return;
}
$exif = @exif_read_data('data://image/jpeg;base64,' . base64_encode($data));
- if (!$exif) {
- return;
- }
- if (!isset($exif['Orientation'])) {
+ if ($exif === false || !$this->isValidExifData($exif)) {
return;
}
$this->exif = $exif;
@@ -460,7 +451,7 @@ class OC_Image implements \OCP\IImage {
return false;
}
$o = $this->getOrientation();
- $this->logger->debug('OC_Image->fixOrientation() Orientation: ' . $o, ['app' => 'core']);
+ $this->logger->debug('Image->fixOrientation() Orientation: ' . $o, ['app' => 'core']);
$rotate = 0;
$flip = false;
switch ($o) {
@@ -507,15 +498,15 @@ class OC_Image implements \OCP\IImage {
$this->resource = $res;
return true;
} else {
- $this->logger->debug('OC_Image->fixOrientation() Error during alpha-saving', ['app' => 'core']);
+ $this->logger->debug('Image->fixOrientation() Error during alpha-saving', ['app' => 'core']);
return false;
}
} else {
- $this->logger->debug('OC_Image->fixOrientation() Error during alpha-blending', ['app' => 'core']);
+ $this->logger->debug('Image->fixOrientation() Error during alpha-blending', ['app' => 'core']);
return false;
}
} else {
- $this->logger->debug('OC_Image->fixOrientation() Error during orientation fixing', ['app' => 'core']);
+ $this->logger->debug('Image->fixOrientation() Error during orientation fixing', ['app' => 'core']);
return false;
}
}
@@ -626,10 +617,10 @@ class OC_Image implements \OCP\IImage {
imagealphablending($this->resource, true);
imagesavealpha($this->resource, true);
} else {
- $this->logger->debug('OC_Image->loadFromFile, GIF image not valid: ' . $imagePath, ['app' => 'core']);
+ $this->logger->debug('Image->loadFromFile, GIF image not valid: ' . $imagePath, ['app' => 'core']);
}
} else {
- $this->logger->debug('OC_Image->loadFromFile, GIF images not supported: ' . $imagePath, ['app' => 'core']);
+ $this->logger->debug('Image->loadFromFile, GIF images not supported: ' . $imagePath, ['app' => 'core']);
}
break;
case IMAGETYPE_JPEG:
@@ -640,10 +631,10 @@ class OC_Image implements \OCP\IImage {
if (@getimagesize($imagePath) !== false) {
$this->resource = @imagecreatefromjpeg($imagePath);
} else {
- $this->logger->debug('OC_Image->loadFromFile, JPG image not valid: ' . $imagePath, ['app' => 'core']);
+ $this->logger->debug('Image->loadFromFile, JPG image not valid: ' . $imagePath, ['app' => 'core']);
}
} else {
- $this->logger->debug('OC_Image->loadFromFile, JPG images not supported: ' . $imagePath, ['app' => 'core']);
+ $this->logger->debug('Image->loadFromFile, JPG images not supported: ' . $imagePath, ['app' => 'core']);
}
break;
case IMAGETYPE_PNG:
@@ -657,10 +648,10 @@ class OC_Image implements \OCP\IImage {
imagealphablending($this->resource, true);
imagesavealpha($this->resource, true);
} else {
- $this->logger->debug('OC_Image->loadFromFile, PNG image not valid: ' . $imagePath, ['app' => 'core']);
+ $this->logger->debug('Image->loadFromFile, PNG image not valid: ' . $imagePath, ['app' => 'core']);
}
} else {
- $this->logger->debug('OC_Image->loadFromFile, PNG images not supported: ' . $imagePath, ['app' => 'core']);
+ $this->logger->debug('Image->loadFromFile, PNG images not supported: ' . $imagePath, ['app' => 'core']);
}
break;
case IMAGETYPE_XBM:
@@ -670,7 +661,7 @@ class OC_Image implements \OCP\IImage {
}
$this->resource = @imagecreatefromxbm($imagePath);
} else {
- $this->logger->debug('OC_Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']);
+ $this->logger->debug('Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']);
}
break;
case IMAGETYPE_WBMP:
@@ -680,7 +671,7 @@ class OC_Image implements \OCP\IImage {
}
$this->resource = @imagecreatefromwbmp($imagePath);
} else {
- $this->logger->debug('OC_Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']);
+ $this->logger->debug('Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']);
}
break;
case IMAGETYPE_BMP:
@@ -702,7 +693,7 @@ class OC_Image implements \OCP\IImage {
return false;
}
$data = fread($fp, 90);
- if (!$data) {
+ if ($data === false) {
return false;
}
fclose($fp);
@@ -716,7 +707,7 @@ class OC_Image implements \OCP\IImage {
$header = unpack($headerFormat, $data);
unset($data, $headerFormat);
- if (!$header) {
+ if ($header === false) {
return false;
}
@@ -734,13 +725,13 @@ class OC_Image implements \OCP\IImage {
// Check for animation indicators
if (strpos(strtoupper($header['Chunk']), 'ANIM') !== false || strpos(strtoupper($header['Chunk']), 'ANMF') !== false) {
// Animated so don't let it reach libgd
- $this->logger->debug('OC_Image->loadFromFile, animated WEBP images not supported: ' . $imagePath, ['app' => 'core']);
+ $this->logger->debug('Image->loadFromFile, animated WEBP images not supported: ' . $imagePath, ['app' => 'core']);
} else {
// We're safe so give it to libgd
$this->resource = @imagecreatefromwebp($imagePath);
}
} else {
- $this->logger->debug('OC_Image->loadFromFile, WEBP images not supported: ' . $imagePath, ['app' => 'core']);
+ $this->logger->debug('Image->loadFromFile, WEBP images not supported: ' . $imagePath, ['app' => 'core']);
}
break;
/*
@@ -776,7 +767,7 @@ class OC_Image implements \OCP\IImage {
}
$this->resource = @imagecreatefromstring($data);
$iType = IMAGETYPE_PNG;
- $this->logger->debug('OC_Image->loadFromFile, Default', ['app' => 'core']);
+ $this->logger->debug('Image->loadFromFile, Default', ['app' => 'core']);
break;
}
if ($this->valid()) {
@@ -788,12 +779,9 @@ class OC_Image implements \OCP\IImage {
}
/**
- * Loads an image from a string of data.
- *
- * @param string $str A string of image data as read from a file.
- * @return bool|\GdImage An image resource or false on error
+ * @inheritDoc
*/
- public function loadFromData(string $str) {
+ public function loadFromData(string $str): GdImage|false {
if (!$this->checkImageDataSize($str)) {
return false;
}
@@ -807,7 +795,7 @@ class OC_Image implements \OCP\IImage {
}
if (!$this->resource) {
- $this->logger->debug('OC_Image->loadFromFile, could not load', ['app' => 'core']);
+ $this->logger->debug('Image->loadFromFile, could not load', ['app' => 'core']);
return false;
}
return $this->resource;
@@ -830,7 +818,7 @@ class OC_Image implements \OCP\IImage {
$this->mimeType = $this->fileInfo->buffer($data);
}
if (!$this->resource) {
- $this->logger->debug('OC_Image->loadFromBase64, could not load', ['app' => 'core']);
+ $this->logger->debug('Image->loadFromBase64, could not load', ['app' => 'core']);
return false;
}
return $this->resource;
@@ -856,11 +844,7 @@ class OC_Image implements \OCP\IImage {
return $this->valid();
}
- /**
- * @param $maxSize
- * @return bool|\GdImage
- */
- private function resizeNew(int $maxSize) {
+ private function resizeNew(int $maxSize): \GdImage|false {
if (!$this->valid()) {
$this->logger->debug(__METHOD__ . '(): No image loaded', ['app' => 'core']);
return false;
@@ -896,12 +880,7 @@ class OC_Image implements \OCP\IImage {
return $this->valid();
}
- /**
- * @param int $width
- * @param int $height
- * @return bool|\GdImage
- */
- public function preciseResizeNew(int $width, int $height) {
+ public function preciseResizeNew(int $width, int $height): \GdImage|false {
if (!($width > 0) || !($height > 0)) {
$this->logger->info(__METHOD__ . '(): Requested image size not bigger than 0', ['app' => 'core']);
return false;
@@ -920,7 +899,11 @@ class OC_Image implements \OCP\IImage {
// preserve transparency
if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
- imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
+ $alpha = imagecolorallocatealpha($process, 0, 0, 0, 127);
+ if ($alpha === false) {
+ $alpha = null;
+ }
+ imagecolortransparent($process, $alpha);
imagealphablending($process, false);
imagesavealpha($process, true);
}
@@ -942,7 +925,7 @@ class OC_Image implements \OCP\IImage {
*/
public function centerCrop(int $size = 0): bool {
if (!$this->valid()) {
- $this->logger->debug('OC_Image->centerCrop, No image loaded', ['app' => 'core']);
+ $this->logger->debug('Image->centerCrop, No image loaded', ['app' => 'core']);
return false;
}
$widthOrig = imagesx($this->resource);
@@ -954,10 +937,10 @@ class OC_Image implements \OCP\IImage {
$width = $height = min($widthOrig, $heightOrig);
if ($ratioOrig > 1) {
- $x = (int) (($widthOrig / 2) - ($width / 2));
+ $x = (int)(($widthOrig / 2) - ($width / 2));
$y = 0;
} else {
- $y = (int) (($heightOrig / 2) - ($height / 2));
+ $y = (int)(($heightOrig / 2) - ($height / 2));
$x = 0;
}
if ($size > 0) {
@@ -969,20 +952,24 @@ class OC_Image implements \OCP\IImage {
}
$process = imagecreatetruecolor($targetWidth, $targetHeight);
if ($process === false) {
- $this->logger->debug('OC_Image->centerCrop, Error creating true color image', ['app' => 'core']);
+ $this->logger->debug('Image->centerCrop, Error creating true color image', ['app' => 'core']);
return false;
}
// preserve transparency
if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
- imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127) ?: null);
+ $alpha = imagecolorallocatealpha($process, 0, 0, 0, 127);
+ if ($alpha === false) {
+ $alpha = null;
+ }
+ imagecolortransparent($process, $alpha);
imagealphablending($process, false);
imagesavealpha($process, true);
}
$result = imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $targetWidth, $targetHeight, $width, $height);
if ($result === false) {
- $this->logger->debug('OC_Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, ['app' => 'core']);
+ $this->logger->debug('Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, ['app' => 'core']);
return false;
}
imagedestroy($this->resource);
@@ -1032,7 +1019,11 @@ class OC_Image implements \OCP\IImage {
// preserve transparency
if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
- imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127) ?: null);
+ $alpha = imagecolorallocatealpha($process, 0, 0, 0, 127);
+ if ($alpha === false) {
+ $alpha = null;
+ }
+ imagecolortransparent($process, $alpha);
imagealphablending($process, false);
imagesavealpha($process, true);
}
@@ -1093,11 +1084,19 @@ class OC_Image implements \OCP\IImage {
}
public function copy(): IImage {
- $image = new OC_Image(null, $this->logger, $this->config);
+ $image = new self($this->logger, $this->appConfig, $this->config);
+ if (!$this->valid()) {
+ /* image is invalid, return an empty one */
+ return $image;
+ }
$image->resource = imagecreatetruecolor($this->width(), $this->height());
+ if (!$image->valid()) {
+ /* image creation failed, cannot copy in it */
+ return $image;
+ }
imagecopy(
- $image->resource(),
- $this->resource(),
+ $image->resource,
+ $this->resource,
0,
0,
0,
@@ -1110,7 +1109,7 @@ class OC_Image implements \OCP\IImage {
}
public function cropCopy(int $x, int $y, int $w, int $h): IImage {
- $image = new OC_Image(null, $this->logger, $this->config);
+ $image = new self($this->logger, $this->appConfig, $this->config);
$image->imageType = $this->imageType;
$image->mimeType = $this->mimeType;
$image->resource = $this->cropNew($x, $y, $w, $h);
@@ -1119,7 +1118,7 @@ class OC_Image implements \OCP\IImage {
}
public function preciseResizeCopy(int $width, int $height): IImage {
- $image = new OC_Image(null, $this->logger, $this->config);
+ $image = new self($this->logger, $this->appConfig, $this->config);
$image->imageType = $this->imageType;
$image->mimeType = $this->mimeType;
$image->resource = $this->preciseResizeNew($width, $height);
@@ -1128,7 +1127,7 @@ class OC_Image implements \OCP\IImage {
}
public function resizeCopy(int $maxSize): IImage {
- $image = new OC_Image(null, $this->logger, $this->config);
+ $image = new self($this->logger, $this->appConfig, $this->config);
$image->imageType = $this->imageType;
$image->mimeType = $this->mimeType;
$image->resource = $this->resizeNew($maxSize);
diff --git a/lib/private/Installer.php b/lib/private/Installer.php
index ad80b26d8bc..e0f7644304e 100644
--- a/lib/private/Installer.php
+++ b/lib/private/Installer.php
@@ -499,7 +499,7 @@ class Installer {
while (false !== ($filename = readdir($dir))) {
if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
- if ($config->getAppValue($filename, "installed_version", null) === null) {
+ if ($config->getAppValue($filename, 'installed_version', null) === null) {
$enabled = $appManager->isDefaultEnabled($filename);
if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
&& $config->getAppValue($filename, 'enabled') !== 'no') {
diff --git a/lib/private/L10N/L10N.php b/lib/private/L10N/L10N.php
index 39d778f80d6..50db373a65d 100644
--- a/lib/private/L10N/L10N.php
+++ b/lib/private/L10N/L10N.php
@@ -83,7 +83,7 @@ class L10N implements IL10N {
$parameters = [$parameters];
}
- return (string) new L10NString($this, $text, $parameters);
+ return (string)new L10NString($this, $text, $parameters);
}
/**
@@ -104,14 +104,14 @@ class L10N implements IL10N {
public function n(string $text_singular, string $text_plural, int $count, array $parameters = []): string {
$identifier = "_{$text_singular}_::_{$text_plural}_";
if (isset($this->translations[$identifier])) {
- return (string) new L10NString($this, $identifier, $parameters, $count);
+ return (string)new L10NString($this, $identifier, $parameters, $count);
}
if ($count === 1) {
- return (string) new L10NString($this, $text_singular, $parameters, $count);
+ return (string)new L10NString($this, $text_singular, $parameters, $count);
}
- return (string) new L10NString($this, $text_plural, $parameters, $count);
+ return (string)new L10NString($this, $text_plural, $parameters, $count);
}
/**
@@ -146,10 +146,10 @@ class L10N implements IL10N {
}
if ($type === 'firstday') {
- return (int) Calendar::getFirstWeekday($this->locale);
+ return (int)Calendar::getFirstWeekday($this->locale);
}
if ($type === 'jsdate') {
- return (string) Calendar::getDateFormat('short', $this->locale);
+ return (string)Calendar::getDateFormat('short', $this->locale);
}
$value = new \DateTime();
@@ -167,13 +167,13 @@ class L10N implements IL10N {
$width = $options['width'];
switch ($type) {
case 'date':
- return (string) Calendar::formatDate($value, $width, $this->locale);
+ return (string)Calendar::formatDate($value, $width, $this->locale);
case 'datetime':
- return (string) Calendar::formatDatetime($value, $width, $this->locale);
+ return (string)Calendar::formatDatetime($value, $width, $this->locale);
case 'time':
- return (string) Calendar::formatTime($value, $width, $this->locale);
+ return (string)Calendar::formatTime($value, $width, $this->locale);
case 'weekdayName':
- return (string) Calendar::getWeekdayName($value, $width, $this->locale);
+ return (string)Calendar::getWeekdayName($value, $width, $this->locale);
default:
return false;
}
diff --git a/lib/private/LDAP/NullLDAPProviderFactory.php b/lib/private/LDAP/NullLDAPProviderFactory.php
index 55561b5692e..60588e4d15b 100644
--- a/lib/private/LDAP/NullLDAPProviderFactory.php
+++ b/lib/private/LDAP/NullLDAPProviderFactory.php
@@ -16,7 +16,7 @@ class NullLDAPProviderFactory implements ILDAPProviderFactory {
}
public function getLDAPProvider() {
- throw new \Exception("No LDAP provider is available");
+ throw new \Exception('No LDAP provider is available');
}
public function isAvailable(): bool {
diff --git a/lib/private/LargeFileHelper.php b/lib/private/LargeFileHelper.php
index 4d96e79ead4..238fb0790b8 100755
--- a/lib/private/LargeFileHelper.php
+++ b/lib/private/LargeFileHelper.php
@@ -153,7 +153,7 @@ class LargeFileHelper {
// For file sizes between 2 GiB and 4 GiB, filesize() will return a
// negative int, as the PHP data type int is signed. Interpret the
// returned int as an unsigned integer and put it into a float.
- return (float) sprintf('%u', $result);
+ return (float)sprintf('%u', $result);
}
return $result;
}
diff --git a/lib/private/Lock/MemcacheLockingProvider.php b/lib/private/Lock/MemcacheLockingProvider.php
index 883abb5da98..b249e08d717 100644
--- a/lib/private/Lock/MemcacheLockingProvider.php
+++ b/lib/private/Lock/MemcacheLockingProvider.php
@@ -62,8 +62,8 @@ class MemcacheLockingProvider extends AbstractLockingProvider {
if ($type === self::LOCK_SHARED) {
// save the old TTL to for `restoreTTL`
$this->oldTTLs[$path] = [
- "ttl" => $this->getTTL($path),
- "time" => $this->timeFactory->getTime()
+ 'ttl' => $this->getTTL($path),
+ 'time' => $this->timeFactory->getTime()
];
if (!$this->memcache->inc($path)) {
throw new LockedException($path, null, $this->getExistingLockForException($path), $readablePath);
diff --git a/lib/private/Log.php b/lib/private/Log.php
index 4fce0436707..4f6003cf53d 100644
--- a/lib/private/Log.php
+++ b/lib/private/Log.php
@@ -41,13 +41,9 @@ class Log implements ILogger, IDataLogger {
public function __construct(
private IWriter $logger,
private SystemConfig $config,
- private ?Normalizer $normalizer = null,
+ private Normalizer $normalizer = new Normalizer(),
private ?IRegistry $crashReporters = null
) {
- // FIXME: php8.1 allows "private Normalizer $normalizer = new Normalizer()," in initializer
- if ($normalizer === null) {
- $this->normalizer = new Normalizer();
- }
}
public function setEventDispatcher(IEventDispatcher $eventDispatcher): void {
@@ -330,7 +326,7 @@ class Log implements ILogger, IDataLogger {
try {
$serializer = $this->getSerializer();
} catch (Throwable $e) {
- $this->error("Failed to load ExceptionSerializer serializer while trying to log " . $exception->getMessage());
+ $this->error('Failed to load ExceptionSerializer serializer while trying to log ' . $exception->getMessage());
return;
}
$data = $context;
diff --git a/lib/private/Log/File.php b/lib/private/Log/File.php
index 28cc856b980..bc14de4ffdf 100644
--- a/lib/private/Log/File.php
+++ b/lib/private/Log/File.php
@@ -73,7 +73,7 @@ class File extends LogDetails implements IWriter, IFileBased {
* get entries from the log in reverse chronological order
*/
public function getEntries(int $limit = 50, int $offset = 0): array {
- $minLevel = $this->config->getValue("loglevel", ILogger::WARN);
+ $minLevel = $this->config->getValue('loglevel', ILogger::WARN);
$entries = [];
$handle = @fopen($this->logFile, 'rb');
if ($handle) {
diff --git a/lib/private/Log/LogDetails.php b/lib/private/Log/LogDetails.php
index bf2c1a22c49..95b09c0a181 100644
--- a/lib/private/Log/LogDetails.php
+++ b/lib/private/Log/LogDetails.php
@@ -22,7 +22,7 @@ abstract class LogDetails {
} catch (\Exception $e) {
$timezone = new \DateTimeZone('UTC');
}
- $time = \DateTime::createFromFormat("U.u", number_format(microtime(true), 4, ".", ""));
+ $time = \DateTime::createFromFormat('U.u', number_format(microtime(true), 4, '.', ''));
if ($time === false) {
$time = new \DateTime('now', $timezone);
} else {
diff --git a/lib/private/Log/PsrLoggerAdapter.php b/lib/private/Log/PsrLoggerAdapter.php
index 16e609eefdb..57a67bd67a7 100644
--- a/lib/private/Log/PsrLoggerAdapter.php
+++ b/lib/private/Log/PsrLoggerAdapter.php
@@ -35,7 +35,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
/**
* System is unusable.
*
- * @param $message
+ * @param $message
* @param mixed[] $context
*/
public function emergency($message, array $context = []): void {
@@ -58,7 +58,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
- * @param $message
+ * @param $message
* @param mixed[] $context
*/
public function alert($message, array $context = []): void {
@@ -80,7 +80,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
*
* Example: Application component unavailable, unexpected exception.
*
- * @param $message
+ * @param $message
* @param mixed[] $context
*/
public function critical($message, array $context = []): void {
@@ -101,7 +101,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
- * @param $message
+ * @param $message
* @param mixed[] $context
*/
public function error($message, array $context = []): void {
@@ -124,7 +124,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
- * @param $message
+ * @param $message
* @param mixed[] $context
*/
public function warning($message, array $context = []): void {
@@ -144,7 +144,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
/**
* Normal but significant events.
*
- * @param $message
+ * @param $message
* @param mixed[] $context
*/
public function notice($message, array $context = []): void {
@@ -166,7 +166,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
*
* Example: User logs in, SQL logs.
*
- * @param $message
+ * @param $message
* @param mixed[] $context
*/
public function info($message, array $context = []): void {
@@ -186,7 +186,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
/**
* Detailed debug information.
*
- * @param $message
+ * @param $message
* @param mixed[] $context
*/
public function debug($message, array $context = []): void {
@@ -207,7 +207,7 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
* Logs with an arbitrary level.
*
* @param mixed $level
- * @param $message
+ * @param $message
* @param mixed[] $context
*
* @throws InvalidArgumentException
diff --git a/lib/private/Mail/EMailTemplate.php b/lib/private/Mail/EMailTemplate.php
index 8047cb80bad..2cb222fd137 100644
--- a/lib/private/Mail/EMailTemplate.php
+++ b/lib/private/Mail/EMailTemplate.php
@@ -346,7 +346,7 @@ EOF;
* Adds a heading to the email
*
* @param string|bool $plainTitle Title that is used in the plain text email
- * if empty the $title is used, if false none will be used
+ * if empty the $title is used, if false none will be used
*/
public function addHeading(string $title, $plainTitle = ''): void {
if ($this->footerAdded) {
@@ -379,7 +379,7 @@ EOF;
*
* @param string $text Note: When $plainText falls back to this, HTML is automatically escaped in the HTML email
* @param string|bool $plainText Text that is used in the plain text email
- * if empty the $text is used, if false none will be used
+ * if empty the $text is used, if false none will be used
*/
public function addBodyText(string $text, $plainText = ''): void {
if ($this->footerAdded) {
@@ -406,9 +406,9 @@ EOF;
* @param string $metaInfo Note: When $plainMetaInfo falls back to this, HTML is automatically escaped in the HTML email
* @param string $icon Absolute path, must be 16*16 pixels
* @param string|bool $plainText Text that is used in the plain text email
- * if empty or true the $text is used, if false none will be used
+ * if empty or true the $text is used, if false none will be used
* @param string|bool $plainMetaInfo Meta info that is used in the plain text email
- * if empty or true the $metaInfo is used, if false none will be used
+ * if empty or true the $metaInfo is used, if false none will be used
* @param integer $plainIndent plainIndent If > 0, Indent plainText by this amount.
* @since 12.0.0
*/
@@ -425,7 +425,7 @@ EOF;
if ($plainText === '' || $plainText === true) {
$plainText = $text;
$text = htmlspecialchars($text);
- $text = str_replace("\n", "<br/>", $text); // convert newlines to HTML breaks
+ $text = str_replace("\n", '<br/>', $text); // convert newlines to HTML breaks
}
if ($plainMetaInfo === '' || $plainMetaInfo === true) {
$plainMetaInfo = $metaInfo;
@@ -536,7 +536,7 @@ EOF;
* @param string $text Text of button; Note: When $plainText falls back to this, HTML is automatically escaped in the HTML email
* @param string $url URL of button
* @param string|false $plainText Text of button in plain text version
- * if empty the $text is used, if false none will be used
+ * if empty the $text is used, if false none will be used
*
* @since 12.0.0
*/
diff --git a/lib/private/Mail/Mailer.php b/lib/private/Mail/Mailer.php
index 4ddb748fc26..0a818b847aa 100644
--- a/lib/private/Mail/Mailer.php
+++ b/lib/private/Mail/Mailer.php
@@ -103,23 +103,11 @@ class Mailer implements IMailer {
* @since 12.0.0
*/
public function createEMailTemplate(string $emailId, array $data = []): IEMailTemplate {
- $class = $this->config->getSystemValueString('mail_template_class', '');
-
- if ($class !== '' && class_exists($class) && is_a($class, EMailTemplate::class, true)) {
- return new $class(
- $this->defaults,
- $this->urlGenerator,
- $this->l10nFactory,
- $emailId,
- $data
- );
- }
-
$logoDimensions = $this->config->getAppValue('theming', 'logoDimensions', self::DEFAULT_DIMENSIONS);
if (str_contains($logoDimensions, 'x')) {
[$width, $height] = explode('x', $logoDimensions);
- $width = (int) $width;
- $height = (int) $height;
+ $width = (int)$width;
+ $height = (int)$height;
if ($width > self::MAX_LOGO_SIZE || $height > self::MAX_LOGO_SIZE) {
if ($width === $height) {
@@ -127,9 +115,9 @@ class Mailer implements IMailer {
$logoHeight = self::MAX_LOGO_SIZE;
} elseif ($width > $height) {
$logoWidth = self::MAX_LOGO_SIZE;
- $logoHeight = (int) (($height / $width) * self::MAX_LOGO_SIZE);
+ $logoHeight = (int)(($height / $width) * self::MAX_LOGO_SIZE);
} else {
- $logoWidth = (int) (($width / $height) * self::MAX_LOGO_SIZE);
+ $logoWidth = (int)(($width / $height) * self::MAX_LOGO_SIZE);
$logoHeight = self::MAX_LOGO_SIZE;
}
} else {
@@ -140,6 +128,20 @@ class Mailer implements IMailer {
$logoWidth = $logoHeight = null;
}
+ $class = $this->config->getSystemValueString('mail_template_class', '');
+
+ if ($class !== '' && class_exists($class) && is_a($class, EMailTemplate::class, true)) {
+ return new $class(
+ $this->defaults,
+ $this->urlGenerator,
+ $this->l10nFactory,
+ $logoWidth,
+ $logoHeight,
+ $emailId,
+ $data
+ );
+ }
+
return new EMailTemplate(
$this->defaults,
$this->urlGenerator,
diff --git a/lib/private/Mail/Provider/Manager.php b/lib/private/Mail/Provider/Manager.php
index 244aa86d68d..14ffeac287b 100644
--- a/lib/private/Mail/Provider/Manager.php
+++ b/lib/private/Mail/Provider/Manager.php
@@ -60,7 +60,7 @@ class Manager implements IManager {
*
* @since 30.0.0
*
- * @return array<string,string> collection of provider id and label ['jmap' => 'JMap Connector']
+ * @return array<string,string> collection of provider id and label ['jmap' => 'JMap Connector']
*/
public function types(): array {
@@ -80,7 +80,7 @@ class Manager implements IManager {
*
* @since 30.0.0
*
- * @return array<string,IProvider> collection of provider id and object ['jmap' => IProviderObject]
+ * @return array<string,IProvider> collection of provider id and object ['jmap' => IProviderObject]
*/
public function providers(): array {
@@ -121,11 +121,11 @@ class Manager implements IManager {
*
* @since 30.0.0
*
- * @param string $providerId provider id
+ * @param string $providerId provider id
*
* @return IProvider|null
*/
- public function findProviderById(string $providerId): IProvider | null {
+ public function findProviderById(string $providerId): IProvider|null {
// evaluate if we already have a cached collection of providers
if (!is_array($this->providersCollection)) {
@@ -145,9 +145,9 @@ class Manager implements IManager {
*
* @since 30.0.0
*
- * @param string $userId user id
+ * @param string $userId user id
*
- * @return array<string,array<string,IService>> collection of provider id, service id and object ['jmap' => ['Service1' => IServiceObject]]
+ * @return array<string,array<string,IService>> collection of provider id, service id and object ['jmap' => ['Service1' => IServiceObject]]
*/
public function services(string $userId): array {
@@ -172,13 +172,13 @@ class Manager implements IManager {
*
* @since 30.0.0
*
- * @param string $userId user id
- * @param string $serviceId service id
- * @param string $providerId provider id
+ * @param string $userId user id
+ * @param string $serviceId service id
+ * @param string $providerId provider id
*
- * @return IService|null returns service object or null if none found
+ * @return IService|null returns service object or null if none found
*/
- public function findServiceById(string $userId, string $serviceId, ?string $providerId = null): IService | null {
+ public function findServiceById(string $userId, string $serviceId, ?string $providerId = null): IService|null {
// evaluate if provider id was specified
if ($providerId !== null) {
@@ -216,13 +216,13 @@ class Manager implements IManager {
*
* @since 30.0.0
*
- * @param string $userId user id
- * @param string $address mail address (e.g. test@example.com)
- * @param string $providerId provider id
+ * @param string $userId user id
+ * @param string $address mail address (e.g. test@example.com)
+ * @param string $providerId provider id
*
- * @return IService|null returns service object or null if none found
+ * @return IService|null returns service object or null if none found
*/
- public function findServiceByAddress(string $userId, string $address, ?string $providerId = null): IService | null {
+ public function findServiceByAddress(string $userId, string $address, ?string $providerId = null): IService|null {
// evaluate if provider id was specified
if ($providerId !== null) {
diff --git a/lib/private/Memcache/CADTrait.php b/lib/private/Memcache/CADTrait.php
index bb010e238dc..3bf94246338 100644
--- a/lib/private/Memcache/CADTrait.php
+++ b/lib/private/Memcache/CADTrait.php
@@ -35,4 +35,21 @@ trait CADTrait {
return false;
}
}
+
+ public function ncad(string $key, mixed $old): bool {
+ //no native cad, emulate with locking
+ if ($this->add($key . '_lock', true)) {
+ $value = $this->get($key);
+ if ($value !== null && $value !== $old) {
+ $this->remove($key);
+ $this->remove($key . '_lock');
+ return true;
+ } else {
+ $this->remove($key . '_lock');
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
}
diff --git a/lib/private/Memcache/Factory.php b/lib/private/Memcache/Factory.php
index c0f4f787200..931c871d0f1 100644
--- a/lib/private/Memcache/Factory.php
+++ b/lib/private/Memcache/Factory.php
@@ -16,7 +16,7 @@ use Psr\Log\LoggerInterface;
class Factory implements ICacheFactory {
public const NULL_CACHE = NullCache::class;
- private string $globalPrefix;
+ private ?string $globalPrefix = null;
private LoggerInterface $logger;
@@ -40,17 +40,23 @@ class Factory implements ICacheFactory {
private IProfiler $profiler;
/**
- * @param string $globalPrefix
+ * @param callable $globalPrefixClosure
* @param LoggerInterface $logger
* @param ?class-string<ICache> $localCacheClass
* @param ?class-string<ICache> $distributedCacheClass
* @param ?class-string<IMemcache> $lockingCacheClass
* @param string $logFile
*/
- public function __construct(string $globalPrefix, LoggerInterface $logger, IProfiler $profiler,
- ?string $localCacheClass = null, ?string $distributedCacheClass = null, ?string $lockingCacheClass = null, string $logFile = '') {
+ public function __construct(
+ private $globalPrefixClosure,
+ LoggerInterface $logger,
+ IProfiler $profiler,
+ ?string $localCacheClass = null,
+ ?string $distributedCacheClass = null,
+ ?string $lockingCacheClass = null,
+ string $logFile = ''
+ ) {
$this->logFile = $logFile;
- $this->globalPrefix = $globalPrefix;
if (!$localCacheClass) {
$localCacheClass = self::NULL_CACHE;
@@ -59,6 +65,7 @@ class Factory implements ICacheFactory {
if (!$distributedCacheClass) {
$distributedCacheClass = $localCacheClass;
}
+
$distributedCacheClass = ltrim($distributedCacheClass, '\\');
$missingCacheMessage = 'Memcache {class} not available for {use} cache';
@@ -85,6 +92,13 @@ class Factory implements ICacheFactory {
$this->profiler = $profiler;
}
+ private function getGlobalPrefix(): ?string {
+ if (is_null($this->globalPrefix)) {
+ $this->globalPrefix = ($this->globalPrefixClosure)();
+ }
+ return $this->globalPrefix;
+ }
+
/**
* create a cache instance for storing locks
*
@@ -92,8 +106,13 @@ class Factory implements ICacheFactory {
* @return IMemcache
*/
public function createLocking(string $prefix = ''): IMemcache {
+ $globalPrefix = $this->getGlobalPrefix();
+ if (is_null($globalPrefix)) {
+ return new ArrayCache($prefix);
+ }
+
assert($this->lockingCacheClass !== null);
- $cache = new $this->lockingCacheClass($this->globalPrefix . '/' . $prefix);
+ $cache = new $this->lockingCacheClass($globalPrefix . '/' . $prefix);
if ($this->lockingCacheClass === Redis::class && $this->profiler->isEnabled()) {
// We only support the profiler with Redis
$cache = new ProfilerWrapperCache($cache, 'Locking');
@@ -114,8 +133,13 @@ class Factory implements ICacheFactory {
* @return ICache
*/
public function createDistributed(string $prefix = ''): ICache {
+ $globalPrefix = $this->getGlobalPrefix();
+ if (is_null($globalPrefix)) {
+ return new ArrayCache($prefix);
+ }
+
assert($this->distributedCacheClass !== null);
- $cache = new $this->distributedCacheClass($this->globalPrefix . '/' . $prefix);
+ $cache = new $this->distributedCacheClass($globalPrefix . '/' . $prefix);
if ($this->distributedCacheClass === Redis::class && $this->profiler->isEnabled()) {
// We only support the profiler with Redis
$cache = new ProfilerWrapperCache($cache, 'Distributed');
@@ -136,8 +160,13 @@ class Factory implements ICacheFactory {
* @return ICache
*/
public function createLocal(string $prefix = ''): ICache {
+ $globalPrefix = $this->getGlobalPrefix();
+ if (is_null($globalPrefix)) {
+ return new ArrayCache($prefix);
+ }
+
assert($this->localCacheClass !== null);
- $cache = new $this->localCacheClass($this->globalPrefix . '/' . $prefix);
+ $cache = new $this->localCacheClass($globalPrefix . '/' . $prefix);
if ($this->localCacheClass === Redis::class && $this->profiler->isEnabled()) {
// We only support the profiler with Redis
$cache = new ProfilerWrapperCache($cache, 'Local');
diff --git a/lib/private/Memcache/LoggerWrapperCache.php b/lib/private/Memcache/LoggerWrapperCache.php
index 11497e2a5d8..c2a06731910 100644
--- a/lib/private/Memcache/LoggerWrapperCache.php
+++ b/lib/private/Memcache/LoggerWrapperCache.php
@@ -150,6 +150,17 @@ class LoggerWrapperCache extends Cache implements IMemcacheTTL {
}
/** @inheritDoc */
+ public function ncad(string $key, mixed $old): bool {
+ file_put_contents(
+ $this->logFile,
+ $this->getNameSpace() . '::ncad::' . $key . "\n",
+ FILE_APPEND
+ );
+
+ return $this->wrappedCache->cad($key, $old);
+ }
+
+ /** @inheritDoc */
public function setTTL(string $key, int $ttl) {
$this->wrappedCache->setTTL($key, $ttl);
}
diff --git a/lib/private/Memcache/NullCache.php b/lib/private/Memcache/NullCache.php
index ab5c491913a..b667869bf0d 100644
--- a/lib/private/Memcache/NullCache.php
+++ b/lib/private/Memcache/NullCache.php
@@ -43,6 +43,11 @@ class NullCache extends Cache implements \OCP\IMemcache {
return true;
}
+ public function ncad(string $key, mixed $old): bool {
+ return true;
+ }
+
+
public function clear($prefix = '') {
return true;
}
diff --git a/lib/private/Memcache/ProfilerWrapperCache.php b/lib/private/Memcache/ProfilerWrapperCache.php
index 84e3d880a0c..97d9d828a32 100644
--- a/lib/private/Memcache/ProfilerWrapperCache.php
+++ b/lib/private/Memcache/ProfilerWrapperCache.php
@@ -18,7 +18,7 @@ use OCP\IMemcacheTTL;
* @template-implements \ArrayAccess<string,mixed>
*/
class ProfilerWrapperCache extends AbstractDataCollector implements IMemcacheTTL, \ArrayAccess {
- /** @var Redis $wrappedCache*/
+ /** @var Redis $wrappedCache */
protected $wrappedCache;
/** @var string $prefix */
@@ -167,6 +167,18 @@ class ProfilerWrapperCache extends AbstractDataCollector implements IMemcacheTTL
}
/** @inheritDoc */
+ public function ncad(string $key, mixed $old): bool {
+ $start = microtime(true);
+ $ret = $this->wrappedCache->ncad($key, $old);
+ $this->data['queries'][] = [
+ 'start' => $start,
+ 'end' => microtime(true),
+ 'op' => $this->getPrefix() . '::ncad::' . $key,
+ ];
+ return $ret;
+ }
+
+ /** @inheritDoc */
public function setTTL(string $key, int $ttl) {
$this->wrappedCache->setTTL($key, $ttl);
}
diff --git a/lib/private/Memcache/Redis.php b/lib/private/Memcache/Redis.php
index 87dc86ab10d..711531e0ac2 100644
--- a/lib/private/Memcache/Redis.php
+++ b/lib/private/Memcache/Redis.php
@@ -23,6 +23,10 @@ class Redis extends Cache implements IMemcacheTTL {
'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end',
'cf0e94b2e9ffc7e04395cf88f7583fc309985910',
],
+ 'ncad' => [
+ 'if redis.call("get", KEYS[1]) ~= ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end',
+ '75526f8048b13ce94a41b58eee59c664b4990ab2',
+ ],
'caSetTtl' => [
'if redis.call("get", KEYS[1]) == ARGV[1] then redis.call("expire", KEYS[1], ARGV[2]) return 1 else return 0 end',
'fa4acbc946d23ef41d7d3910880b60e6e4972d72',
@@ -164,6 +168,12 @@ class Redis extends Cache implements IMemcacheTTL {
return $this->evalLua('cad', [$key], [$old]) > 0;
}
+ public function ncad(string $key, mixed $old): bool {
+ $old = self::encodeValue($old);
+
+ return $this->evalLua('ncad', [$key], [$old]) > 0;
+ }
+
public function setTTL($key, $ttl) {
if ($ttl === 0) {
// having infinite TTL can lead to leaked keys as the prefix changes with version upgrades
@@ -202,10 +212,10 @@ class Redis extends Cache implements IMemcacheTTL {
}
protected static function encodeValue(mixed $value): string {
- return is_int($value) ? (string) $value : json_encode($value);
+ return is_int($value) ? (string)$value : json_encode($value);
}
protected static function decodeValue(string $value): mixed {
- return is_numeric($value) ? (int) $value : json_decode($value, true);
+ return is_numeric($value) ? (int)$value : json_decode($value, true);
}
}
diff --git a/lib/private/NaturalSort.php b/lib/private/NaturalSort.php
index 120e05a8eb2..9b097340b63 100644
--- a/lib/private/NaturalSort.php
+++ b/lib/private/NaturalSort.php
@@ -82,7 +82,7 @@ class NaturalSort {
* @param string $a first string to compare
* @param string $b second string to compare
* @return int -1 if $b comes before $a, 1 if $a comes before $b
- * or 0 if the strings are identical
+ * or 0 if the strings are identical
*/
public function compare($a, $b) {
// Needed because PHP doesn't sort correctly when numbers are enclosed in
diff --git a/lib/private/Notification/Manager.php b/lib/private/Notification/Manager.php
index 7d042e6f8d8..8edbca0380d 100644
--- a/lib/private/Notification/Manager.php
+++ b/lib/private/Notification/Manager.php
@@ -68,7 +68,7 @@ class Manager implements IManager {
}
/**
* @param string $appClass The service must implement IApp, otherwise a
- * \InvalidArgumentException is thrown later
+ * \InvalidArgumentException is thrown later
* @since 17.0.0
*/
public function registerApp(string $appClass): void {
@@ -78,8 +78,8 @@ class Manager implements IManager {
/**
* @param \Closure $service The service must implement INotifier, otherwise a
* \InvalidArgumentException is thrown later
- * @param \Closure $info An array with the keys 'id' and 'name' containing
- * the app id and the app name
+ * @param \Closure $info An array with the keys 'id' and 'name' containing
+ * the app id and the app name
* @deprecated 17.0.0 use registerNotifierService instead.
* @since 8.2.0 - Parameter $info was added in 9.0.0
*/
@@ -93,7 +93,7 @@ class Manager implements IManager {
/**
* @param string $notifierService The service must implement INotifier, otherwise a
- * \InvalidArgumentException is thrown later
+ * \InvalidArgumentException is thrown later
* @since 17.0.0
*/
public function registerNotifierService(string $notifierService): void {
diff --git a/lib/private/OCM/Model/OCMProvider.php b/lib/private/OCM/Model/OCMProvider.php
index 17a356428f7..9bda95ebc17 100644
--- a/lib/private/OCM/Model/OCMProvider.php
+++ b/lib/private/OCM/Model/OCMProvider.php
@@ -163,8 +163,8 @@ class OCMProvider implements IOCMProvider {
*/
public function import(array $data): static {
$this->setEnabled(is_bool($data['enabled'] ?? '') ? $data['enabled'] : false)
- ->setApiVersion((string)($data['apiVersion'] ?? ''))
- ->setEndPoint($data['endPoint'] ?? '');
+ ->setApiVersion((string)($data['apiVersion'] ?? ''))
+ ->setEndPoint($data['endPoint'] ?? '');
$resources = [];
foreach (($data['resourceTypes'] ?? []) as $resourceData) {
diff --git a/lib/private/OCM/Model/OCMResource.php b/lib/private/OCM/Model/OCMResource.php
index c69763ca4ba..68f9ee18f79 100644
--- a/lib/private/OCM/Model/OCMResource.php
+++ b/lib/private/OCM/Model/OCMResource.php
@@ -85,8 +85,8 @@ class OCMResource implements IOCMResource {
*/
public function import(array $data): static {
return $this->setName((string)($data['name'] ?? ''))
- ->setShareTypes($data['shareTypes'] ?? [])
- ->setProtocols($data['protocols'] ?? []);
+ ->setShareTypes($data['shareTypes'] ?? [])
+ ->setProtocols($data['protocols'] ?? []);
}
/**
diff --git a/lib/private/Preview/BackgroundCleanupJob.php b/lib/private/Preview/BackgroundCleanupJob.php
index deadcd007b1..acf7bf22f52 100644
--- a/lib/private/Preview/BackgroundCleanupJob.php
+++ b/lib/private/Preview/BackgroundCleanupJob.php
@@ -16,6 +16,7 @@ use OCP\Files\IMimeTypeLoader;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IDBConnection;
+use function Symfony\Component\Translation\t;
class BackgroundCleanupJob extends TimedJob {
/** @var IDBConnection */
@@ -64,6 +65,11 @@ class BackgroundCleanupJob extends TimedJob {
}
private function getOldPreviewLocations(): \Iterator {
+ if ($this->connection->getShardDefinition('filecache')) {
+ // sharding is new enough that we don't need to support this
+ return;
+ }
+
$qb = $this->connection->getQueryBuilder();
$qb->select('a.name')
->from('filecache', 'a')
@@ -106,6 +112,15 @@ class BackgroundCleanupJob extends TimedJob {
return [];
}
+ if ($this->connection->getShardDefinition('filecache')) {
+ $chunks = $this->getAllPreviewIds($data['path'], 1000);
+ foreach ($chunks as $chunk) {
+ yield from $this->findMissingSources($chunk);
+ }
+
+ return;
+ }
+
/*
* This lovely like is the result of the way the new previews are stored
* We take the md5 of the name (fileid) and split the first 7 chars. That way
@@ -155,4 +170,46 @@ class BackgroundCleanupJob extends TimedJob {
$cursor->closeCursor();
}
+
+ private function getAllPreviewIds(string $previewRoot, int $chunkSize): \Iterator {
+ // See `getNewPreviewLocations` for some more info about the logic here
+ $like = $this->connection->escapeLikeParameter($previewRoot). '/_/_/_/_/_/_/_/%';
+
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('name', 'fileid')
+ ->from('filecache')
+ ->where(
+ $qb->expr()->andX(
+ $qb->expr()->eq('storage', $qb->createNamedParameter($this->previewFolder->getStorageId())),
+ $qb->expr()->like('path', $qb->createNamedParameter($like)),
+ $qb->expr()->eq('mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('httpd/unix-directory'))),
+ $qb->expr()->gt('fileid', $qb->createParameter('min_id')),
+ )
+ )
+ ->orderBy('fileid', 'ASC')
+ ->setMaxResults($chunkSize);
+
+ $minId = 0;
+ while (true) {
+ $qb->setParameter('min_id', $minId);
+ $rows = $qb->executeQuery()->fetchAll();
+ if (count($rows) > 0) {
+ $minId = $rows[count($rows) - 1]['fileid'];
+ yield array_map(function ($row) {
+ return (int)$row['name'];
+ }, $rows);
+ } else {
+ break;
+ }
+ }
+ }
+
+ private function findMissingSources(array $ids): array {
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('fileid')
+ ->from('filecache')
+ ->where($qb->expr()->in('fileid', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)));
+ $found = $qb->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
+ return array_diff($ids, $found);
+ }
}
diff --git a/lib/private/Preview/Bitmap.php b/lib/private/Preview/Bitmap.php
index c9fad50841f..a3d5fbfd4ec 100644
--- a/lib/private/Preview/Bitmap.php
+++ b/lib/private/Preview/Bitmap.php
@@ -61,7 +61,7 @@ abstract class Bitmap extends ProviderV2 {
//new bitmap image object
$image = new \OCP\Image();
- $image->loadFromData((string) $bp);
+ $image->loadFromData((string)$bp);
//check if image object is valid
return $image->valid() ? $image : null;
}
diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php
index 4083b9f4f61..460637c9a99 100644
--- a/lib/private/Preview/Generator.php
+++ b/lib/private/Preview/Generator.php
@@ -171,7 +171,7 @@ class Generator {
$previewFiles[] = $preview;
}
} catch (\InvalidArgumentException $e) {
- throw new NotFoundException("", 0, $e);
+ throw new NotFoundException('', 0, $e);
}
if ($preview->getSize() === 0) {
@@ -272,13 +272,13 @@ class Generator {
$hardwareConcurrency = self::getHardwareConcurrency();
switch ($type) {
- case "preview_concurrency_all":
+ case 'preview_concurrency_all':
$fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8;
$concurrency_all = $this->config->getSystemValueInt($type, $fallback);
- $concurrency_new = $this->getNumConcurrentPreviews("preview_concurrency_new");
+ $concurrency_new = $this->getNumConcurrentPreviews('preview_concurrency_new');
$cached[$type] = max($concurrency_all, $concurrency_new);
break;
- case "preview_concurrency_new":
+ case 'preview_concurrency_new':
$fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4;
$cached[$type] = $this->config->getSystemValueInt($type, $fallback);
break;
diff --git a/lib/private/Preview/HEIC.php b/lib/private/Preview/HEIC.php
index d198f11fdef..e5d73c943a4 100644
--- a/lib/private/Preview/HEIC.php
+++ b/lib/private/Preview/HEIC.php
@@ -31,7 +31,7 @@ class HEIC extends ProviderV2 {
* {@inheritDoc}
*/
public function isAvailable(FileInfo $file): bool {
- return in_array('HEIC', \Imagick::queryFormats("HEI*"));
+ return in_array('HEIC', \Imagick::queryFormats('HEI*'));
}
/**
@@ -70,7 +70,7 @@ class HEIC extends ProviderV2 {
//new bitmap image object
$image = new \OCP\Image();
- $image->loadFromData((string) $bp);
+ $image->loadFromData((string)$bp);
//check if image object is valid
return $image->valid() ? $image : null;
}
diff --git a/lib/private/Preview/MarkDown.php b/lib/private/Preview/MarkDown.php
index 41a79455042..c20433a1ac0 100644
--- a/lib/private/Preview/MarkDown.php
+++ b/lib/private/Preview/MarkDown.php
@@ -52,7 +52,7 @@ class MarkDown extends TXT {
$lines = preg_split("/\r\n|\n|\r/", $content);
// Define text size of text file preview
- $fontSize = $maxX ? (int) ((1 / ($maxX >= 512 ? 60 : 40) * $maxX)) : 10;
+ $fontSize = $maxX ? (int)((1 / ($maxX >= 512 ? 60 : 40) * $maxX)) : 10;
$image = imagecreate($maxX, $maxY);
imagecolorallocate($image, 255, 255, 255);
diff --git a/lib/private/Preview/Movie.php b/lib/private/Preview/Movie.php
index cfc05b8cce9..4a6104930d6 100644
--- a/lib/private/Preview/Movie.php
+++ b/lib/private/Preview/Movie.php
@@ -120,7 +120,7 @@ class Movie extends ProviderV2 {
$proc = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
$returnCode = -1;
- $output = "";
+ $output = '';
if (is_resource($proc)) {
$stdout = trim(stream_get_contents($pipes[1]));
$stderr = trim(stream_get_contents($pipes[2]));
diff --git a/lib/private/Preview/SVG.php b/lib/private/Preview/SVG.php
index 73dc0488bf1..d9f7701f411 100644
--- a/lib/private/Preview/SVG.php
+++ b/lib/private/Preview/SVG.php
@@ -58,7 +58,7 @@ class SVG extends ProviderV2 {
//new image object
$image = new \OCP\Image();
- $image->loadFromData((string) $svg);
+ $image->loadFromData((string)$svg);
//check if image object is valid
if ($image->valid()) {
$image->scaleDownToFit($maxX, $maxY);
diff --git a/lib/private/Preview/TXT.php b/lib/private/Preview/TXT.php
index 68597f8dbd0..1a1d64f3e08 100644
--- a/lib/private/Preview/TXT.php
+++ b/lib/private/Preview/TXT.php
@@ -50,7 +50,7 @@ class TXT extends ProviderV2 {
$lines = preg_split("/\r\n|\n|\r/", $content);
// Define text size of text file preview
- $fontSize = $maxX ? (int) ((1 / 32) * $maxX) : 5; //5px
+ $fontSize = $maxX ? (int)((1 / 32) * $maxX) : 5; //5px
$lineSize = ceil($fontSize * 1.5);
$image = imagecreate($maxX, $maxY);
@@ -67,7 +67,7 @@ class TXT extends ProviderV2 {
$index = $index + 1;
$x = 1;
- $y = (int) ($index * $lineSize);
+ $y = (int)($index * $lineSize);
if ($canUseTTF === true) {
imagettftext($image, $fontSize, 0, $x, $y, $textColor, $fontFile, $line);
diff --git a/lib/private/PreviewManager.php b/lib/private/PreviewManager.php
index f19ff25abb7..6f43687ceea 100644
--- a/lib/private/PreviewManager.php
+++ b/lib/private/PreviewManager.php
@@ -389,7 +389,7 @@ class PreviewManager implements IPreview {
if (is_string($movieBinary)) {
- $this->registerCoreProvider(Preview\Movie::class, '/video\/.*/', ["movieBinary" => $movieBinary]);
+ $this->registerCoreProvider(Preview\Movie::class, '/video\/.*/', ['movieBinary' => $movieBinary]);
}
}
}
diff --git a/lib/private/Profile/ProfileManager.php b/lib/private/Profile/ProfileManager.php
index e575740f970..5e36a9c2f56 100644
--- a/lib/private/Profile/ProfileManager.php
+++ b/lib/private/Profile/ProfileManager.php
@@ -93,7 +93,7 @@ class ProfileManager implements IProfileManager {
}
$account = $this->accountManager->getAccount($user);
- return (bool) filter_var(
+ return (bool)filter_var(
$account->getProperty(IAccountManager::PROPERTY_PROFILE_ENABLED)->getValue(),
FILTER_VALIDATE_BOOLEAN,
FILTER_NULL_ON_FAILURE,
diff --git a/lib/private/Profiler/FileProfilerStorage.php b/lib/private/Profiler/FileProfilerStorage.php
index 8cb6f2e6f79..b4494ef7a37 100644
--- a/lib/private/Profiler/FileProfilerStorage.php
+++ b/lib/private/Profiler/FileProfilerStorage.php
@@ -46,7 +46,7 @@ class FileProfilerStorage {
while (\count($result) < $limit && $line = $this->readLineFromFile($file)) {
$values = str_getcsv($line);
[$csvToken, $csvMethod, $csvUrl, $csvTime, $csvParent, $csvStatusCode] = $values;
- $csvTime = (int) $csvTime;
+ $csvTime = (int)$csvTime;
if ($url && !str_contains($csvUrl, $url) || $method && !str_contains($csvMethod, $method) || $statusCode && !str_contains($csvStatusCode, $statusCode)) {
continue;
diff --git a/lib/private/RedisFactory.php b/lib/private/RedisFactory.php
index b5779cbfbaa..dcb56cee9ef 100644
--- a/lib/private/RedisFactory.php
+++ b/lib/private/RedisFactory.php
@@ -13,7 +13,7 @@ class RedisFactory {
public const REDIS_MINIMAL_VERSION = '4.0.0';
public const REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION = '5.3.0';
- /** @var \Redis|\RedisCluster */
+ /** @var \Redis|\RedisCluster */
private $instance;
private SystemConfig $config;
diff --git a/lib/private/Repair.php b/lib/private/Repair.php
index d1904e08431..630ee249209 100644
--- a/lib/private/Repair.php
+++ b/lib/private/Repair.php
@@ -99,7 +99,7 @@ class Repair implements IOutput {
try {
$step->run($this);
} catch (\Exception $e) {
- $this->logger->error("Exception while executing repair step " . $step->getName(), ['exception' => $e]);
+ $this->logger->error('Exception while executing repair step ' . $step->getName(), ['exception' => $e]);
$this->dispatcher->dispatchTyped(new RepairErrorEvent($e->getMessage()));
}
}
diff --git a/lib/private/Repair/CleanTags.php b/lib/private/Repair/CleanTags.php
index f2fc8156f29..258808cb28a 100644
--- a/lib/private/Repair/CleanTags.php
+++ b/lib/private/Repair/CleanTags.php
@@ -107,7 +107,7 @@ class CleanTags implements IRepairStep {
$output,
'%d tags for delete files have been removed.',
'vcategory_to_object', 'objid',
- 'filecache', 'fileid', 'path_hash'
+ 'filecache', 'fileid', 'fileid'
);
}
@@ -147,8 +147,8 @@ class CleanTags implements IRepairStep {
* @param string $deleteId
* @param string $sourceTable
* @param string $sourceId
- * @param string $sourceNullColumn If this column is null in the source table,
- * the entry is deleted in the $deleteTable
+ * @param string $sourceNullColumn If this column is null in the source table,
+ * the entry is deleted in the $deleteTable
*/
protected function deleteOrphanEntries(IOutput $output, $repairInfo, $deleteTable, $deleteId, $sourceTable, $sourceId, $sourceNullColumn) {
$qb = $this->connection->getQueryBuilder();
@@ -166,19 +166,20 @@ class CleanTags implements IRepairStep {
$orphanItems = [];
while ($row = $result->fetch()) {
- $orphanItems[] = (int) $row[$deleteId];
+ $orphanItems[] = (int)$row[$deleteId];
}
+ $deleteQuery = $this->connection->getQueryBuilder();
+ $deleteQuery->delete($deleteTable)
+ ->where(
+ $deleteQuery->expr()->eq('type', $deleteQuery->expr()->literal('files'))
+ )
+ ->andWhere($deleteQuery->expr()->in($deleteId, $deleteQuery->createParameter('ids')));
if (!empty($orphanItems)) {
$orphanItemsBatch = array_chunk($orphanItems, 200);
foreach ($orphanItemsBatch as $items) {
- $qb->delete($deleteTable)
- ->where(
- $qb->expr()->eq('type', $qb->expr()->literal('files'))
- )
- ->andWhere($qb->expr()->in($deleteId, $qb->createParameter('ids')));
- $qb->setParameter('ids', $items, IQueryBuilder::PARAM_INT_ARRAY);
- $qb->execute();
+ $deleteQuery->setParameter('ids', $items, IQueryBuilder::PARAM_INT_ARRAY);
+ $deleteQuery->executeStatement();
}
}
diff --git a/lib/private/Repair/Collation.php b/lib/private/Repair/Collation.php
index a01a684151b..9557aabd718 100644
--- a/lib/private/Repair/Collation.php
+++ b/lib/private/Repair/Collation.php
@@ -15,7 +15,7 @@ use OCP\Migration\IRepairStep;
use Psr\Log\LoggerInterface;
class Collation implements IRepairStep {
- /** @var IConfig */
+ /** @var IConfig */
protected $config;
protected LoggerInterface $logger;
@@ -92,14 +92,14 @@ class Collation implements IRepairStep {
* @return string[]
*/
protected function getAllNonUTF8BinTables(IDBConnection $connection) {
- $dbName = $this->config->getSystemValueString("dbname");
+ $dbName = $this->config->getSystemValueString('dbname');
$characterSet = $this->config->getSystemValueBool('mysql.utf8mb4', false) ? 'utf8mb4' : 'utf8';
// fetch tables by columns
$statement = $connection->executeQuery(
- "SELECT DISTINCT(TABLE_NAME) AS `table`" .
- " FROM INFORMATION_SCHEMA . COLUMNS" .
- " WHERE TABLE_SCHEMA = ?" .
+ 'SELECT DISTINCT(TABLE_NAME) AS `table`' .
+ ' FROM INFORMATION_SCHEMA . COLUMNS' .
+ ' WHERE TABLE_SCHEMA = ?' .
" AND (COLLATION_NAME <> '" . $characterSet . "_bin' OR CHARACTER_SET_NAME <> '" . $characterSet . "')" .
" AND TABLE_NAME LIKE '*PREFIX*%'",
[$dbName]
@@ -112,9 +112,9 @@ class Collation implements IRepairStep {
// fetch tables by collation
$statement = $connection->executeQuery(
- "SELECT DISTINCT(TABLE_NAME) AS `table`" .
- " FROM INFORMATION_SCHEMA . TABLES" .
- " WHERE TABLE_SCHEMA = ?" .
+ 'SELECT DISTINCT(TABLE_NAME) AS `table`' .
+ ' FROM INFORMATION_SCHEMA . TABLES' .
+ ' WHERE TABLE_SCHEMA = ?' .
" AND TABLE_COLLATION <> '" . $characterSet . "_bin'" .
" AND TABLE_NAME LIKE '*PREFIX*%'",
[$dbName]
diff --git a/lib/private/Repair/NC25/AddMissingSecretJob.php b/lib/private/Repair/NC25/AddMissingSecretJob.php
index b407ef2a2a9..46b89d5f6f7 100644
--- a/lib/private/Repair/NC25/AddMissingSecretJob.php
+++ b/lib/private/Repair/NC25/AddMissingSecretJob.php
@@ -33,7 +33,7 @@ class AddMissingSecretJob implements IRepairStep {
try {
$this->config->setSystemValue('passwordsalt', $this->random->generate(30));
} catch (HintException $e) {
- $output->warning("passwordsalt is missing from your config.php and your config.php is read only. Please fix it manually.");
+ $output->warning('passwordsalt is missing from your config.php and your config.php is read only. Please fix it manually.');
}
}
@@ -42,7 +42,7 @@ class AddMissingSecretJob implements IRepairStep {
try {
$this->config->setSystemValue('secret', $this->random->generate(48));
} catch (HintException $e) {
- $output->warning("secret is missing from your config.php and your config.php is read only. Please fix it manually.");
+ $output->warning('secret is missing from your config.php and your config.php is read only. Please fix it manually.');
}
}
}
diff --git a/lib/private/Repair/OldGroupMembershipShares.php b/lib/private/Repair/OldGroupMembershipShares.php
index 54f2078395e..027f179596c 100644
--- a/lib/private/Repair/OldGroupMembershipShares.php
+++ b/lib/private/Repair/OldGroupMembershipShares.php
@@ -57,9 +57,9 @@ class OldGroupMembershipShares implements IRepairStep {
->from('share', 's1')
->where($query->expr()->isNotNull('s1.parent'))
// \OC\Share\Constant::$shareTypeGroupUserUnique === 2
- ->andWhere($query->expr()->eq('s1.share_type', $query->expr()->literal(2)))
- ->andWhere($query->expr()->isNotNull('s2.id'))
- ->andWhere($query->expr()->eq('s2.share_type', $query->expr()->literal(IShare::TYPE_GROUP)))
+ ->andWhere($query->expr()->eq('s1.share_type', $query->expr()->literal(2)))
+ ->andWhere($query->expr()->isNotNull('s2.id'))
+ ->andWhere($query->expr()->eq('s2.share_type', $query->expr()->literal(IShare::TYPE_GROUP)))
->leftJoin('s1', 'share', 's2', $query->expr()->eq('s1.parent', 's2.id'));
$deleteQuery = $this->connection->getQueryBuilder();
@@ -69,7 +69,7 @@ class OldGroupMembershipShares implements IRepairStep {
$result = $query->execute();
while ($row = $result->fetch()) {
if (!$this->isMember($row['group'], $row['user'])) {
- $deletedEntries += $deleteQuery->setParameter('share', (int) $row['id'])
+ $deletedEntries += $deleteQuery->setParameter('share', (int)$row['id'])
->execute();
}
}
diff --git a/lib/private/Repair/Owncloud/MigrateOauthTables.php b/lib/private/Repair/Owncloud/MigrateOauthTables.php
index e8728cd2f66..94ec0eba3e6 100644
--- a/lib/private/Repair/Owncloud/MigrateOauthTables.php
+++ b/lib/private/Repair/Owncloud/MigrateOauthTables.php
@@ -32,11 +32,11 @@ class MigrateOauthTables implements IRepairStep {
public function run(IOutput $output) {
$schema = new SchemaWrapper($this->db);
if (!$schema->hasTable('oauth2_clients')) {
- $output->info("oauth2_clients table does not exist.");
+ $output->info('oauth2_clients table does not exist.');
return;
}
- $output->info("Update the oauth2_access_tokens table schema.");
+ $output->info('Update the oauth2_access_tokens table schema.');
$schema = new SchemaWrapper($this->db);
$table = $schema->getTable('oauth2_access_tokens');
if (!$table->hasColumn('hashed_code')) {
@@ -58,7 +58,7 @@ class MigrateOauthTables implements IRepairStep {
$table->addIndex(['client_id'], 'oauth2_access_client_id_idx');
}
- $output->info("Update the oauth2_clients table schema.");
+ $output->info('Update the oauth2_clients table schema.');
$schema = new SchemaWrapper($this->db);
$table = $schema->getTable('oauth2_clients');
if ($table->getColumn('name')->getLength() !== 64) {
@@ -114,7 +114,7 @@ class MigrateOauthTables implements IRepairStep {
$result->closeCursor();
// 2. Insert them into the client_identifier column.
- foreach ($identifiers as ["id" => $id, "identifier" => $clientIdentifier]) {
+ foreach ($identifiers as ['id' => $id, 'identifier' => $clientIdentifier]) {
$insertQuery = $this->db->getQueryBuilder();
$insertQuery->update('oauth2_clients')
->set('client_identifier', $insertQuery->createNamedParameter($clientIdentifier, IQueryBuilder::PARAM_STR))
@@ -122,7 +122,7 @@ class MigrateOauthTables implements IRepairStep {
->executeStatement();
}
- $output->info("Drop the identifier column.");
+ $output->info('Drop the identifier column.');
$schema = new SchemaWrapper($this->db);
$table = $schema->getTable('oauth2_clients');
$table->dropColumn('identifier');
diff --git a/lib/private/Repair/Owncloud/SaveAccountsTableData.php b/lib/private/Repair/Owncloud/SaveAccountsTableData.php
index 1b6da7c858f..08665687b29 100644
--- a/lib/private/Repair/Owncloud/SaveAccountsTableData.php
+++ b/lib/private/Repair/Owncloud/SaveAccountsTableData.php
@@ -150,7 +150,7 @@ class SaveAccountsTableData implements IRepairStep {
* @throws \UnexpectedValueException
*/
protected function migrateUserInfo(IQueryBuilder $update, $userdata) {
- $state = (int) $userdata['state'];
+ $state = (int)$userdata['state'];
if ($state === 3) {
// Deleted user, ignore
return;
diff --git a/lib/private/Repair/RemoveLinkShares.php b/lib/private/Repair/RemoveLinkShares.php
index f128b6f731b..634494acb2f 100644
--- a/lib/private/Repair/RemoveLinkShares.php
+++ b/lib/private/Repair/RemoveLinkShares.php
@@ -111,7 +111,7 @@ class RemoveLinkShares implements IRepairStep {
$data = $result->fetch();
$result->closeCursor();
- return (int) $data['total'];
+ return (int)$data['total'];
}
/**
@@ -180,7 +180,7 @@ class RemoveLinkShares implements IRepairStep {
$users = array_keys($this->userToNotify);
foreach ($users as $user) {
- $notification->setUser((string) $user);
+ $notification->setUser((string)$user);
$this->notificationManager->notify($notification);
}
}
diff --git a/lib/private/Repair/RepairInvalidShares.php b/lib/private/Repair/RepairInvalidShares.php
index f28ae1c45fb..71e6359da5b 100644
--- a/lib/private/Repair/RepairInvalidShares.php
+++ b/lib/private/Repair/RepairInvalidShares.php
@@ -65,7 +65,7 @@ class RepairInvalidShares implements IRepairStep {
$query->select('s1.parent')
->from('share', 's1')
->where($query->expr()->isNotNull('s1.parent'))
- ->andWhere($query->expr()->isNull('s2.id'))
+ ->andWhere($query->expr()->isNull('s2.id'))
->leftJoin('s1', 'share', 's2', $query->expr()->eq('s1.parent', 's2.id'))
->groupBy('s1.parent')
->setMaxResults(self::CHUNK_SIZE);
@@ -80,7 +80,7 @@ class RepairInvalidShares implements IRepairStep {
$result = $query->execute();
while ($row = $result->fetch()) {
$deletedInLastChunk++;
- $deletedEntries += $deleteQuery->setParameter('parent', (int) $row['parent'])
+ $deletedEntries += $deleteQuery->setParameter('parent', (int)$row['parent'])
->execute();
}
$result->closeCursor();
diff --git a/lib/private/Repair/RepairLogoDimension.php b/lib/private/Repair/RepairLogoDimension.php
index 122da205986..854aeb3ab07 100644
--- a/lib/private/Repair/RepairLogoDimension.php
+++ b/lib/private/Repair/RepairLogoDimension.php
@@ -9,6 +9,8 @@ declare(strict_types=1);
namespace OC\Repair;
use OCA\Theming\ImageManager;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
use OCP\IConfig;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
@@ -44,9 +46,18 @@ class RepairLogoDimension implements IRepairStep {
return;
}
- $simpleFile = $imageManager->getImage('logo', false);
-
- $image = @imagecreatefromstring($simpleFile->getContent());
+ try {
+ try {
+ $simpleFile = $imageManager->getImage('logo', false);
+ $image = @imagecreatefromstring($simpleFile->getContent());
+ } catch (NotFoundException|NotPermittedException) {
+ $simpleFile = $imageManager->getImage('logo');
+ $image = false;
+ }
+ } catch (NotFoundException|NotPermittedException) {
+ $output->info('Theming is not used to provide a logo');
+ return;
+ }
$dimensions = '';
if ($image !== false) {
diff --git a/lib/private/Repair/RepairMimeTypes.php b/lib/private/Repair/RepairMimeTypes.php
index 2eece761c8d..6932299dc4a 100644
--- a/lib/private/Repair/RepairMimeTypes.php
+++ b/lib/private/Repair/RepairMimeTypes.php
@@ -58,6 +58,7 @@ class RepairMimeTypes implements IRepairStep {
$update = $this->connection->getQueryBuilder();
$update->update('filecache')
+ ->runAcrossAllShards()
->set('mimetype', $update->createParameter('mimetype'))
->where($update->expr()->neq('mimetype', $update->createParameter('mimetype'), IQueryBuilder::PARAM_INT))
->andWhere($update->expr()->neq('mimetype', $update->createParameter('folder'), IQueryBuilder::PARAM_INT))
@@ -226,10 +227,10 @@ class RepairMimeTypes implements IRepairStep {
*/
private function introduceFlatOpenDocumentType(): IResult|int|null {
$updatedMimetypes = [
- "fodt" => "application/vnd.oasis.opendocument.text-flat-xml",
- "fods" => "application/vnd.oasis.opendocument.spreadsheet-flat-xml",
- "fodg" => "application/vnd.oasis.opendocument.graphics-flat-xml",
- "fodp" => "application/vnd.oasis.opendocument.presentation-flat-xml",
+ 'fodt' => 'application/vnd.oasis.opendocument.text-flat-xml',
+ 'fods' => 'application/vnd.oasis.opendocument.spreadsheet-flat-xml',
+ 'fodg' => 'application/vnd.oasis.opendocument.graphics-flat-xml',
+ 'fodp' => 'application/vnd.oasis.opendocument.presentation-flat-xml',
];
return $this->updateMimetypes($updatedMimetypes);
@@ -251,8 +252,8 @@ class RepairMimeTypes implements IRepairStep {
*/
private function introduceOnlyofficeFormType(): IResult|int|null {
$updatedMimetypes = [
- "oform" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform",
- "docxf" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf",
+ 'oform' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform',
+ 'docxf' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf',
];
return $this->updateMimetypes($updatedMimetypes);
diff --git a/lib/private/Route/Route.php b/lib/private/Route/Route.php
index 2fef3b10806..ab5a1f6b59a 100644
--- a/lib/private/Route/Route.php
+++ b/lib/private/Route/Route.php
@@ -128,7 +128,7 @@ class Route extends SymfonyRoute implements IRoute {
*/
public function actionInclude($file) {
$function = function ($param) use ($file) {
- unset($param["_route"]);
+ unset($param['_route']);
$_GET = array_merge($_GET, $param);
unset($param);
require_once "$file";
diff --git a/lib/private/Route/Router.php b/lib/private/Route/Router.php
index b04b6a4d21c..646d1d4e6ed 100644
--- a/lib/private/Route/Router.php
+++ b/lib/private/Route/Router.php
@@ -111,9 +111,14 @@ class Router implements IRouter {
if ($this->loaded) {
return;
}
+ $this->eventLogger->start('route:load:' . $requestedApp, 'Loading Routes for ' . $requestedApp);
if (is_null($app)) {
$this->loaded = true;
$routingFiles = $this->getRoutingFiles();
+
+ foreach (\OC_App::getEnabledApps() as $enabledApp) {
+ $this->loadAttributeRoutes($enabledApp);
+ }
} else {
if (isset($this->loadedApps[$app])) {
return;
@@ -125,21 +130,9 @@ class Router implements IRouter {
} else {
$routingFiles = [];
}
- }
- $this->eventLogger->start('route:load:' . $requestedApp, 'Loading Routes for ' . $requestedApp);
-
- if ($requestedApp !== null && in_array($requestedApp, \OC_App::getEnabledApps())) {
- $routes = $this->getAttributeRoutes($requestedApp);
- if (count($routes) > 0) {
- $this->useCollection($requestedApp);
- $this->setupRoutes($routes, $requestedApp);
- $collection = $this->getCollection($requestedApp);
- $this->root->addCollection($collection);
- // Also add the OCS collection
- $collection = $this->getCollection($requestedApp . '.ocs');
- $collection->addPrefix('/ocsapp');
- $this->root->addCollection($collection);
+ if ($this->appManager->isEnabledForUser($app)) {
+ $this->loadAttributeRoutes($app);
}
}
@@ -413,6 +406,23 @@ class Router implements IRouter {
return $routeName;
}
+ private function loadAttributeRoutes(string $app): void {
+ $routes = $this->getAttributeRoutes($app);
+ if (count($routes) === 0) {
+ return;
+ }
+
+ $this->useCollection($app);
+ $this->setupRoutes($routes, $app);
+ $collection = $this->getCollection($app);
+ $this->root->addCollection($collection);
+
+ // Also add the OCS collection
+ $collection = $this->getCollection($app . '.ocs');
+ $collection->addPrefix('/ocsapp');
+ $this->root->addCollection($collection);
+ }
+
/**
* @throws ReflectionException
*/
diff --git a/lib/private/Search/FilterFactory.php b/lib/private/Search/FilterFactory.php
index 1317dd759af..1466042291d 100644
--- a/lib/private/Search/FilterFactory.php
+++ b/lib/private/Search/FilterFactory.php
@@ -27,7 +27,7 @@ final class FilterFactory {
FilterDefinition::TYPE_NC_USER => new Filter\UserFilter($filter, \OC::$server->get(IUserManager::class)),
FilterDefinition::TYPE_PERSON => self::getPerson($filter),
FilterDefinition::TYPE_STRING => new Filter\StringFilter($filter),
- FilterDefinition::TYPE_STRINGS => new Filter\StringsFilter(... (array) $filter),
+ FilterDefinition::TYPE_STRINGS => new Filter\StringsFilter(... (array)$filter),
default => throw new RuntimeException('Invalid filter type '. $type),
};
}
diff --git a/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php b/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php
index 0e272d94d0d..33c2a3aae62 100644
--- a/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php
+++ b/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php
@@ -45,7 +45,7 @@ class DatabaseBackend implements IBackend {
$row = $result->fetch();
$result->closeCursor();
- return (int) $row['attempts'];
+ return (int)$row['attempts'];
}
/**
diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php
index 7e310035ce4..596fcf408fa 100644
--- a/lib/private/Security/Bruteforce/Throttler.php
+++ b/lib/private/Security/Bruteforce/Throttler.php
@@ -176,7 +176,7 @@ class Throttler implements IThrottler {
return 0;
}
- $maxAgeTimestamp = (int) ($this->timeFactory->getTime() - 3600 * $maxAgeHours);
+ $maxAgeTimestamp = (int)($this->timeFactory->getTime() - 3600 * $maxAgeHours);
return $this->backend->getAttempts(
$ipAddress->getSubnet(),
@@ -204,7 +204,7 @@ class Throttler implements IThrottler {
if ($delay > self::MAX_DELAY) {
return self::MAX_DELAY_MS;
}
- return (int) \ceil($delay * 1000);
+ return (int)\ceil($delay * 1000);
}
/**
diff --git a/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php
index 0f637e5afd6..993f74ae0e4 100644
--- a/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php
+++ b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php
@@ -30,7 +30,11 @@ class ContentSecurityPolicyNonceManager {
public function getNonce(): string {
if ($this->nonce === '') {
if (empty($this->request->server['CSP_NONCE'])) {
- $this->nonce = base64_encode($this->csrfTokenManager->getToken()->getEncryptedValue());
+ // Get the token from the CSRF token, we only use the "shared secret" part
+ // as the first part does not add any security / entropy to the token
+ // so it can be ignored to keep the nonce short while keeping the same randomness
+ $csrfSecret = explode(':', ($this->csrfTokenManager->getToken()->getEncryptedValue()));
+ $this->nonce = end($csrfSecret);
} else {
$this->nonce = $this->request->server['CSP_NONCE'];
}
diff --git a/lib/private/Security/Ip/Range.php b/lib/private/Security/Ip/Range.php
index 4f960166d6a..39c03677f81 100644
--- a/lib/private/Security/Ip/Range.php
+++ b/lib/private/Security/Ip/Range.php
@@ -30,7 +30,7 @@ class Range implements IRange {
}
public function contains(IAddress $address): bool {
- return $this->range->contains(Factory::parseAddressString((string) $address));
+ return $this->range->contains(Factory::parseAddressString((string)$address));
}
public function __toString(): string {
diff --git a/lib/private/Security/Ip/RemoteAddress.php b/lib/private/Security/Ip/RemoteAddress.php
index 54cdb96132a..cef511099b1 100644
--- a/lib/private/Security/Ip/RemoteAddress.php
+++ b/lib/private/Security/Ip/RemoteAddress.php
@@ -66,6 +66,6 @@ class RemoteAddress implements IRemoteAddress, IAddress {
}
public function __toString(): string {
- return (string) $this->ip;
+ return (string)$this->ip;
}
}
diff --git a/lib/private/Security/SecureRandom.php b/lib/private/Security/SecureRandom.php
index 459d43475b7..b2a3d19ce74 100644
--- a/lib/private/Security/SecureRandom.php
+++ b/lib/private/Security/SecureRandom.php
@@ -24,7 +24,7 @@ class SecureRandom implements ISecureRandom {
* Generate a secure random string of specified length.
* @param int $length The length of the generated string
* @param string $characters An optional list of characters to use if no character list is
- * specified all valid base64 characters are used.
+ * specified all valid base64 characters are used.
* @throws \LengthException if an invalid length is requested
*/
public function generate(
diff --git a/lib/private/Server.php b/lib/private/Server.php
index a0072b43ee2..01d5bdac0b6 100644
--- a/lib/private/Server.php
+++ b/lib/private/Server.php
@@ -637,44 +637,51 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerService(Factory::class, function (Server $c) {
$profiler = $c->get(IProfiler::class);
- $arrayCacheFactory = new \OC\Memcache\Factory('', $c->get(LoggerInterface::class),
+ $arrayCacheFactory = new \OC\Memcache\Factory(fn () => '', $c->get(LoggerInterface::class),
$profiler,
ArrayCache::class,
ArrayCache::class,
ArrayCache::class
);
- /** @var \OCP\IConfig $config */
- $config = $c->get(\OCP\IConfig::class);
-
- if ($config->getSystemValueBool('installed', false) && !(defined('PHPUNIT_RUN') && PHPUNIT_RUN)) {
- if (!$config->getSystemValueBool('log_query')) {
- try {
- $v = \OC_App::getAppVersions();
- } catch (\Doctrine\DBAL\Exception $e) {
- // Database service probably unavailable
- // Probably related to https://github.com/nextcloud/server/issues/37424
- return $arrayCacheFactory;
+ /** @var SystemConfig $config */
+ $config = $c->get(SystemConfig::class);
+
+ if ($config->getValue('installed', false) && !(defined('PHPUNIT_RUN') && PHPUNIT_RUN)) {
+ $logQuery = $config->getValue('log_query');
+ $prefixClosure = function () use ($logQuery) {
+ if (!$logQuery) {
+ try {
+ $v = \OC_App::getAppVersions();
+ } catch (\Doctrine\DBAL\Exception $e) {
+ // Database service probably unavailable
+ // Probably related to https://github.com/nextcloud/server/issues/37424
+ return null;
+ }
+ } else {
+ // If the log_query is enabled, we can not get the app versions
+ // as that does a query, which will be logged and the logging
+ // depends on redis and here we are back again in the same function.
+ $v = [
+ 'log_query' => 'enabled',
+ ];
}
- } else {
- // If the log_query is enabled, we can not get the app versions
- // as that does a query, which will be logged and the logging
- // depends on redis and here we are back again in the same function.
- $v = [
- 'log_query' => 'enabled',
- ];
- }
- $v['core'] = implode(',', \OC_Util::getVersion());
- $version = implode(',', $v);
- $instanceId = \OC_Util::getInstanceId();
- $path = \OC::$SERVERROOT;
- $prefix = md5($instanceId . '-' . $version . '-' . $path);
- return new \OC\Memcache\Factory($prefix,
+ $v['core'] = implode(',', \OC_Util::getVersion());
+ $version = implode(',', $v);
+ $instanceId = \OC_Util::getInstanceId();
+ $path = \OC::$SERVERROOT;
+ return md5($instanceId . '-' . $version . '-' . $path);
+ };
+ return new \OC\Memcache\Factory($prefixClosure,
$c->get(LoggerInterface::class),
$profiler,
- $config->getSystemValue('memcache.local', null),
- $config->getSystemValue('memcache.distributed', null),
- $config->getSystemValue('memcache.locking', null),
- $config->getSystemValueString('redis_log_file')
+ /** @psalm-taint-escape callable */
+ $config->getValue('memcache.local', null),
+ /** @psalm-taint-escape callable */
+ $config->getValue('memcache.distributed', null),
+ /** @psalm-taint-escape callable */
+ $config->getValue('memcache.locking', null),
+ /** @psalm-taint-escape callable */
+ $config->getValue('redis_log_file')
);
}
return $arrayCacheFactory;
@@ -735,7 +742,7 @@ class Server extends ServerContainer implements IServerContainer {
$logger = $factory->get($logType);
$registry = $c->get(\OCP\Support\CrashReport\IRegistry::class);
- return new Log($logger, $this->get(SystemConfig::class), null, $registry);
+ return new Log($logger, $this->get(SystemConfig::class), crashReporters: $registry);
});
$this->registerAlias(ILogger::class, \OC\Log::class);
/** @deprecated 19.0.0 */
@@ -804,7 +811,7 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(IDBConnection::class, ConnectionAdapter::class);
$this->registerService(Connection::class, function (Server $c) {
$systemConfig = $c->get(SystemConfig::class);
- $factory = new \OC\DB\ConnectionFactory($systemConfig);
+ $factory = new \OC\DB\ConnectionFactory($systemConfig, $c->get(ICacheFactory::class));
$type = $systemConfig->getValue('dbtype', 'sqlite');
if (!$factory->isValidType($type)) {
throw new \OC\DatabaseException('Invalid database type');
diff --git a/lib/private/Session/Internal.php b/lib/private/Session/Internal.php
index 5398dc710af..b465bcd3eda 100644
--- a/lib/private/Session/Internal.php
+++ b/lib/private/Session/Internal.php
@@ -29,8 +29,10 @@ class Internal extends Session {
* @param string $name
* @throws \Exception
*/
- public function __construct(string $name,
- private LoggerInterface $logger) {
+ public function __construct(
+ string $name,
+ private ?LoggerInterface $logger,
+ ) {
set_error_handler([$this, 'trapError']);
$this->invoke('session_name', [$name]);
$this->invoke('session_cache_limiter', ['']);
@@ -204,7 +206,7 @@ class Internal extends Session {
$timeSpent > 0.5 => ILogger::INFO,
default => ILogger::DEBUG,
};
- $this->logger->log(
+ $this->logger?->log(
$logLevel,
"Slow session operation $functionName detected",
[
diff --git a/lib/private/Settings/Section.php b/lib/private/Settings/Section.php
index 4f8234254b1..9cc6523b9ae 100644
--- a/lib/private/Settings/Section.php
+++ b/lib/private/Settings/Section.php
@@ -32,7 +32,7 @@ class Section implements IIconSection {
/**
* @return string The ID of the section. It is supposed to be a lower case string,
- * e.g. 'ldap'
+ * e.g. 'ldap'
*/
public function getID() {
return $this->id;
@@ -40,7 +40,7 @@ class Section implements IIconSection {
/**
* @return string The translated name as it should be displayed, e.g. 'LDAP / AD
- * integration'. Use the L10N service to translate it.
+ * integration'. Use the L10N service to translate it.
*/
public function getName() {
return $this->name;
@@ -48,8 +48,8 @@ class Section implements IIconSection {
/**
* @return int whether the form should be rather on the top or bottom of
- * the settings navigation. The sections are arranged in ascending order of
- * the priority values. It is required to return a value between 0 and 99.
+ * the settings navigation. The sections are arranged in ascending order of
+ * the priority values. It is required to return a value between 0 and 99.
*
* E.g.: 70
*/
@@ -59,7 +59,7 @@ class Section implements IIconSection {
/**
* @return string The relative path to an 16*16 icon describing the section.
- * e.g. '/core/img/places/files.svg'
+ * e.g. '/core/img/places/files.svg'
*
* @since 12
*/
diff --git a/lib/private/Setup.php b/lib/private/Setup.php
index 62db4879bbc..6212a561abb 100644
--- a/lib/private/Setup.php
+++ b/lib/private/Setup.php
@@ -147,7 +147,7 @@ class Setup {
* a few system checks.
*
* @return array of system info, including an "errors" value
- * in case of errors/warnings
+ * in case of errors/warnings
*/
public function getSystemInfo(bool $allowAllDatabases = false): array {
$databases = $this->getSupportedDatabases($allowAllDatabases);
@@ -230,7 +230,7 @@ class Setup {
$error[] = $l->t('Set an admin password.');
}
if (empty($options['directory'])) {
- $options['directory'] = \OC::$SERVERROOT . "/data";
+ $options['directory'] = \OC::$SERVERROOT . '/data';
}
if (!isset(self::$dbSetupClasses[$dbType])) {
@@ -248,7 +248,7 @@ class Setup {
// validate the data directory
if ((!is_dir($dataDir) && !mkdir($dataDir)) || !is_writable($dataDir)) {
- $error[] = $l->t("Cannot create or write into the data directory %s", [$dataDir]);
+ $error[] = $l->t('Cannot create or write into the data directory %s', [$dataDir]);
}
if (!empty($error)) {
@@ -392,7 +392,7 @@ class Setup {
$userSession->login($username, $password);
$user = $userSession->getUser();
if (!$user) {
- $error[] = "No account found in session.";
+ $error[] = 'No account found in session.';
return $error;
}
$userSession->createSessionToken($request, $user->getUID(), $username, $password);
@@ -509,7 +509,7 @@ class Setup {
$df = disk_free_space(\OC::$SERVERROOT);
$size = strlen($content) + 10240;
if ($df !== false && $df < (float)$size) {
- throw new \Exception(\OC::$SERVERROOT . " does not have enough space for writing the htaccess file! Not writing it back!");
+ throw new \Exception(\OC::$SERVERROOT . ' does not have enough space for writing the htaccess file! Not writing it back!');
}
}
//suppress errors in case we don't have permissions for it
@@ -542,7 +542,7 @@ class Setup {
$content .= "# Section for Apache 2.2 to 2.6\n";
$content .= "<IfModule mod_autoindex.c>\n";
$content .= " IndexIgnore *\n";
- $content .= "</IfModule>";
+ $content .= '</IfModule>';
$baseDir = Server::get(IConfig::class)->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data');
file_put_contents($baseDir . '/.htaccess', $content);
diff --git a/lib/private/Setup/AbstractDatabase.php b/lib/private/Setup/AbstractDatabase.php
index e96ca8a2f3e..b1d93f55cc0 100644
--- a/lib/private/Setup/AbstractDatabase.php
+++ b/lib/private/Setup/AbstractDatabase.php
@@ -50,14 +50,14 @@ abstract class AbstractDatabase {
public function validate($config) {
$errors = [];
if (empty($config['dbuser']) && empty($config['dbname'])) {
- $errors[] = $this->trans->t("Enter the database Login and name for %s", [$this->dbprettyname]);
+ $errors[] = $this->trans->t('Enter the database Login and name for %s', [$this->dbprettyname]);
} elseif (empty($config['dbuser'])) {
- $errors[] = $this->trans->t("Enter the database Login for %s", [$this->dbprettyname]);
+ $errors[] = $this->trans->t('Enter the database Login for %s', [$this->dbprettyname]);
} elseif (empty($config['dbname'])) {
- $errors[] = $this->trans->t("Enter the database name for %s", [$this->dbprettyname]);
+ $errors[] = $this->trans->t('Enter the database name for %s', [$this->dbprettyname]);
}
if (substr_count($config['dbname'], '.') >= 1) {
- $errors[] = $this->trans->t("You cannot use dots in the database name %s", [$this->dbprettyname]);
+ $errors[] = $this->trans->t('You cannot use dots in the database name %s', [$this->dbprettyname]);
}
return $errors;
}
@@ -70,9 +70,9 @@ abstract class AbstractDatabase {
$dbPort = !empty($config['dbport']) ? $config['dbport'] : '';
$dbTablePrefix = $config['dbtableprefix'] ?? 'oc_';
- $createUserConfig = $this->config->getValue("setup_create_db_user", true);
+ $createUserConfig = $this->config->getValue('setup_create_db_user', true);
// accept `false` both as bool and string, since setting config values from env will result in a string
- $this->tryCreateDbUser = $createUserConfig !== false && $createUserConfig !== "false";
+ $this->tryCreateDbUser = $createUserConfig !== false && $createUserConfig !== 'false';
$this->config->setValues([
'dbname' => $dbName,
@@ -133,7 +133,7 @@ abstract class AbstractDatabase {
abstract public function setupDatabase($username);
public function runMigrations(?IOutput $output = null) {
- if (!is_dir(\OC::$SERVERROOT."/core/Migrations")) {
+ if (!is_dir(\OC::$SERVERROOT.'/core/Migrations')) {
return;
}
$ms = new MigrationService('core', \OC::$server->get(Connection::class), $output);
diff --git a/lib/private/Setup/OCI.php b/lib/private/Setup/OCI.php
index 3a0fa34d8d1..47e5e5436a5 100644
--- a/lib/private/Setup/OCI.php
+++ b/lib/private/Setup/OCI.php
@@ -31,11 +31,11 @@ class OCI extends AbstractDatabase {
public function validate($config) {
$errors = [];
if (empty($config['dbuser']) && empty($config['dbname'])) {
- $errors[] = $this->trans->t("Enter the database Login and name for %s", [$this->dbprettyname]);
+ $errors[] = $this->trans->t('Enter the database Login and name for %s', [$this->dbprettyname]);
} elseif (empty($config['dbuser'])) {
- $errors[] = $this->trans->t("Enter the database Login for %s", [$this->dbprettyname]);
+ $errors[] = $this->trans->t('Enter the database Login for %s', [$this->dbprettyname]);
} elseif (empty($config['dbname'])) {
- $errors[] = $this->trans->t("Enter the database name for %s", [$this->dbprettyname]);
+ $errors[] = $this->trans->t('Enter the database name for %s', [$this->dbprettyname]);
}
return $errors;
}
diff --git a/lib/private/Setup/PostgreSQL.php b/lib/private/Setup/PostgreSQL.php
index 4ece8957ce6..73f2dfe0623 100644
--- a/lib/private/Setup/PostgreSQL.php
+++ b/lib/private/Setup/PostgreSQL.php
@@ -51,16 +51,6 @@ class PostgreSQL extends AbstractDatabase {
$this->dbPassword = \OC::$server->get(ISecureRandom::class)->generate(30, ISecureRandom::CHAR_ALPHANUMERIC);
$this->createDBUser($connection);
-
- // Go to the main database and grant create on the public schema
- // The code below is implemented to make installing possible with PostgreSQL version 15:
- // https://www.postgresql.org/docs/release/15.0/
- // From the release notes: For new databases having no need to defend against insider threats, granting CREATE permission will yield the behavior of prior releases
- // Therefore we assume that the database is only used by one user/service which is Nextcloud
- // Additional services should get installed in a separate database in order to stay secure
- // Also see https://www.postgresql.org/docs/15/ddl-schemas.html#DDL-SCHEMAS-PATTERNS
- $connectionMainDatabase->executeQuery('GRANT CREATE ON SCHEMA public TO "' . addslashes($this->dbUser) . '"');
- $connectionMainDatabase->close();
}
}
@@ -73,6 +63,20 @@ class PostgreSQL extends AbstractDatabase {
$this->createDatabase($connection);
// the connection to dbname=postgres is not needed anymore
$connection->close();
+
+ if ($this->tryCreateDbUser) {
+ if ($canCreateRoles) {
+ // Go to the main database and grant create on the public schema
+ // The code below is implemented to make installing possible with PostgreSQL version 15:
+ // https://www.postgresql.org/docs/release/15.0/
+ // From the release notes: For new databases having no need to defend against insider threats, granting CREATE permission will yield the behavior of prior releases
+ // Therefore we assume that the database is only used by one user/service which is Nextcloud
+ // Additional services should get installed in a separate database in order to stay secure
+ // Also see https://www.postgresql.org/docs/15/ddl-schemas.html#DDL-SCHEMAS-PATTERNS
+ $connectionMainDatabase->executeQuery('GRANT CREATE ON SCHEMA public TO "' . addslashes($this->dbUser) . '"');
+ $connectionMainDatabase->close();
+ }
+ }
} catch (\Exception $e) {
$this->logger->warning('Error trying to connect as "postgres", assuming database is setup and tables need to be created', [
'exception' => $e,
@@ -101,7 +105,7 @@ class PostgreSQL extends AbstractDatabase {
private function createDatabase(Connection $connection) {
if (!$this->databaseExists($connection)) {
//The database does not exists... let's create it
- $query = $connection->prepare("CREATE DATABASE " . addslashes($this->dbName) . " OWNER \"" . addslashes($this->dbUser) . '"');
+ $query = $connection->prepare('CREATE DATABASE ' . addslashes($this->dbName) . ' OWNER "' . addslashes($this->dbUser) . '"');
try {
$query->execute();
} catch (DatabaseException $e) {
@@ -110,7 +114,7 @@ class PostgreSQL extends AbstractDatabase {
]);
}
} else {
- $query = $connection->prepare("REVOKE ALL PRIVILEGES ON DATABASE " . addslashes($this->dbName) . " FROM PUBLIC");
+ $query = $connection->prepare('REVOKE ALL PRIVILEGES ON DATABASE ' . addslashes($this->dbName) . ' FROM PUBLIC');
try {
$query->execute();
} catch (DatabaseException $e) {
@@ -151,7 +155,7 @@ class PostgreSQL extends AbstractDatabase {
}
// create the user
- $query = $connection->prepare("CREATE USER \"" . addslashes($this->dbUser) . "\" CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'");
+ $query = $connection->prepare('CREATE USER "' . addslashes($this->dbUser) . "\" CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'");
$query->execute();
if ($this->databaseExists($connection)) {
$query = $connection->prepare('GRANT CONNECT ON DATABASE ' . addslashes($this->dbName) . ' TO "' . addslashes($this->dbUser) . '"');
diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php
index 366b9cad976..3ea429dfe3d 100644
--- a/lib/private/Share20/DefaultShareProvider.php
+++ b/lib/private/Share20/DefaultShareProvider.php
@@ -28,6 +28,7 @@ use OCP\L10N\IFactory;
use OCP\Mail\IMailer;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IAttributes;
+use OCP\Share\IManager;
use OCP\Share\IShare;
use OCP\Share\IShareProviderSupportsAccept;
use OCP\Share\IShareProviderWithNotification;
@@ -54,6 +55,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
private IURLGenerator $urlGenerator,
private ITimeFactory $timeFactory,
private LoggerInterface $logger,
+ private IManager $shareManager,
) {
}
@@ -95,6 +97,8 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
if ($expirationDate !== null) {
$qb->setValue('expiration', $qb->createNamedParameter($expirationDate, 'datetime'));
}
+
+ $qb->setValue('reminder_sent', $qb->createNamedParameter($share->getReminderSent(), IQueryBuilder::PARAM_BOOL));
} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
//Set the GID of the group we share with
$qb->setValue('share_with', $qb->createNamedParameter($share->getSharedWith()));
@@ -221,6 +225,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
->set('expiration', $qb->createNamedParameter($expirationDate, IQueryBuilder::PARAM_DATE))
->set('note', $qb->createNamedParameter($share->getNote()))
->set('accepted', $qb->createNamedParameter($share->getStatus()))
+ ->set('reminder_sent', $qb->createNamedParameter($share->getReminderSent(), IQueryBuilder::PARAM_BOOL))
->execute();
} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
$qb = $this->dbConn->getQueryBuilder();
@@ -607,7 +612,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = true) {
if (!$shallow) {
- throw new \Exception("non-shallow getSharesInFolder is no longer supported");
+ throw new \Exception('non-shallow getSharesInFolder is no longer supported');
}
$qb = $this->dbConn->getQueryBuilder();
@@ -670,6 +675,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
foreach ($chunks as $chunk) {
$qb->setParameter('chunk', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
+ $a = $qb->getSQL();
$cursor = $qb->executeQuery();
while ($data = $cursor->fetch()) {
$shares[$data['fileid']][] = $this->createShare($data);
@@ -771,7 +777,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
// If the recipient is set for a group share resolve to that user
if ($recipientId !== null && $share->getShareType() === IShare::TYPE_GROUP) {
- $share = $this->resolveGroupShares([(int) $share->getId() => $share], $recipientId)[0];
+ $share = $this->resolveGroupShares([(int)$share->getId() => $share], $recipientId)[0];
}
return $share;
@@ -880,9 +886,9 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
while ($data = $cursor->fetch()) {
if ($data['fileid'] && $data['path'] === null) {
- $data['path'] = (string) $data['path'];
- $data['name'] = (string) $data['name'];
- $data['checksum'] = (string) $data['checksum'];
+ $data['path'] = (string)$data['path'];
+ $data['name'] = (string)$data['name'];
+ $data['checksum'] = (string)$data['checksum'];
}
if ($this->isAccessibleResult($data)) {
$shares[] = $this->createShare($data);
@@ -1003,7 +1009,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
}
/**
- * Create a share object from an database row
+ * Create a share object from a database row
*
* @param mixed[] $data
* @return \OCP\Share\IShare
@@ -1065,6 +1071,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
$share->setProviderId($this->identifier());
$share->setHideDownload((int)$data['hide_download'] === 1);
+ $share->setReminderSent((bool)$data['reminder_sent']);
return $share;
}
@@ -1200,10 +1207,14 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
if (!empty($ids)) {
$chunks = array_chunk($ids, 100);
+
+ $qb = $this->dbConn->getQueryBuilder();
+ $qb->delete('share')
+ ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP)))
+ ->andWhere($qb->expr()->in('parent', $qb->createParameter('parents')));
+
foreach ($chunks as $chunk) {
- $qb->delete('share')
- ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP)))
- ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)));
+ $qb->setParameter('parents', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
$qb->execute();
}
}
@@ -1223,6 +1234,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
*
* @param string $uid
* @param string $gid
+ * @return void
*/
public function userDeletedFromGroup($uid, $gid) {
/*
@@ -1234,7 +1246,7 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP)))
->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($gid)));
- $cursor = $qb->execute();
+ $cursor = $qb->executeQuery();
$ids = [];
while ($row = $cursor->fetch()) {
$ids[] = (int)$row['id'];
@@ -1243,15 +1255,58 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
if (!empty($ids)) {
$chunks = array_chunk($ids, 100);
+
+ /*
+ * Delete all special shares with this user for the found group shares
+ */
+ $qb = $this->dbConn->getQueryBuilder();
+ $qb->delete('share')
+ ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP)))
+ ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($uid)))
+ ->andWhere($qb->expr()->in('parent', $qb->createParameter('parents')));
+
foreach ($chunks as $chunk) {
- /*
- * Delete all special shares with this users for the found group shares
- */
- $qb->delete('share')
- ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP)))
- ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($uid)))
- ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)));
- $qb->execute();
+ $qb->setParameter('parents', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
+ $qb->executeStatement();
+ }
+ }
+
+ if ($this->shareManager->shareWithGroupMembersOnly()) {
+ $user = $this->userManager->get($uid);
+ if ($user === null) {
+ return;
+ }
+ $userGroups = $this->groupManager->getUserGroupIds($user);
+ $userGroups = array_diff($userGroups, $this->shareManager->shareWithGroupMembersOnlyExcludeGroupsList());
+
+ // Delete user shares received by the user from users in the group.
+ $userReceivedShares = $this->shareManager->getSharedWith($uid, IShare::TYPE_USER, null, -1);
+ foreach ($userReceivedShares as $share) {
+ $owner = $this->userManager->get($share->getSharedBy());
+ if ($owner === null) {
+ continue;
+ }
+ $ownerGroups = $this->groupManager->getUserGroupIds($owner);
+ $mutualGroups = array_intersect($userGroups, $ownerGroups);
+
+ if (count($mutualGroups) === 0) {
+ $this->shareManager->deleteShare($share);
+ }
+ }
+
+ // Delete user shares from the user to users in the group.
+ $userEmittedShares = $this->shareManager->getSharesBy($uid, IShare::TYPE_USER, null, true, -1);
+ foreach ($userEmittedShares as $share) {
+ $recipient = $this->userManager->get($share->getSharedWith());
+ if ($recipient === null) {
+ continue;
+ }
+ $recipientGroups = $this->groupManager->getUserGroupIds($recipient);
+ $mutualGroups = array_intersect($userGroups, $recipientGroups);
+
+ if (count($mutualGroups) === 0) {
+ $this->shareManager->deleteShare($share);
+ }
}
}
}
@@ -1339,8 +1394,8 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
protected function filterSharesOfUser(array $shares) {
// Group shares when the user has a share exception
foreach ($shares as $id => $share) {
- $type = (int) $share['share_type'];
- $permissions = (int) $share['permissions'];
+ $type = (int)$share['share_type'];
+ $permissions = (int)$share['permissions'];
if ($type === IShare::TYPE_USERGROUP) {
unset($shares[$share['parent']]);
diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php
index e76f4586dfd..3e085e08d7d 100644
--- a/lib/private/Share20/Manager.php
+++ b/lib/private/Share20/Manager.php
@@ -11,6 +11,7 @@ use OC\Files\Mount\MoveableMount;
use OC\KnownUser\KnownUserService;
use OC\Share20\Exception\ProviderException;
use OCA\Files_Sharing\AppInfo\Application;
+use OCA\Files_Sharing\SharedStorage;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
use OCP\Files\Folder;
@@ -570,7 +571,7 @@ class Manager implements IManager {
// No sense in checking if the method is not there.
if (method_exists($share, 'setParent')) {
$storage = $share->getNode()->getStorage();
- if ($storage->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) {
+ if ($storage->instanceOfStorage(SharedStorage::class)) {
/** @var \OCA\Files_Sharing\SharedStorage $storage */
$share->setParent($storage->getShareId());
}
@@ -931,7 +932,7 @@ class Manager implements IManager {
*
* @param IShare $share the share to update its password.
* @param IShare $originalShare the original share to compare its
- * password with.
+ * password with.
* @return boolean whether the password was updated or not.
*/
private function updateSharePasswordIfNeeded(IShare $share, IShare $originalShare) {
@@ -1101,7 +1102,7 @@ class Manager implements IManager {
public function getSharesInFolder($userId, Folder $node, $reshares = false, $shallow = true) {
$providers = $this->factory->getAllProviders();
if (!$shallow) {
- throw new \Exception("non-shallow getSharesInFolder is no longer supported");
+ throw new \Exception('non-shallow getSharesInFolder is no longer supported');
}
return array_reduce($providers, function ($shares, IShareProvider $provider) use ($userId, $node, $reshares) {
diff --git a/lib/private/Share20/ProviderFactory.php b/lib/private/Share20/ProviderFactory.php
index dbe251a49df..e1a2c9a5375 100644
--- a/lib/private/Share20/ProviderFactory.php
+++ b/lib/private/Share20/ProviderFactory.php
@@ -44,9 +44,9 @@ class ProviderFactory implements IProviderFactory {
private $defaultProvider = null;
/** @var FederatedShareProvider */
private $federatedProvider = null;
- /** @var ShareByMailProvider */
+ /** @var ShareByMailProvider */
private $shareByMailProvider;
- /** @var \OCA\Circles\ShareByCircleProvider */
+ /** @var \OCA\Circles\ShareByCircleProvider */
private $shareByCircleProvider = null;
/** @var bool */
private $circlesAreNotAvailable = false;
@@ -88,6 +88,7 @@ class ProviderFactory implements IProviderFactory {
$this->serverContainer->getURLGenerator(),
$this->serverContainer->query(ITimeFactory::class),
$this->serverContainer->get(LoggerInterface::class),
+ $this->serverContainer->get(IManager::class),
);
}
diff --git a/lib/private/Share20/Share.php b/lib/private/Share20/Share.php
index ac95e3ac0d4..1c0b21b8038 100644
--- a/lib/private/Share20/Share.php
+++ b/lib/private/Share20/Share.php
@@ -72,6 +72,7 @@ class Share implements IShare {
private $nodeCacheEntry;
/** @var bool */
private $hideDownload = false;
+ private bool $reminderSent = false;
private bool $noExpirationDate = false;
@@ -191,7 +192,7 @@ class Share implements IShare {
}
if ($this->fileId === null) {
- throw new NotFoundException("Share source not found");
+ throw new NotFoundException('Share source not found');
} else {
return $this->fileId;
}
@@ -613,4 +614,13 @@ class Share implements IShare {
public function getHideDownload(): bool {
return $this->hideDownload;
}
+
+ public function setReminderSent(bool $reminderSent): IShare {
+ $this->reminderSent = $reminderSent;
+ return $this;
+ }
+
+ public function getReminderSent(): bool {
+ return $this->reminderSent;
+ }
}
diff --git a/lib/private/Share20/ShareAttributes.php b/lib/private/Share20/ShareAttributes.php
index fbdcbf1ad26..96da1e336e3 100644
--- a/lib/private/Share20/ShareAttributes.php
+++ b/lib/private/Share20/ShareAttributes.php
@@ -47,9 +47,9 @@ class ShareAttributes implements IAttributes {
foreach ($this->attributes as $scope => $keys) {
foreach ($keys as $key => $value) {
$result[] = [
- "scope" => $scope,
- "key" => $key,
- "value" => $value,
+ 'scope' => $scope,
+ 'key' => $key,
+ 'value' => $value,
];
}
}
diff --git a/lib/private/SpeechToText/SpeechToTextManager.php b/lib/private/SpeechToText/SpeechToTextManager.php
index d6cda473875..d69add2d80b 100644
--- a/lib/private/SpeechToText/SpeechToTextManager.php
+++ b/lib/private/SpeechToText/SpeechToTextManager.php
@@ -24,6 +24,9 @@ use OCP\SpeechToText\ISpeechToTextManager;
use OCP\SpeechToText\ISpeechToTextProvider;
use OCP\SpeechToText\ISpeechToTextProviderWithId;
use OCP\SpeechToText\ISpeechToTextProviderWithUserId;
+use OCP\TaskProcessing\IManager as ITaskProcessingManager;
+use OCP\TaskProcessing\Task;
+use OCP\TaskProcessing\TaskTypes\AudioToText;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
@@ -41,6 +44,7 @@ class SpeechToTextManager implements ISpeechToTextManager {
private IJobList $jobList,
private IConfig $config,
private IUserSession $userSession,
+ private ITaskProcessingManager $taskProcessingManager,
) {
}
@@ -112,7 +116,30 @@ class SpeechToTextManager implements ISpeechToTextManager {
}
}
- public function transcribeFile(File $file): string {
+ public function transcribeFile(File $file, ?string $userId = null, string $appId = 'core'): string {
+ // try to run a TaskProcessing core:audio2text task
+ // this covers scheduling as well because OC\SpeechToText\TranscriptionJob calls this method
+ try {
+ if (isset($this->taskProcessingManager->getAvailableTaskTypes()['core:audio2text'])) {
+ $taskProcessingTask = new Task(
+ AudioToText::ID,
+ ['input' => $file->getId()],
+ $appId,
+ $userId,
+ 'from-SpeechToTextManager||' . $file->getId() . '||' . ($userId ?? '') . '||' . $appId,
+ );
+ $resultTask = $this->taskProcessingManager->runTask($taskProcessingTask);
+ if ($resultTask->getStatus() === Task::STATUS_SUCCESSFUL) {
+ $output = $resultTask->getOutput();
+ if (isset($output['output']) && is_string($output['output'])) {
+ return $output['output'];
+ }
+ }
+ }
+ } catch (Throwable $e) {
+ throw new RuntimeException('Failed to run a Speech-to-text job from STTManager with TaskProcessing for file ' . $file->getId(), 0, $e);
+ }
+
if (!$this->hasProviders()) {
throw new PreConditionNotMetException('No SpeechToText providers have been registered');
}
diff --git a/lib/private/SpeechToText/TranscriptionJob.php b/lib/private/SpeechToText/TranscriptionJob.php
index a46fd737865..6e899ef6e96 100644
--- a/lib/private/SpeechToText/TranscriptionJob.php
+++ b/lib/private/SpeechToText/TranscriptionJob.php
@@ -63,7 +63,7 @@ class TranscriptionJob extends QueuedJob {
);
return;
}
- $result = $this->speechToTextManager->transcribeFile($file);
+ $result = $this->speechToTextManager->transcribeFile($file, $userId, $appId);
$this->eventDispatcher->dispatchTyped(
new TranscriptionSuccessfulEvent(
$fileId,
diff --git a/lib/private/StreamImage.php b/lib/private/StreamImage.php
index 9290bf38b0f..34fe590efbd 100644
--- a/lib/private/StreamImage.php
+++ b/lib/private/StreamImage.php
@@ -133,4 +133,12 @@ class StreamImage implements IStreamImage {
public function resizeCopy(int $maxSize): IImage {
throw new \BadMethodCallException('Not implemented');
}
+
+ public function loadFromData(string $str): \GdImage|false {
+ throw new \BadMethodCallException('Not implemented');
+ }
+
+ public function readExif(string $data): void {
+ throw new \BadMethodCallException('Not implemented');
+ }
}
diff --git a/lib/private/Streamer.php b/lib/private/Streamer.php
index 4eca1105002..1f510f730ee 100644
--- a/lib/private/Streamer.php
+++ b/lib/private/Streamer.php
@@ -32,7 +32,7 @@ class Streamer {
* @param IRequest $request
* @param int|float $size The size of the files in bytes
* @param int $numberOfFiles The number of files (and directories) that will
- * be included in the streamed file
+ * be included in the streamed file
*/
public function __construct(IRequest $request, int|float $size, int $numberOfFiles) {
/**
diff --git a/lib/private/SystemConfig.php b/lib/private/SystemConfig.php
index f817e327b19..ed77526c29c 100644
--- a/lib/private/SystemConfig.php
+++ b/lib/private/SystemConfig.php
@@ -115,6 +115,24 @@ class SystemConfig {
}
/**
+ * Since system config is admin controlled, we can tell psalm to ignore any taint
+ *
+ * @psalm-taint-escape sql
+ * @psalm-taint-escape html
+ * @psalm-taint-escape ldap
+ * @psalm-taint-escape callable
+ * @psalm-taint-escape file
+ * @psalm-taint-escape ssrf
+ * @psalm-taint-escape cookie
+ * @psalm-taint-escape header
+ * @psalm-taint-escape has_quotes
+ * @psalm-pure
+ */
+ public static function trustSystemConfig(mixed $value): mixed {
+ return $value;
+ }
+
+ /**
* Lists all available config keys
* @return array an array of key names
*/
@@ -150,7 +168,7 @@ class SystemConfig {
* @return mixed the value or $default
*/
public function getValue($key, $default = '') {
- return $this->config->getValue($key, $default);
+ return $this->trustSystemConfig($this->config->getValue($key, $default));
}
/**
diff --git a/lib/private/SystemTag/SystemTagObjectMapper.php b/lib/private/SystemTag/SystemTagObjectMapper.php
index 10117eebeaa..157948e6e0c 100644
--- a/lib/private/SystemTag/SystemTagObjectMapper.php
+++ b/lib/private/SystemTag/SystemTagObjectMapper.php
@@ -234,7 +234,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper {
return ((int)$row[0] === \count($objIds));
}
- return (bool) $row;
+ return (bool)$row;
}
/**
diff --git a/lib/private/TagManager.php b/lib/private/TagManager.php
index 3258ce50bc5..f99653f2c05 100644
--- a/lib/private/TagManager.php
+++ b/lib/private/TagManager.php
@@ -43,7 +43,7 @@ class TagManager implements ITagManager, IEventListener {
* @param array $defaultTags An array of default tags to be used if none are stored.
* @param boolean $includeShared Whether to include tags for items shared with this user by others.
* @param string $userId user for which to retrieve the tags, defaults to the currently
- * logged in user
+ * logged in user
* @return \OCP\ITags
*
* since 20.0.0 $includeShared isn't used anymore
diff --git a/lib/private/Tags.php b/lib/private/Tags.php
index 420cabc36b9..20fd644132b 100644
--- a/lib/private/Tags.php
+++ b/lib/private/Tags.php
@@ -149,7 +149,7 @@ class Tags implements ITags {
*
* @param array $objIds array of object ids
* @return array|false of tags id as key to array of tag names
- * or false if an error occurred
+ * or false if an error occurred
*/
public function getTagsForObjects(array $objIds) {
$entries = [];
@@ -343,7 +343,7 @@ class Tags implements ITags {
* Add a list of new tags.
*
* @param string|string[] $names A string with a name or an array of strings containing
- * the name(s) of the tag(s) to add.
+ * the name(s) of the tag(s) to add.
* @param bool $sync When true, save the tags
* @param int|null $id int Optional object id to add to this|these tag(s)
* @return bool Returns false on error.
diff --git a/lib/private/Talk/Broker.php b/lib/private/Talk/Broker.php
index fc8e0280043..86e7e7ff4c1 100644
--- a/lib/private/Talk/Broker.php
+++ b/lib/private/Talk/Broker.php
@@ -47,7 +47,7 @@ class Broker implements IBroker {
$context = $this->coordinator->getRegistrationContext();
if ($context === null) {
// Backend requested too soon, e.g. from the bootstrap `register` method of an app
- throw new RuntimeException("Not all apps have been registered yet");
+ throw new RuntimeException('Not all apps have been registered yet');
}
$backendRegistration = $context->getTalkBackendRegistration();
if ($backendRegistration === null) {
@@ -63,7 +63,7 @@ class Broker implements IBroker {
// Remember and return
return $this->hasBackend = true;
} catch (Throwable $e) {
- $this->logger->error("Talk backend {class} could not be loaded: " . $e->getMessage(), [
+ $this->logger->error('Talk backend {class} could not be loaded: ' . $e->getMessage(), [
'class' => $backendRegistration->getService(),
'exception' => $e,
]);
@@ -81,7 +81,7 @@ class Broker implements IBroker {
array $moderators,
?IConversationOptions $options = null): IConversation {
if (!$this->hasBackend()) {
- throw new NoBackendException("The Talk broker has no registered backend");
+ throw new NoBackendException('The Talk broker has no registered backend');
}
return $this->backend->createConversation(
@@ -93,7 +93,7 @@ class Broker implements IBroker {
public function deleteConversation(string $id): void {
if (!$this->hasBackend()) {
- throw new NoBackendException("The Talk broker has no registered backend");
+ throw new NoBackendException('The Talk broker has no registered backend');
}
$this->backend->deleteConversation($id);
diff --git a/lib/private/TaskProcessing/Manager.php b/lib/private/TaskProcessing/Manager.php
index a804115a631..fb0a4da4c4e 100644
--- a/lib/private/TaskProcessing/Manager.php
+++ b/lib/private/TaskProcessing/Manager.php
@@ -68,7 +68,7 @@ class Manager implements IManager {
public const LEGACY_PREFIX_TEXTTOIMAGE = 'legacy:TextToImage:';
public const LEGACY_PREFIX_SPEECHTOTEXT = 'legacy:SpeechToText:';
- /** @var list<IProvider>|null */
+ /** @var list<IProvider>|null */
private ?array $providers = null;
/**
@@ -87,9 +87,7 @@ class Manager implements IManager {
private IEventDispatcher $dispatcher,
IAppDataFactory $appDataFactory,
private IRootFolder $rootFolder,
- private \OCP\TextProcessing\IManager $textProcessingManager,
private \OCP\TextToImage\IManager $textToImageManager,
- private \OCP\SpeechToText\ISpeechToTextManager $speechToTextManager,
private IUserMountCache $userMountCache,
private IClientService $clientService,
private IAppManager $appManager,
@@ -98,8 +96,34 @@ class Manager implements IManager {
}
+ /**
+ * This is almost a copy of textProcessingManager->getProviders
+ * to avoid a dependency cycle between TextProcessingManager and TaskProcessingManager
+ */
+ private function _getRawTextProcessingProviders(): array {
+ $context = $this->coordinator->getRegistrationContext();
+ if ($context === null) {
+ return [];
+ }
+
+ $providers = [];
+
+ foreach ($context->getTextProcessingProviders() as $providerServiceRegistration) {
+ $class = $providerServiceRegistration->getService();
+ try {
+ $providers[$class] = $this->serverContainer->get($class);
+ } catch (\Throwable $e) {
+ $this->logger->error('Failed to load Text processing provider ' . $class, [
+ 'exception' => $e,
+ ]);
+ }
+ }
+
+ return $providers;
+ }
+
private function _getTextProcessingProviders(): array {
- $oldProviders = $this->textProcessingManager->getProviders();
+ $oldProviders = $this->_getRawTextProcessingProviders();
$newProviders = [];
foreach ($oldProviders as $oldProvider) {
$provider = new class($oldProvider) implements IProvider, ISynchronousProvider {
@@ -190,7 +214,7 @@ class Manager implements IManager {
* @return ITaskType[]
*/
private function _getTextProcessingTaskTypes(): array {
- $oldProviders = $this->textProcessingManager->getProviders();
+ $oldProviders = $this->_getRawTextProcessingProviders();
$newTaskTypes = [];
foreach ($oldProviders as $oldProvider) {
// These are already implemented in the TaskProcessing realm
@@ -344,12 +368,35 @@ class Manager implements IManager {
return $newProviders;
}
+ /**
+ * This is almost a copy of SpeechToTextManager->getProviders
+ * to avoid a dependency cycle between SpeechToTextManager and TaskProcessingManager
+ */
+ private function _getRawSpeechToTextProviders(): array {
+ $context = $this->coordinator->getRegistrationContext();
+ if ($context === null) {
+ return [];
+ }
+ $providers = [];
+ foreach ($context->getSpeechToTextProviders() as $providerServiceRegistration) {
+ $class = $providerServiceRegistration->getService();
+ try {
+ $providers[$class] = $this->serverContainer->get($class);
+ } catch (NotFoundExceptionInterface|ContainerExceptionInterface|\Throwable $e) {
+ $this->logger->error('Failed to load SpeechToText provider ' . $class, [
+ 'exception' => $e,
+ ]);
+ }
+ }
+
+ return $providers;
+ }
/**
* @return IProvider[]
*/
private function _getSpeechToTextProviders(): array {
- $oldProviders = $this->speechToTextManager->getProviders();
+ $oldProviders = $this->_getRawSpeechToTextProviders();
$newProviders = [];
foreach ($oldProviders as $oldProvider) {
$newProvider = new class($oldProvider, $this->rootFolder, $this->appData) implements IProvider, ISynchronousProvider {
@@ -649,24 +696,24 @@ class Manager implements IManager {
return $this->providers;
}
- public function getPreferredProvider(string $taskType) {
+ public function getPreferredProvider(string $taskTypeId) {
try {
$preferences = json_decode($this->config->getAppValue('core', 'ai.taskprocessing_provider_preferences', 'null'), associative: true, flags: JSON_THROW_ON_ERROR);
$providers = $this->getProviders();
- if (isset($preferences[$taskType])) {
- $provider = current(array_values(array_filter($providers, fn ($provider) => $provider->getId() === $preferences[$taskType])));
+ if (isset($preferences[$taskTypeId])) {
+ $provider = current(array_values(array_filter($providers, fn ($provider) => $provider->getId() === $preferences[$taskTypeId])));
if ($provider !== false) {
return $provider;
}
}
// By default, use the first available provider
foreach ($providers as $provider) {
- if ($provider->getTaskTypeId() === $taskType) {
+ if ($provider->getTaskTypeId() === $taskTypeId) {
return $provider;
}
}
} catch (\JsonException $e) {
- $this->logger->warning('Failed to parse provider preferences while getting preferred provider for task type ' . $taskType, ['exception' => $e]);
+ $this->logger->warning('Failed to parse provider preferences while getting preferred provider for task type ' . $taskTypeId, ['exception' => $e]);
}
throw new \OCP\TaskProcessing\Exception\Exception('No matching provider found');
}
@@ -674,14 +721,14 @@ class Manager implements IManager {
public function getAvailableTaskTypes(): array {
if ($this->availableTaskTypes === null) {
$taskTypes = $this->_getTaskTypes();
- $providers = $this->getProviders();
$availableTaskTypes = [];
- foreach ($providers as $provider) {
- if (!isset($taskTypes[$provider->getTaskTypeId()])) {
+ foreach ($taskTypes as $taskType) {
+ try {
+ $provider = $this->getPreferredProvider($taskType->getId());
+ } catch (\OCP\TaskProcessing\Exception\Exception $e) {
continue;
}
- $taskType = $taskTypes[$provider->getTaskTypeId()];
try {
$availableTaskTypes[$provider->getTaskTypeId()] = [
'name' => $taskType->getName(),
@@ -716,55 +763,69 @@ class Manager implements IManager {
if (!$this->canHandleTask($task)) {
throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
}
- $taskTypes = $this->getAvailableTaskTypes();
- $inputShape = $taskTypes[$task->getTaskTypeId()]['inputShape'];
- $inputShapeDefaults = $taskTypes[$task->getTaskTypeId()]['inputShapeDefaults'];
- $inputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['inputShapeEnumValues'];
- $optionalInputShape = $taskTypes[$task->getTaskTypeId()]['optionalInputShape'];
- $optionalInputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['optionalInputShapeEnumValues'];
- $optionalInputShapeDefaults = $taskTypes[$task->getTaskTypeId()]['optionalInputShapeDefaults'];
- // validate input
- $this->validateInput($inputShape, $inputShapeDefaults, $inputShapeEnumValues, $task->getInput());
- $this->validateInput($optionalInputShape, $optionalInputShapeDefaults, $optionalInputShapeEnumValues, $task->getInput(), true);
- // authenticate access to mentioned files
- $ids = [];
- foreach ($inputShape + $optionalInputShape as $key => $descriptor) {
- if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
- /** @var list<int>|int $inputSlot */
- $inputSlot = $task->getInput()[$key];
- if (is_array($inputSlot)) {
- $ids += $inputSlot;
- } else {
- $ids[] = $inputSlot;
- }
- }
- }
- foreach ($ids as $fileId) {
- $this->validateFileId($fileId);
- $this->validateUserAccessToFile($fileId, $task->getUserId());
- }
- // remove superfluous keys and set input
- $input = $this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape);
- $inputWithDefaults = $this->fillInputDefaults($input, $inputShapeDefaults, $optionalInputShapeDefaults);
- $task->setInput($inputWithDefaults);
+ $this->prepareTask($task);
$task->setStatus(Task::STATUS_SCHEDULED);
- $task->setScheduledAt(time());
- $provider = $this->getPreferredProvider($task->getTaskTypeId());
- // calculate expected completion time
- $completionExpectedAt = new \DateTime('now');
- $completionExpectedAt->add(new \DateInterval('PT'.$provider->getExpectedRuntime().'S'));
- $task->setCompletionExpectedAt($completionExpectedAt);
- // create a db entity and insert into db table
- $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
- $this->taskMapper->insert($taskEntity);
- // make sure the scheduler knows the id
- $task->setId($taskEntity->getId());
+ $this->storeTask($task);
// schedule synchronous job if the provider is synchronous
+ $provider = $this->getPreferredProvider($task->getTaskTypeId());
if ($provider instanceof ISynchronousProvider) {
$this->jobList->add(SynchronousBackgroundJob::class, null);
}
}
+ public function runTask(Task $task): Task {
+ if (!$this->canHandleTask($task)) {
+ throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
+ }
+
+ $provider = $this->getPreferredProvider($task->getTaskTypeId());
+ if ($provider instanceof ISynchronousProvider) {
+ $this->prepareTask($task);
+ $task->setStatus(Task::STATUS_SCHEDULED);
+ $this->storeTask($task);
+ $this->processTask($task, $provider);
+ $task = $this->getTask($task->getId());
+ } else {
+ $this->scheduleTask($task);
+ // poll task
+ while ($task->getStatus() === Task::STATUS_SCHEDULED || $task->getStatus() === Task::STATUS_RUNNING) {
+ sleep(1);
+ $task = $this->getTask($task->getId());
+ }
+ }
+ return $task;
+ }
+
+ public function processTask(Task $task, ISynchronousProvider $provider): bool {
+ try {
+ try {
+ $input = $this->prepareInputData($task);
+ } catch (GenericFileException|NotPermittedException|LockedException|ValidationException|UnauthorizedException $e) {
+ $this->logger->warning('Failed to prepare input data for a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
+ $this->setTaskResult($task->getId(), $e->getMessage(), null);
+ return false;
+ }
+ try {
+ $this->setTaskStatus($task, Task::STATUS_RUNNING);
+ $output = $provider->process($task->getUserId(), $input, fn (float $progress) => $this->setTaskProgress($task->getId(), $progress));
+ } catch (ProcessingException $e) {
+ $this->logger->warning('Failed to process a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
+ $this->setTaskResult($task->getId(), $e->getMessage(), null);
+ return false;
+ } catch (\Throwable $e) {
+ $this->logger->error('Unknown error while processing TaskProcessing task', ['exception' => $e]);
+ $this->setTaskResult($task->getId(), $e->getMessage(), null);
+ return false;
+ }
+ $this->setTaskResult($task->getId(), null, $output);
+ } catch (NotFoundException $e) {
+ $this->logger->info('Could not find task anymore after execution. Moving on.', ['exception' => $e]);
+ } catch (Exception $e) {
+ $this->logger->error('Failed to report result of TaskProcessing task', ['exception' => $e]);
+ }
+ return true;
+ }
+
public function deleteTask(Task $task): void {
$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
$this->taskMapper->delete($taskEntity);
@@ -1096,6 +1157,72 @@ class Manager implements IManager {
}
/**
+ * Validate input, fill input default values, set completionExpectedAt, set scheduledAt
+ *
+ * @param Task $task
+ * @return void
+ * @throws UnauthorizedException
+ * @throws ValidationException
+ * @throws \OCP\TaskProcessing\Exception\Exception
+ */
+ private function prepareTask(Task $task): void {
+ $taskTypes = $this->getAvailableTaskTypes();
+ $taskType = $taskTypes[$task->getTaskTypeId()];
+ $inputShape = $taskType['inputShape'];
+ $inputShapeDefaults = $taskType['inputShapeDefaults'];
+ $inputShapeEnumValues = $taskType['inputShapeEnumValues'];
+ $optionalInputShape = $taskType['optionalInputShape'];
+ $optionalInputShapeEnumValues = $taskType['optionalInputShapeEnumValues'];
+ $optionalInputShapeDefaults = $taskType['optionalInputShapeDefaults'];
+ // validate input
+ $this->validateInput($inputShape, $inputShapeDefaults, $inputShapeEnumValues, $task->getInput());
+ $this->validateInput($optionalInputShape, $optionalInputShapeDefaults, $optionalInputShapeEnumValues, $task->getInput(), true);
+ // authenticate access to mentioned files
+ $ids = [];
+ foreach ($inputShape + $optionalInputShape as $key => $descriptor) {
+ if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
+ /** @var list<int>|int $inputSlot */
+ $inputSlot = $task->getInput()[$key];
+ if (is_array($inputSlot)) {
+ $ids += $inputSlot;
+ } else {
+ $ids[] = $inputSlot;
+ }
+ }
+ }
+ foreach ($ids as $fileId) {
+ $this->validateFileId($fileId);
+ $this->validateUserAccessToFile($fileId, $task->getUserId());
+ }
+ // remove superfluous keys and set input
+ $input = $this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape);
+ $inputWithDefaults = $this->fillInputDefaults($input, $inputShapeDefaults, $optionalInputShapeDefaults);
+ $task->setInput($inputWithDefaults);
+ $task->setScheduledAt(time());
+ $provider = $this->getPreferredProvider($task->getTaskTypeId());
+ // calculate expected completion time
+ $completionExpectedAt = new \DateTime('now');
+ $completionExpectedAt->add(new \DateInterval('PT'.$provider->getExpectedRuntime().'S'));
+ $task->setCompletionExpectedAt($completionExpectedAt);
+ }
+
+ /**
+ * Store the task in the DB and set its ID in the \OCP\TaskProcessing\Task input param
+ *
+ * @param Task $task
+ * @return void
+ * @throws Exception
+ * @throws \JsonException
+ */
+ private function storeTask(Task $task): void {
+ // create a db entity and insert into db table
+ $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
+ $this->taskMapper->insert($taskEntity);
+ // make sure the scheduler knows the id
+ $task->setId($taskEntity->getId());
+ }
+
+ /**
* @param array $output
* @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
* @return array
@@ -1189,9 +1316,9 @@ class Manager implements IManager {
];
try {
$client->request($httpMethod, $uri, $options);
- } catch (ClientException | ServerException $e) {
+ } catch (ClientException|ServerException $e) {
$this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Request failed', ['exception' => $e]);
- } catch (\Exception | \Throwable $e) {
+ } catch (\Exception|\Throwable $e) {
$this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Unknown error', ['exception' => $e]);
}
} elseif (str_starts_with($method, 'AppAPI:') && str_starts_with($uri, '/')) {
diff --git a/lib/private/TaskProcessing/SynchronousBackgroundJob.php b/lib/private/TaskProcessing/SynchronousBackgroundJob.php
index 093882d4c1e..de3b424176c 100644
--- a/lib/private/TaskProcessing/SynchronousBackgroundJob.php
+++ b/lib/private/TaskProcessing/SynchronousBackgroundJob.php
@@ -9,14 +9,8 @@ namespace OC\TaskProcessing;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\QueuedJob;
-use OCP\Files\GenericFileException;
-use OCP\Files\NotPermittedException;
-use OCP\Lock\LockedException;
use OCP\TaskProcessing\Exception\Exception;
use OCP\TaskProcessing\Exception\NotFoundException;
-use OCP\TaskProcessing\Exception\ProcessingException;
-use OCP\TaskProcessing\Exception\UnauthorizedException;
-use OCP\TaskProcessing\Exception\ValidationException;
use OCP\TaskProcessing\IManager;
use OCP\TaskProcessing\ISynchronousProvider;
use OCP\TaskProcessing\Task;
@@ -43,55 +37,41 @@ class SynchronousBackgroundJob extends QueuedJob {
if (!$provider instanceof ISynchronousProvider) {
continue;
}
- $taskType = $provider->getTaskTypeId();
+ $taskTypeId = $provider->getTaskTypeId();
+ // only use this provider if it is the preferred one
+ $preferredProvider = $this->taskProcessingManager->getPreferredProvider($taskTypeId);
+ if ($provider->getId() !== $preferredProvider->getId()) {
+ continue;
+ }
try {
- $task = $this->taskProcessingManager->getNextScheduledTask([$taskType]);
+ $task = $this->taskProcessingManager->getNextScheduledTask([$taskTypeId]);
} catch (NotFoundException $e) {
continue;
} catch (Exception $e) {
$this->logger->error('Unknown error while retrieving scheduled TaskProcessing tasks', ['exception' => $e]);
continue;
}
- try {
- try {
- $input = $this->taskProcessingManager->prepareInputData($task);
- } catch (GenericFileException|NotPermittedException|LockedException|ValidationException|UnauthorizedException $e) {
- $this->logger->warning('Failed to prepare input data for a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
- $this->taskProcessingManager->setTaskResult($task->getId(), $e->getMessage(), null);
- // Schedule again
- $this->jobList->add(self::class, $argument);
- return;
- }
- try {
- $this->taskProcessingManager->setTaskStatus($task, Task::STATUS_RUNNING);
- $output = $provider->process($task->getUserId(), $input, fn (float $progress) => $this->taskProcessingManager->setTaskProgress($task->getId(), $progress));
- } catch (ProcessingException $e) {
- $this->logger->warning('Failed to process a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
- $this->taskProcessingManager->setTaskResult($task->getId(), $e->getMessage(), null);
- // Schedule again
- $this->jobList->add(self::class, $argument);
- return;
- } catch (\Throwable $e) {
- $this->logger->error('Unknown error while processing TaskProcessing task', ['exception' => $e]);
- $this->taskProcessingManager->setTaskResult($task->getId(), $e->getMessage(), null);
- // Schedule again
- $this->jobList->add(self::class, $argument);
- return;
- }
- $this->taskProcessingManager->setTaskResult($task->getId(), null, $output);
- } catch (NotFoundException $e) {
- $this->logger->info('Could not find task anymore after execution. Moving on.', ['exception' => $e]);
- } catch (Exception $e) {
- $this->logger->error('Failed to report result of TaskProcessing task', ['exception' => $e]);
+ if (!$this->taskProcessingManager->processTask($task, $provider)) {
+ // Schedule again
+ $this->jobList->add(self::class, $argument);
}
}
+ // check if this job needs to be scheduled again:
+ // if there is at least one preferred synchronous provider that has a scheduled task
$synchronousProviders = array_filter($providers, fn ($provider) =>
$provider instanceof ISynchronousProvider);
- $taskTypes = array_values(array_map(fn ($provider) =>
- $provider->getTaskTypeId(),
- $synchronousProviders
- ));
+ $synchronousPreferredProviders = array_filter($synchronousProviders, function ($provider) {
+ $taskTypeId = $provider->getTaskTypeId();
+ $preferredProvider = $this->taskProcessingManager->getPreferredProvider($taskTypeId);
+ return $provider->getId() === $preferredProvider->getId();
+ });
+ $taskTypes = array_values(
+ array_map(
+ fn ($provider) => $provider->getTaskTypeId(),
+ $synchronousPreferredProviders
+ )
+ );
$taskTypesWithTasks = array_filter($taskTypes, function ($taskType) {
try {
$this->taskProcessingManager->getNextScheduledTask([$taskType]);
diff --git a/lib/private/TempManager.php b/lib/private/TempManager.php
index 68a53f493db..74b4d6b1f24 100644
--- a/lib/private/TempManager.php
+++ b/lib/private/TempManager.php
@@ -138,7 +138,7 @@ class TempManager implements ITempManager {
\OC_Helper::rmdirr($file);
} catch (\UnexpectedValueException $ex) {
$this->log->warning(
- "Error deleting temporary file/folder: {file} - Reason: {error}",
+ 'Error deleting temporary file/folder: {file} - Reason: {error}',
[
'file' => $file,
'error' => $ex->getMessage(),
diff --git a/lib/private/Template/Base.php b/lib/private/Template/Base.php
index 2adf9172f6b..b48c10143cf 100644
--- a/lib/private/Template/Base.php
+++ b/lib/private/Template/Base.php
@@ -24,11 +24,14 @@ class Base {
* @param string $template
* @param string $requestToken
* @param \OCP\IL10N $l10n
+ * @param string $cspNonce
* @param Defaults $theme
*/
- public function __construct($template, $requestToken, $l10n, $theme) {
- $this->vars = [];
- $this->vars['requesttoken'] = $requestToken;
+ public function __construct($template, $requestToken, $l10n, $theme, $cspNonce) {
+ $this->vars = [
+ 'cspNonce' => $cspNonce,
+ 'requesttoken' => $requestToken,
+ ];
$this->l10n = $l10n;
$this->template = $template;
$this->theme = $theme;
diff --git a/lib/private/Template/JSConfigHelper.php b/lib/private/Template/JSConfigHelper.php
index eb253042f19..55fb348f01b 100644
--- a/lib/private/Template/JSConfigHelper.php
+++ b/lib/private/Template/JSConfigHelper.php
@@ -160,16 +160,16 @@ class JSConfigHelper {
];
$array = [
- "_oc_debug" => $this->config->getSystemValue('debug', false) ? 'true' : 'false',
- "_oc_isadmin" => $uid !== null && $this->groupManager->isAdmin($uid) ? 'true' : 'false',
- "backendAllowsPasswordConfirmation" => $userBackendAllowsPasswordConfirmation ? 'true' : 'false',
- "oc_dataURL" => is_string($dataLocation) ? "\"" . $dataLocation . "\"" : 'false',
- "_oc_webroot" => "\"" . \OC::$WEBROOT . "\"",
- "_oc_appswebroots" => str_replace('\\/', '/', json_encode($apps_paths)), // Ugly unescape slashes waiting for better solution
- "datepickerFormatDate" => json_encode($this->l->l('jsdate', null)),
+ '_oc_debug' => $this->config->getSystemValue('debug', false) ? 'true' : 'false',
+ '_oc_isadmin' => $uid !== null && $this->groupManager->isAdmin($uid) ? 'true' : 'false',
+ 'backendAllowsPasswordConfirmation' => $userBackendAllowsPasswordConfirmation ? 'true' : 'false',
+ 'oc_dataURL' => is_string($dataLocation) ? '"' . $dataLocation . '"' : 'false',
+ '_oc_webroot' => '"' . \OC::$WEBROOT . '"',
+ '_oc_appswebroots' => str_replace('\\/', '/', json_encode($apps_paths)), // Ugly unescape slashes waiting for better solution
+ 'datepickerFormatDate' => json_encode($this->l->l('jsdate', null)),
'nc_lastLogin' => $lastConfirmTimestamp,
'nc_pageLoad' => time(),
- "dayNames" => json_encode([
+ 'dayNames' => json_encode([
$this->l->t('Sunday'),
$this->l->t('Monday'),
$this->l->t('Tuesday'),
@@ -178,7 +178,7 @@ class JSConfigHelper {
$this->l->t('Friday'),
$this->l->t('Saturday')
]),
- "dayNamesShort" => json_encode([
+ 'dayNamesShort' => json_encode([
$this->l->t('Sun.'),
$this->l->t('Mon.'),
$this->l->t('Tue.'),
@@ -187,7 +187,7 @@ class JSConfigHelper {
$this->l->t('Fri.'),
$this->l->t('Sat.')
]),
- "dayNamesMin" => json_encode([
+ 'dayNamesMin' => json_encode([
$this->l->t('Su'),
$this->l->t('Mo'),
$this->l->t('Tu'),
@@ -196,7 +196,7 @@ class JSConfigHelper {
$this->l->t('Fr'),
$this->l->t('Sa')
]),
- "monthNames" => json_encode([
+ 'monthNames' => json_encode([
$this->l->t('January'),
$this->l->t('February'),
$this->l->t('March'),
@@ -210,7 +210,7 @@ class JSConfigHelper {
$this->l->t('November'),
$this->l->t('December')
]),
- "monthNamesShort" => json_encode([
+ 'monthNamesShort' => json_encode([
$this->l->t('Jan.'),
$this->l->t('Feb.'),
$this->l->t('Mar.'),
@@ -224,9 +224,9 @@ class JSConfigHelper {
$this->l->t('Nov.'),
$this->l->t('Dec.')
]),
- "firstDay" => json_encode($firstDay),
- "_oc_config" => json_encode($config),
- "oc_appconfig" => json_encode([
+ 'firstDay' => json_encode($firstDay),
+ '_oc_config' => json_encode($config),
+ 'oc_appconfig' => json_encode([
'core' => [
'defaultExpireDateEnabled' => $defaultExpireDateEnabled,
'defaultExpireDate' => $defaultExpireDate,
@@ -246,7 +246,7 @@ class JSConfigHelper {
'defaultRemoteExpireDateEnforced' => $defaultRemoteExpireDateEnforced,
]
]),
- "_theme" => json_encode([
+ '_theme' => json_encode([
'entity' => $this->defaults->getEntity(),
'name' => $this->defaults->getName(),
'productName' => $this->defaults->getProductName(),
diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php
index 7b33f88d4db..2e397d2a3e2 100644
--- a/lib/private/TemplateLayout.php
+++ b/lib/private/TemplateLayout.php
@@ -8,6 +8,7 @@
namespace OC;
use bantu\IniGetWrapper\IniGetWrapper;
+use OC\AppFramework\Http\Request;
use OC\Authentication\Token\IProvider;
use OC\Files\FilenameValidator;
use OC\Search\SearchQuery;
@@ -20,6 +21,7 @@ use OCP\Defaults;
use OCP\IConfig;
use OCP\IInitialStateService;
use OCP\INavigationManager;
+use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\L10N\IFactory;
@@ -286,6 +288,13 @@ class TemplateLayout extends \OC_Template {
}
}
+ $request = \OCP\Server::get(IRequest::class);
+ if ($request->isUserAgent([Request::USER_AGENT_CLIENT_IOS, Request::USER_AGENT_SAFARI, Request::USER_AGENT_SAFARI_MOBILE])) {
+ // Prevent auto zoom with iOS but still allow user zoom
+ // On chrome (and others) this does not work (will also disable user zoom)
+ $this->assign('viewport_maximum_scale', '1.0');
+ }
+
$this->assign('initialStates', $this->initialState->getInitialStates());
$this->assign('id-app-content', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-content' : '#content');
@@ -300,7 +309,7 @@ class TemplateLayout extends \OC_Template {
protected function getVersionHashSuffix($path = false, $file = false) {
if ($this->config->getSystemValueBool('debug', false)) {
// allows chrome workspace mapping in debug mode
- return "";
+ return '';
}
$themingSuffix = '';
$v = [];
diff --git a/lib/private/TextProcessing/Manager.php b/lib/private/TextProcessing/Manager.php
index a03c028a5c9..9801a99ddec 100644
--- a/lib/private/TextProcessing/Manager.php
+++ b/lib/private/TextProcessing/Manager.php
@@ -20,13 +20,22 @@ use OCP\DB\Exception;
use OCP\IConfig;
use OCP\IServerContainer;
use OCP\PreConditionNotMetException;
+use OCP\TaskProcessing\IManager as TaskProcessingIManager;
+use OCP\TaskProcessing\TaskTypes\TextToText;
+use OCP\TaskProcessing\TaskTypes\TextToTextHeadline;
+use OCP\TaskProcessing\TaskTypes\TextToTextSummary;
+use OCP\TaskProcessing\TaskTypes\TextToTextTopics;
use OCP\TextProcessing\Exception\TaskFailureException;
+use OCP\TextProcessing\FreePromptTaskType;
+use OCP\TextProcessing\HeadlineTaskType;
use OCP\TextProcessing\IManager;
use OCP\TextProcessing\IProvider;
use OCP\TextProcessing\IProviderWithExpectedRuntime;
use OCP\TextProcessing\IProviderWithId;
+use OCP\TextProcessing\SummaryTaskType;
use OCP\TextProcessing\Task;
use OCP\TextProcessing\Task as OCPTask;
+use OCP\TextProcessing\TopicsTaskType;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Throwable;
@@ -42,6 +51,7 @@ class Manager implements IManager {
private IJobList $jobList,
private TaskMapper $taskMapper,
private IConfig $config,
+ private TaskProcessingIManager $taskProcessingManager,
) {
}
@@ -98,6 +108,55 @@ class Manager implements IManager {
* @inheritDoc
*/
public function runTask(OCPTask $task): string {
+ // try to run a task processing task if possible
+ $taskTypeClass = $task->getType();
+ $taskProcessingCompatibleTaskTypes = [
+ FreePromptTaskType::class => TextToText::ID,
+ HeadlineTaskType::class => TextToTextHeadline::ID,
+ SummaryTaskType::class => TextToTextSummary::ID,
+ TopicsTaskType::class => TextToTextTopics::ID,
+ ];
+ if (isset($taskProcessingCompatibleTaskTypes[$taskTypeClass]) && isset($this->taskProcessingManager->getAvailableTaskTypes()[$taskProcessingCompatibleTaskTypes[$taskTypeClass]])) {
+ try {
+ $taskProcessingTaskTypeId = $taskProcessingCompatibleTaskTypes[$taskTypeClass];
+ $taskProcessingTask = new \OCP\TaskProcessing\Task(
+ $taskProcessingTaskTypeId,
+ ['input' => $task->getInput()],
+ $task->getAppId(),
+ $task->getUserId(),
+ $task->getIdentifier(),
+ );
+
+ $task->setStatus(OCPTask::STATUS_RUNNING);
+ if ($task->getId() === null) {
+ $taskEntity = $this->taskMapper->insert(DbTask::fromPublicTask($task));
+ $task->setId($taskEntity->getId());
+ } else {
+ $this->taskMapper->update(DbTask::fromPublicTask($task));
+ }
+ $this->logger->debug('Running a TextProcessing (' . $taskTypeClass . ') task with TaskProcessing');
+ $taskProcessingResultTask = $this->taskProcessingManager->runTask($taskProcessingTask);
+ if ($taskProcessingResultTask->getStatus() === \OCP\TaskProcessing\Task::STATUS_SUCCESSFUL) {
+ $output = $taskProcessingResultTask->getOutput();
+ if (isset($output['output']) && is_string($output['output'])) {
+ $task->setOutput($output['output']);
+ $task->setStatus(OCPTask::STATUS_SUCCESSFUL);
+ $this->taskMapper->update(DbTask::fromPublicTask($task));
+ return $output['output'];
+ }
+ }
+ } catch (\Throwable $e) {
+ $this->logger->error('TextProcessing to TaskProcessing failed', ['exception' => $e]);
+ $task->setStatus(OCPTask::STATUS_FAILED);
+ $this->taskMapper->update(DbTask::fromPublicTask($task));
+ throw new TaskFailureException('TextProcessing to TaskProcessing failed: ' . $e->getMessage(), 0, $e);
+ }
+ $task->setStatus(OCPTask::STATUS_FAILED);
+ $this->taskMapper->update(DbTask::fromPublicTask($task));
+ throw new TaskFailureException('Could not run task');
+ }
+
+ // try to run the text processing task
if (!$this->canHandleTask($task)) {
throw new PreConditionNotMetException('No text processing provider is installed that can handle this task');
}
@@ -169,7 +228,7 @@ class Manager implements IManager {
throw new PreConditionNotMetException('No LanguageModel provider is installed that can handle this task');
}
[$provider,] = $this->getPreferredProviders($task);
- $maxExecutionTime = (int) ini_get('max_execution_time');
+ $maxExecutionTime = (int)ini_get('max_execution_time');
// Offload the task to a background job if the expected runtime of the likely provider is longer than 80% of our max execution time
// or if the provider doesn't provide a getExpectedRuntime() method
if (!$provider instanceof IProviderWithExpectedRuntime || $provider->getExpectedRuntime() > $maxExecutionTime * 0.8) {
diff --git a/lib/private/TextToImage/Manager.php b/lib/private/TextToImage/Manager.php
index 6ad3592a1b7..2d6cc6450a1 100644
--- a/lib/private/TextToImage/Manager.php
+++ b/lib/private/TextToImage/Manager.php
@@ -124,16 +124,16 @@ class Manager implements IManager {
$folder = $this->appData->newFolder('text2image');
}
try {
- $folder = $folder->getFolder((string) $task->getId());
+ $folder = $folder->getFolder((string)$task->getId());
} catch(NotFoundException) {
$this->logger->debug('Creating new folder in appdata Text2Image results folder');
- $folder = $folder->newFolder((string) $task->getId());
+ $folder = $folder->newFolder((string)$task->getId());
}
$this->logger->debug('Creating result files for Text2Image task');
$resources = [];
$files = [];
for ($i = 0; $i < $task->getNumberOfImages(); $i++) {
- $file = $folder->newFile((string) $i);
+ $file = $folder->newFile((string)$i);
$files[] = $file;
$resource = $file->write();
if ($resource !== false && $resource !== true && is_resource($resource)) {
@@ -216,7 +216,7 @@ class Manager implements IManager {
throw new PreConditionNotMetException('No text to image provider is installed that can handle this task');
}
$providers = $this->getPreferredProviders();
- $maxExecutionTime = (int) ini_get('max_execution_time');
+ $maxExecutionTime = (int)ini_get('max_execution_time');
// Offload the task to a background job if the expected runtime of the likely provider is longer than 80% of our max execution time
if ($providers[0]->getExpectedRuntime() > $maxExecutionTime * 0.8) {
$this->scheduleTask($task);
diff --git a/lib/private/URLGenerator.php b/lib/private/URLGenerator.php
index ab568a3296d..1ddc9aaa0e1 100644
--- a/lib/private/URLGenerator.php
+++ b/lib/private/URLGenerator.php
@@ -112,7 +112,7 @@ class URLGenerator implements IURLGenerator {
* @param string $appName app
* @param string $file file
* @param array $args array with param=>value, will be appended to the returned url
- * The value of $args will be urlencoded
+ * The value of $args will be urlencoded
* @return string the url
*
* Returns a url to the given app and file.
@@ -303,7 +303,7 @@ class URLGenerator implements IURLGenerator {
*/
public function getBaseUrl(): string {
// BaseUrl can be equal to 'http(s)://' during the first steps of the initial setup.
- if ($this->baseUrl === null || $this->baseUrl === "http://" || $this->baseUrl === "https://") {
+ if ($this->baseUrl === null || $this->baseUrl === 'http://' || $this->baseUrl === 'https://') {
$this->baseUrl = $this->request->getServerProtocol() . '://' . $this->request->getServerHost() . \OC::$WEBROOT;
}
return $this->baseUrl;
diff --git a/lib/private/Updater.php b/lib/private/Updater.php
index e26faf86f92..2722c172f1a 100644
--- a/lib/private/Updater.php
+++ b/lib/private/Updater.php
@@ -147,7 +147,7 @@ class Updater extends BasicEmitter {
// this should really be a JSON file
require \OC::$SERVERROOT . '/version.php';
/** @var string $vendor */
- return (string) $vendor;
+ return (string)$vendor;
}
/**
diff --git a/lib/private/Updater/VersionCheck.php b/lib/private/Updater/VersionCheck.php
index 9ad129db1a4..cc5ff63379c 100644
--- a/lib/private/Updater/VersionCheck.php
+++ b/lib/private/Updater/VersionCheck.php
@@ -61,7 +61,7 @@ class VersionCheck {
$version['php_minor'] = PHP_MINOR_VERSION;
$version['php_release'] = PHP_RELEASE_VERSION;
$version['category'] = $this->computeCategory();
- $version['isSubscriber'] = (int) $this->registry->delegateHasValidSubscription();
+ $version['isSubscriber'] = (int)$this->registry->delegateHasValidSubscription();
$versionString = implode('x', $version);
//fetch xml data from updater
diff --git a/lib/private/User/Database.php b/lib/private/User/Database.php
index bd6aa7ba2c2..a80cc39a732 100644
--- a/lib/private/User/Database.php
+++ b/lib/private/User/Database.php
@@ -108,7 +108,7 @@ class Database extends ABackend implements
// Repopulate the cache
$this->loadUser($uid);
- return (bool) $result;
+ return (bool)$result;
}, $this->dbConn);
}
diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php
index 2c8cc10dc15..96be58a84a2 100644
--- a/lib/private/User/Manager.php
+++ b/lib/private/User/Manager.php
@@ -455,7 +455,7 @@ class Manager extends PublicEmitter implements IUserManager {
* returns how many users per backend exist (if supported by backend)
*
* @param boolean $hasLoggedIn when true only users that have a lastLogin
- * entry in the preferences table will be affected
+ * entry in the preferences table will be affected
* @return array<string, int> an array of backend class as key and count number as value
*/
public function countUsers() {
@@ -486,7 +486,7 @@ class Manager extends PublicEmitter implements IUserManager {
*
* @param IGroup[] $groups an array of gid to search in
* @return array|int an array of backend class as key and count number as value
- * if $hasLoggedIn is true only an int is returned
+ * if $hasLoggedIn is true only an int is returned
*/
public function countUsersOfGroups(array $groups) {
$users = [];
@@ -506,7 +506,7 @@ class Manager extends PublicEmitter implements IUserManager {
* @psalm-param \Closure(\OCP\IUser):?bool $callback
* @param string $search
* @param boolean $onlySeen when true only users that have a lastLogin entry
- * in the preferences table will be affected
+ * in the preferences table will be affected
* @since 9.0.0
*/
public function callForAllUsers(\Closure $callback, $search = '', $onlySeen = false) {
diff --git a/lib/private/User/User.php b/lib/private/User/User.php
index 6495b5cf276..6f7ceb08532 100644
--- a/lib/private/User/User.php
+++ b/lib/private/User/User.php
@@ -216,9 +216,9 @@ class User implements IUser {
*/
public function getLastLogin() {
if ($this->lastLogin === null) {
- $this->lastLogin = (int) $this->config->getUserValue($this->uid, 'login', 'lastLogin', 0);
+ $this->lastLogin = (int)$this->config->getUserValue($this->uid, 'login', 'lastLogin', 0);
}
- return (int) $this->lastLogin;
+ return (int)$this->lastLogin;
}
/**
diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php
index 3347e4a6ec2..f48f4fd0b98 100644
--- a/lib/private/legacy/OC_App.php
+++ b/lib/private/legacy/OC_App.php
@@ -171,7 +171,7 @@ class OC_App {
*
* @param bool $forceRefresh whether to refresh the cache
* @param bool $all whether to return apps for all users, not only the
- * currently logged in one
+ * currently logged in one
* @return string[]
*/
public static function getEnabledApps(bool $forceRefresh = false, bool $all = false): array {
@@ -886,7 +886,7 @@ class OC_App {
} elseif ($englishFallback !== false) {
return $englishFallback;
}
- return (string) $fallback;
+ return (string)$fallback;
}
/**
diff --git a/lib/private/legacy/OC_Files.php b/lib/private/legacy/OC_Files.php
index 76d61a98558..07caba42ff2 100644
--- a/lib/private/legacy/OC_Files.php
+++ b/lib/private/legacy/OC_Files.php
@@ -43,7 +43,7 @@ class OC_Files {
OC_Response::setContentDispositionHeader($name, 'attachment');
header('Content-Transfer-Encoding: binary', true);
header('Expires: 0');
- header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
+ header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
$fileSize = \OC\Files\Filesystem::filesize($filename);
$type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename));
if ($fileSize > -1) {
@@ -334,8 +334,8 @@ class OC_Files {
foreach ($rangeArray as $range) {
echo "\r\n--".self::getBoundary()."\r\n".
- "Content-type: ".$type."\r\n".
- "Content-range: bytes ".$range['from']."-".$range['to']."/".$range['size']."\r\n\r\n";
+ 'Content-type: '.$type."\r\n".
+ 'Content-range: bytes '.$range['from'].'-'.$range['to'].'/'.$range['size']."\r\n\r\n";
$view->readfilePart($filename, $range['from'], $range['to']);
}
echo "\r\n--".self::getBoundary()."--\r\n";
diff --git a/lib/private/legacy/OC_Helper.php b/lib/private/legacy/OC_Helper.php
index 7db60363ff3..33cc966da2a 100644
--- a/lib/private/legacy/OC_Helper.php
+++ b/lib/private/legacy/OC_Helper.php
@@ -44,7 +44,7 @@ class OC_Helper {
*/
public static function humanFileSize(int|float $bytes): string {
if ($bytes < 0) {
- return "?";
+ return '?';
}
if ($bytes < 1024) {
return "$bytes B";
@@ -127,7 +127,7 @@ class OC_Helper {
}
$files = scandir($src);
foreach ($files as $file) {
- if ($file != "." && $file != "..") {
+ if ($file != '.' && $file != '..') {
self::copyr("$src/$file", "$dest/$file");
}
}
@@ -195,21 +195,21 @@ class OC_Helper {
* @param bool $path
* @internal param string $program name
* @internal param string $optional search path, defaults to $PATH
- * @return bool true if executable program found in path
+ * @return bool true if executable program found in path
*/
public static function canExecute($name, $path = false) {
// path defaults to PATH from environment if not set
if ($path === false) {
- $path = getenv("PATH");
+ $path = getenv('PATH');
}
// we look for an executable file of that name
- $exts = [""];
- $check_fn = "is_executable";
+ $exts = [''];
+ $check_fn = 'is_executable';
// Default check will be done with $path directories :
$dirs = explode(PATH_SEPARATOR, $path);
// WARNING : We have to check if open_basedir is enabled :
$obd = OC::$server->get(IniGetWrapper::class)->getString('open_basedir');
- if ($obd != "none") {
+ if ($obd != 'none') {
$obd_values = explode(PATH_SEPARATOR, $obd);
if (count($obd_values) > 0 and $obd_values[0]) {
// open_basedir is in effect !
@@ -516,13 +516,13 @@ class OC_Helper {
$free = 0.0;
}
} catch (\Exception $e) {
- if ($path === "") {
+ if ($path === '') {
throw $e;
}
/** @var LoggerInterface $logger */
$logger = \OC::$server->get(LoggerInterface::class);
- $logger->warning("Error while getting quota info, using root quota", ['exception' => $e]);
- $rootInfo = self::getStorageInfo("");
+ $logger->warning('Error while getting quota info, using root quota', ['exception' => $e]);
+ $rootInfo = self::getStorageInfo('');
$memcache->set($cacheKey, $rootInfo, 5 * 60);
return $rootInfo;
}
diff --git a/lib/private/legacy/OC_Hook.php b/lib/private/legacy/OC_Hook.php
index 2f6686f9126..5c36a253895 100644
--- a/lib/private/legacy/OC_Hook.php
+++ b/lib/private/legacy/OC_Hook.php
@@ -43,8 +43,8 @@ class OC_Hook {
}
// Connect the hook handler to the requested emitter
self::$registered[$signalClass][$signalName][] = [
- "class" => $slotClass,
- "name" => $slotName
+ 'class' => $slotClass,
+ 'name' => $slotName
];
// No chance for failure ;-)
@@ -79,7 +79,7 @@ class OC_Hook {
// Call all slots
foreach (self::$registered[$signalClass][$signalName] as $i) {
try {
- call_user_func([ $i["class"], $i["name"] ], $params);
+ call_user_func([ $i['class'], $i['name'] ], $params);
} catch (Exception $e) {
self::$thrownExceptions[] = $e;
\OC::$server->getLogger()->logException($e);
diff --git a/lib/private/legacy/OC_Template.php b/lib/private/legacy/OC_Template.php
index 5caa733b115..422709cec7d 100644
--- a/lib/private/legacy/OC_Template.php
+++ b/lib/private/legacy/OC_Template.php
@@ -43,6 +43,7 @@ class OC_Template extends \OC\Template\Base {
$theme = OC_Util::getTheme();
$requestToken = (OC::$server->getSession() && $registerCall) ? \OCP\Util::callRegister() : '';
+ $cspNonce = \OCP\Server::get(\OC\Security\CSP\ContentSecurityPolicyNonceManager::class)->getNonce();
$parts = explode('/', $app); // fix translation when app is something like core/lostpassword
$l10n = \OC::$server->getL10N($parts[0]);
@@ -56,7 +57,13 @@ class OC_Template extends \OC\Template\Base {
$this->path = $path;
$this->app = $app;
- parent::__construct($template, $requestToken, $l10n, $themeDefaults);
+ parent::__construct(
+ $template,
+ $requestToken,
+ $l10n,
+ $themeDefaults,
+ $cspNonce,
+ );
}
@@ -88,7 +95,7 @@ class OC_Template extends \OC\Template\Base {
* @param string $tag tag name of the element
* @param array $attributes array of attributes for the element
* @param string $text the text content for the element. If $text is null then the
- * element will be written as empty element. So use "" to get a closing tag.
+ * element will be written as empty element. So use "" to get a closing tag.
*/
public function addHeader($tag, $attributes, $text = null) {
$this->headers[] = [
@@ -165,7 +172,7 @@ class OC_Template extends \OC\Template\Base {
* @return boolean|null
*/
public static function printUserPage($application, $name, $parameters = []) {
- $content = new OC_Template($application, $name, "user");
+ $content = new OC_Template($application, $name, 'user');
foreach ($parameters as $key => $value) {
$content->assign($key, $value);
}
@@ -180,7 +187,7 @@ class OC_Template extends \OC\Template\Base {
* @return bool
*/
public static function printAdminPage($application, $name, $parameters = []) {
- $content = new OC_Template($application, $name, "admin");
+ $content = new OC_Template($application, $name, 'admin');
foreach ($parameters as $key => $value) {
$content->assign($key, $value);
}
diff --git a/lib/private/legacy/OC_User.php b/lib/private/legacy/OC_User.php
index d2978f6ad21..b8a00de84cc 100644
--- a/lib/private/legacy/OC_User.php
+++ b/lib/private/legacy/OC_User.php
@@ -142,7 +142,7 @@ class OC_User {
public static function loginWithApache(\OCP\Authentication\IApacheBackend $backend) {
$uid = $backend->getCurrentUserId();
$run = true;
- OC_Hook::emit("OC_User", "pre_login", ["run" => &$run, "uid" => $uid, 'backend' => $backend]);
+ OC_Hook::emit('OC_User', 'pre_login', ['run' => &$run, 'uid' => $uid, 'backend' => $backend]);
if ($uid) {
if (self::getUser() !== $uid) {
@@ -213,9 +213,9 @@ class OC_User {
* Verify with Apache whether user is authenticated.
*
* @return boolean|null
- * true: authenticated
- * false: not authenticated
- * null: not handled / no backend available
+ * true: authenticated
+ * false: not authenticated
+ * null: not handled / no backend available
*/
public static function handleApacheAuth() {
$backend = self::findFirstActiveUsedBackend();
diff --git a/lib/private/legacy/OC_Util.php b/lib/private/legacy/OC_Util.php
index 3b5222fee64..84bb0b645d5 100644
--- a/lib/private/legacy/OC_Util.php
+++ b/lib/private/legacy/OC_Util.php
@@ -296,7 +296,7 @@ class OC_Util {
private static function generatePath($application, $directory, $file) {
if (is_null($file)) {
$file = $application;
- $application = "";
+ $application = '';
}
if (!empty($application)) {
return "$application/$directory/$file";
@@ -322,7 +322,7 @@ class OC_Util {
if ($application !== 'core' && $file !== null) {
self::addTranslations($application);
}
- self::addExternalResource($application, $prepend, $path, "script");
+ self::addExternalResource($application, $prepend, $path, 'script');
}
/**
@@ -335,7 +335,7 @@ class OC_Util {
*/
public static function addVendorScript($application, $file = null, $prepend = false) {
$path = OC_Util::generatePath($application, 'vendor', $file);
- self::addExternalResource($application, $prepend, $path, "script");
+ self::addExternalResource($application, $prepend, $path, 'script');
}
/**
@@ -356,7 +356,7 @@ class OC_Util {
} else {
$path = "l10n/$languageCode";
}
- self::addExternalResource($application, $prepend, $path, "script");
+ self::addExternalResource($application, $prepend, $path, 'script');
}
/**
@@ -369,7 +369,7 @@ class OC_Util {
*/
public static function addStyle($application, $file = null, $prepend = false) {
$path = OC_Util::generatePath($application, 'css', $file);
- self::addExternalResource($application, $prepend, $path, "style");
+ self::addExternalResource($application, $prepend, $path, 'style');
}
/**
@@ -382,7 +382,7 @@ class OC_Util {
*/
public static function addVendorStyle($application, $file = null, $prepend = false) {
$path = OC_Util::generatePath($application, 'vendor', $file);
- self::addExternalResource($application, $prepend, $path, "style");
+ self::addExternalResource($application, $prepend, $path, 'style');
}
/**
@@ -394,8 +394,8 @@ class OC_Util {
* @param string $type (script or style)
* @return void
*/
- private static function addExternalResource($application, $prepend, $path, $type = "script") {
- if ($type === "style") {
+ private static function addExternalResource($application, $prepend, $path, $type = 'script') {
+ if ($type === 'style') {
if (!in_array($path, self::$styles)) {
if ($prepend === true) {
array_unshift(self::$styles, $path);
@@ -403,7 +403,7 @@ class OC_Util {
self::$styles[] = $path;
}
}
- } elseif ($type === "script") {
+ } elseif ($type === 'script') {
if (!in_array($path, self::$scripts)) {
if ($prepend === true) {
array_unshift(self::$scripts, $path);
@@ -991,7 +991,7 @@ class OC_Util {
* @return string the theme
*/
public static function getTheme() {
- $theme = \OC::$server->getSystemConfig()->getValue("theme", '');
+ $theme = \OC::$server->getSystemConfig()->getValue('theme', '');
if ($theme === '') {
if (is_dir(OC::$SERVERROOT . '/themes/default')) {
diff --git a/lib/private/legacy/template/functions.php b/lib/private/legacy/template/functions.php
index 84ada2aa6d8..87b91639fd3 100644
--- a/lib/private/legacy/template/functions.php
+++ b/lib/private/legacy/template/functions.php
@@ -102,7 +102,7 @@ function print_unescaped($string) {
*
* @param string $app the appname
* @param string|string[] $file the filename,
- * if an array is given it will add all scripts
+ * if an array is given it will add all scripts
*/
function script($app, $file = null) {
if (is_array($file)) {
@@ -118,7 +118,7 @@ function script($app, $file = null) {
* Shortcut for adding vendor scripts to a page
* @param string $app the appname
* @param string|string[] $file the filename,
- * if an array is given it will add all scripts
+ * if an array is given it will add all scripts
*/
function vendor_script($app, $file = null) {
if (is_array($file)) {
@@ -134,7 +134,7 @@ function vendor_script($app, $file = null) {
* Shortcut for adding styles to a page
* @param string $app the appname
* @param string|string[] $file the filename,
- * if an array is given it will add all styles
+ * if an array is given it will add all styles
*/
function style($app, $file = null) {
if (is_array($file)) {
@@ -150,7 +150,7 @@ function style($app, $file = null) {
* Shortcut for adding vendor styles to a page
* @param string $app the appname
* @param string|string[] $file the filename,
- * if an array is given it will add all styles
+ * if an array is given it will add all styles
*/
function vendor_style($app, $file = null) {
if (is_array($file)) {
@@ -165,7 +165,7 @@ function vendor_style($app, $file = null) {
/**
* Shortcut for adding translations to a page
* @param string $app the appname
- * if an array is given it will add all styles
+ * if an array is given it will add all styles
*/
function translation($app) {
OC_Util::addTranslations($app);
@@ -175,7 +175,7 @@ function translation($app) {
* Shortcut for HTML imports
* @param string $app the appname
* @param string|string[] $file the path relative to the app's component folder,
- * if an array is given it will add all components
+ * if an array is given it will add all components
*/
function component($app, $file) {
if (is_array($file)) {