From 5a520e306de8fe6cf5ec3a8be6e8863e8da483a4 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Thu, 23 Jul 2026 08:57:46 +0200 Subject: [PATCH 1/7] feat(files_sharing): Add LegacyBackend for Sharing Signed-off-by: provokateurin --- .../composer/composer/autoload_classmap.php | 2 + .../composer/composer/autoload_static.php | 2 + .../files_sharing/lib/AppInfo/Application.php | 18 + .../Version35000Date20260720121254.php | 34 + .../lib/Sharing/LegacyBackend.php | 513 ++++++++++++++ .../tests/Sharing/LegacyBackendTest.php | 659 ++++++++++++++++++ build/rector-strict.php | 2 + psalm-strict.xml | 2 + 8 files changed, 1232 insertions(+) create mode 100644 apps/files_sharing/lib/Migration/Version35000Date20260720121254.php create mode 100644 apps/files_sharing/lib/Sharing/LegacyBackend.php create mode 100644 apps/files_sharing/tests/Sharing/LegacyBackendTest.php diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php index 36aa12de200aa..b9aad64edd185 100644 --- a/apps/files_sharing/composer/composer/autoload_classmap.php +++ b/apps/files_sharing/composer/composer/autoload_classmap.php @@ -93,6 +93,7 @@ 'OCA\\Files_Sharing\\Migration\\Version33000Date20251030081948' => $baseDir . '/../lib/Migration/Version33000Date20251030081948.php', 'OCA\\Files_Sharing\\Migration\\Version33000Date20260306120000' => $baseDir . '/../lib/Migration/Version33000Date20260306120000.php', 'OCA\\Files_Sharing\\Migration\\Version33000Date20260306150000' => $baseDir . '/../lib/Migration/Version33000Date20260306150000.php', + 'OCA\\Files_Sharing\\Migration\\Version35000Date20260720121254' => $baseDir . '/../lib/Migration/Version35000Date20260720121254.php', 'OCA\\Files_Sharing\\MountProvider' => $baseDir . '/../lib/MountProvider.php', 'OCA\\Files_Sharing\\Notification\\Listener' => $baseDir . '/../lib/Notification/Listener.php', 'OCA\\Files_Sharing\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php', @@ -107,6 +108,7 @@ 'OCA\\Files_Sharing\\SharedMount' => $baseDir . '/../lib/SharedMount.php', 'OCA\\Files_Sharing\\SharedStorage' => $baseDir . '/../lib/SharedStorage.php', 'OCA\\Files_Sharing\\SharesReminderJob' => $baseDir . '/../lib/SharesReminderJob.php', + 'OCA\\Files_Sharing\\Sharing\\LegacyBackend' => $baseDir . '/../lib/Sharing/LegacyBackend.php', 'OCA\\Files_Sharing\\Updater' => $baseDir . '/../lib/Updater.php', 'OCA\\Files_Sharing\\ViewOnly' => $baseDir . '/../lib/ViewOnly.php', ); diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php index 0550359c85807..cf671c5fa5964 100644 --- a/apps/files_sharing/composer/composer/autoload_static.php +++ b/apps/files_sharing/composer/composer/autoload_static.php @@ -108,6 +108,7 @@ class ComposerStaticInitFiles_Sharing 'OCA\\Files_Sharing\\Migration\\Version33000Date20251030081948' => __DIR__ . '/..' . '/../lib/Migration/Version33000Date20251030081948.php', 'OCA\\Files_Sharing\\Migration\\Version33000Date20260306120000' => __DIR__ . '/..' . '/../lib/Migration/Version33000Date20260306120000.php', 'OCA\\Files_Sharing\\Migration\\Version33000Date20260306150000' => __DIR__ . '/..' . '/../lib/Migration/Version33000Date20260306150000.php', + 'OCA\\Files_Sharing\\Migration\\Version35000Date20260720121254' => __DIR__ . '/..' . '/../lib/Migration/Version35000Date20260720121254.php', 'OCA\\Files_Sharing\\MountProvider' => __DIR__ . '/..' . '/../lib/MountProvider.php', 'OCA\\Files_Sharing\\Notification\\Listener' => __DIR__ . '/..' . '/../lib/Notification/Listener.php', 'OCA\\Files_Sharing\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php', @@ -122,6 +123,7 @@ class ComposerStaticInitFiles_Sharing 'OCA\\Files_Sharing\\SharedMount' => __DIR__ . '/..' . '/../lib/SharedMount.php', 'OCA\\Files_Sharing\\SharedStorage' => __DIR__ . '/..' . '/../lib/SharedStorage.php', 'OCA\\Files_Sharing\\SharesReminderJob' => __DIR__ . '/..' . '/../lib/SharesReminderJob.php', + 'OCA\\Files_Sharing\\Sharing\\LegacyBackend' => __DIR__ . '/..' . '/../lib/Sharing/LegacyBackend.php', 'OCA\\Files_Sharing\\Updater' => __DIR__ . '/..' . '/../lib/Updater.php', 'OCA\\Files_Sharing\\ViewOnly' => __DIR__ . '/..' . '/../lib/ViewOnly.php', ); diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php index 580a3bb163d04..dd908449d9d74 100644 --- a/apps/files_sharing/lib/AppInfo/Application.php +++ b/apps/files_sharing/lib/AppInfo/Application.php @@ -36,6 +36,7 @@ use OCA\Files_Sharing\MountProvider; use OCA\Files_Sharing\Notification\Listener; use OCA\Files_Sharing\Notification\Notifier; +use OCA\Files_Sharing\Sharing\LegacyBackend; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -49,6 +50,7 @@ use OCP\Files\Events\BeforeZipCreatedEvent; use OCP\Files\Events\Node\BeforeNodeReadEvent; use OCP\Files\Events\UserHomeSetupEvent; +use OCP\Files\IRootFolder; use OCP\Group\Events\BeforeGroupDeletedEvent; use OCP\Group\Events\GroupChangedEvent; use OCP\Group\Events\GroupDeletedEvent; @@ -58,10 +60,16 @@ use OCP\IDBConnection; use OCP\IGroup; use OCP\Interaction\RestrictInteractionEvent; +use OCP\L10N\IFactory; +use OCP\Server; use OCP\Share\Events\BeforeShareDeletedEvent; use OCP\Share\Events\ShareCreatedEvent; use OCP\Share\Events\ShareMovedEvent; use OCP\Share\Events\ShareTransferredEvent; +use OCP\Share\IManager; +use OCP\Sharing\ISharingManager; +use OCP\Sharing\ISharingRegistry; +use OCP\Snowflake\ISnowflakeGenerator; use OCP\User\Events\UserChangedEvent; use OCP\User\Events\UserDeletedEvent; use OCP\Util; @@ -136,6 +144,16 @@ function () use ($c) { $context->registerConfigLexicon(ConfigLexicon::class); $context->registerEventListener(RestrictInteractionEvent::class, RestrictInteractionListener::class); + + $registry = Server::get(ISharingRegistry::class); + $registry->registerLegacyBackend(new LegacyBackend( + Server::get(IFactory::class), + Server::get(IDBConnection::class), + Server::get(IRootFolder::class), + Server::get(IManager::class), + Server::get(ISnowflakeGenerator::class), + Server::get(ISharingManager::class), + )); } #[\Override] diff --git a/apps/files_sharing/lib/Migration/Version35000Date20260720121254.php b/apps/files_sharing/lib/Migration/Version35000Date20260720121254.php new file mode 100644 index 0000000000000..8bdb2bab7083b --- /dev/null +++ b/apps/files_sharing/lib/Migration/Version35000Date20260720121254.php @@ -0,0 +1,34 @@ +createTable('share_legacy_mapping'); + $shareLegacyMappingTable->addColumn('id', Types::BIGINT); + $shareLegacyMappingTable->addColumn('legacy_provider', Types::STRING, ['length' => 128]); + $shareLegacyMappingTable->addColumn('legacy_id', Types::STRING, ['length' => 128]); + $shareLegacyMappingTable->setPrimaryKey(['id']); + $shareLegacyMappingTable->addUniqueIndex(['legacy_provider', 'legacy_id']); + + return null; + } +} diff --git a/apps/files_sharing/lib/Sharing/LegacyBackend.php b/apps/files_sharing/lib/Sharing/LegacyBackend.php new file mode 100644 index 0000000000000..43e9a2fd451f0 --- /dev/null +++ b/apps/files_sharing/lib/Sharing/LegacyBackend.php @@ -0,0 +1,513 @@ +getLegacyShares($share->id); + + /** @var array, array>> $legacyShareMapping */ + $legacyShareMapping = []; + foreach ($legacyShares as $legacyShare) { + $recipientTypeClass = $this->legacyShareTypeToRecipientTypeClass($legacyShare->getShareType()); + $sharedWith = $legacyShare->getSharedWith(); + $legacyNodeId = (string)$legacyShare->getNodeId(); + + $legacyShareMapping[$recipientTypeClass] ??= []; + $legacyShareMapping[$recipientTypeClass][$sharedWith] ??= []; + $legacyShareMapping[$recipientTypeClass][$sharedWith][$legacyNodeId] = $legacyShare; + } + + /** @var array $validLegacyShares */ + $validLegacyShares = []; + /** @var array $updatedLegacyShares */ + $updatedLegacyShares = []; + foreach ($share->recipients as $recipient) { + $legacyShareType = $this->recipientTypeClassToLegacyShareType($recipient->class, $recipient->instance); + foreach ($share->sources as $source) { + $create = false; + if (($legacyShare = $legacyShareMapping[$recipient->class][$recipient->value][$source->value] ?? null) === null) { + $legacyShare = $this->legacyManager->newShare(); + $legacyShare->setShareType($legacyShareType); + $legacyShare->setNodeId((int)$source->value); + // TODO: Support federation + if ($recipient->class !== TokenShareRecipientType::class) { + $legacyShare->setSharedWith($recipient->value); + } + + $this->setCommonFields($share, $legacyShare); + $create = true; + } + + $update = false; + + // TODO: Support federation + if ($recipient->initiator !== null && $legacyShare->getSharedBy() !== $recipient->initiator->userId) { + $legacyShare->setSharedBy($recipient->initiator->userId); + $update = true; + } + + [$permissions, $allowDownload] = $this->permissionsToLegacyPermission($source, array_keys($share->getEnabledPermissions())); + if ($legacyShare->getPermissions() !== $permissions) { + $legacyShare->setPermissions($permissions); + $update = true; + } + + if ($legacyShare->getShareType() === IShare::TYPE_LINK || $legacyShare->getShareType() === IShare::TYPE_EMAIL) { + if ($legacyShare->getHideDownload() !== !$allowDownload) { + $legacyShare->setHideDownload(!$allowDownload); + $update = true; + } + } else { + $attributes = $legacyShare->getAttributes() ?? $legacyShare->newAttributes(); + if ($attributes->getAttribute('permissions', 'download') !== !$allowDownload) { + $attributes->setAttribute('permissions', 'download', true); + $legacyShare->setAttributes($attributes); + $update = true; + } + } + + $token = $recipient->secret ?? ''; + if (in_array($recipient->class, [EmailShareRecipientType::class, TokenShareRecipientType::class], true) && $legacyShare->getToken() !== $token) { + $legacyShare->setToken($token); + $update = true; + } + + if ($create) { + $legacyShare = $this->legacyManager->createShare($legacyShare); + $this->addLegacyFullId($share->id, explode(':', $legacyShare->getFullId())[0], $legacyShare->getId()); + $legacyShares[$legacyShare->getFullId()] = $legacyShare; + } elseif ($update) { + $updatedLegacyShares[$legacyShare->getFullId()] = true; + $legacyShares[$legacyShare->getFullId()] = $legacyShare; + } + + $validLegacyShares[$legacyShare->getFullId()] = true; + } + } + + $invalidLegacyShares = array_diff(array_keys($legacyShares), array_keys($validLegacyShares)); + foreach ($invalidLegacyShares as $invalidLegacyShareId) { + $this->deleteShare($invalidLegacyShareId); + unset($legacyShares[$invalidLegacyShareId]); + } + + foreach ($legacyShares as $legacyShare) { + $update = $this->setCommonFields($share, $legacyShare); + + if ($update || ($updatedLegacyShares[$legacyShare->getFullId()] ?? false)) { + $this->legacyManager->updateShare($legacyShare, false); + } + } + } + + private function setCommonFields(Share $share, IShare $legacyShare): bool { + $update = false; + + // TODO: Support federation + if ($legacyShare->getShareOwner() !== $share->owner->userId) { + $legacyShare->setShareOwner($share->owner->userId); + $update = true; + } + + // TODO: Implement accept/reject mechanism + $status = IShare::STATUS_ACCEPTED; + if ($legacyShare->getShareType() === IShare::TYPE_USER && $legacyShare->getStatus() !== $status) { + $legacyShare->setStatus($status); + $update = true; + } + + $note = $share->properties[NoteSharePropertyType::class]?->value ?? ''; + if ($legacyShare->getNote() !== $note) { + $legacyShare->setNote($note); + $update = true; + } + + $label = $share->properties[LabelSharePropertyType::class]?->value ?? ''; + if (($legacyShare->getShareType() === IShare::TYPE_LINK || $legacyShare->getShareType() === IShare::TYPE_EMAIL) && $legacyShare->getLabel() !== $label) { + $legacyShare->setLabel($label); + $update = true; + } + + $expirationDateProperty = $share->properties[ExpirationDateSharePropertyType::class] ?? null; + $expirationDate = $expirationDateProperty?->value; + if ($expirationDate !== null) { + $expirationDate = DateTime::createFromFormat(DateTimeInterface::ATOM, $expirationDate); + } + + if ($legacyShare->getExpirationDate()?->getTimestamp() !== $expirationDate?->getTimestamp()) { + $legacyShare->setExpirationDate($expirationDate); + $update = true; + } + + // The value and applying a default value is already handled by Unified Sharing. + if ($expirationDate === null && !$legacyShare->getNoExpirationDate()) { + $legacyShare->setNoExpirationDate(true); + $update = true; + } + + // TODO: Update details that are the same for all legacy shares + + return $update; + } + + /** + * @param list> $permissions + * @return array{int-mask-of, bool} + */ + private function permissionsToLegacyPermission(ShareSource $source, array $permissions): array { + $node = $this->rootFolder->getFirstNodeById((int)$source->value); + if (!$node instanceof Node) { + throw new RuntimeException('Share source does not exist: ' . $source->value); + } + + $nodeIsFile = $node instanceof File; + + /** @var int-mask-of $legacyPermissions */ + $legacyPermissions = 0; + $allowDownload = false; + + if (in_array(NodeReadSharePermissionType::class, $permissions, true)) { + $legacyPermissions |= Constants::PERMISSION_READ; + } + + if (in_array(NodeUpdateSharePermissionType::class, $permissions, true)) { + $legacyPermissions |= Constants::PERMISSION_UPDATE; + } + + if (!$nodeIsFile && in_array(NodeCreateSharePermissionType::class, $permissions, true)) { + $legacyPermissions |= Constants::PERMISSION_CREATE; + } + + if (!$nodeIsFile && in_array(NodeDeleteSharePermissionType::class, $permissions, true)) { + $legacyPermissions |= Constants::PERMISSION_DELETE; + } + + if (in_array(ReshareSharePermissionType::class, $permissions, true)) { + $legacyPermissions |= Constants::PERMISSION_SHARE; + } + + if (in_array(NodeDownloadSharePermissionType::class, $permissions, true)) { + $allowDownload = true; + } + + return [$legacyPermissions, $allowDownload]; + } + + /** + * @param int-mask-of $legacyPermissions + * @return list> + */ + private function legacyPermissionsToPermissions(int $legacyPermissions, bool $allowDownload): array { + $permissions = []; + + foreach ([ + NodeReadSharePermissionType::class => Constants::PERMISSION_READ, + NodeUpdateSharePermissionType::class => Constants::PERMISSION_UPDATE, + NodeCreateSharePermissionType::class => Constants::PERMISSION_CREATE, + NodeDeleteSharePermissionType::class => Constants::PERMISSION_DELETE, + ReshareSharePermissionType::class => Constants::PERMISSION_SHARE, + ] as $permissionTypeClass => $mask) { + if (($legacyPermissions & $mask) === $mask) { + $permissions[] = $permissionTypeClass; + } + } + + if ($allowDownload) { + $permissions[] = NodeDownloadSharePermissionType::class; + } + + return $permissions; + } + + #[\Override] + public function deleteShare(string $id): void { + foreach ($this->getLegacyFullIds($id) as $legacyShareId) { + try { + $this->legacyManager->deleteShare($this->legacyManager->getShareById($legacyShareId, null, false)); + $this->removeLegacyFullId($legacyShareId); + } catch (ShareNotFound) { + throw new ShareNotFoundException(); + } + } + } + + #[\Override] + public function getShare(string $id): Share { + $legacyShareIds = $this->getLegacyFullIds($id); + if ($legacyShareIds === []) { + throw new ShareNotFoundException(); + } + + $sources = []; + $recipients = []; + + $legacyShares = array_map(fn (string $legacyShareId): IShare => $this->legacyManager->getShareById($legacyShareId, null, false), $legacyShareIds); + + foreach ($legacyShares as $legacyShare) { + $nodeId = $legacyShare->getNodeId(); + $sources[$nodeId] ??= new ShareSource(NodeShareSourceType::class, (string)$nodeId); + + $recipientTypeClass = $this->legacyShareTypeToRecipientTypeClass($legacyShare->getShareType()); + $isTokenRecipient = $recipientTypeClass === TokenShareRecipientType::class; + /** @psalm-suppress ArgumentTypeCoercion */ + $recipients[] = new ShareRecipient( + $recipientTypeClass, + $isTokenRecipient ? '' : $legacyShare->getSharedWith(), + // TODO: Support federation + null, + $isTokenRecipient ? $legacyShare->getToken() : $this->sharingManager->generateSecret(), + new ShareUser( + $legacyShare->getSharedBy(), + // TODO: Support federation + null, + ), + ); + + // TODO: Support per-recipient permissions + } + + /** @psalm-suppress ArgumentTypeCoercion */ + $owner = new ShareUser( + $legacyShares[0]->getShareOwner(), + // TODO: Support federation + null, + ); + + // TODO + $properties = []; + + // TODO: Support per-recipient permissions + $allowDownload = $legacyShares[0]->getShareType() === IShare::TYPE_LINK || $legacyShares[0]->getShareType() === IShare::TYPE_EMAIL + ? !$legacyShares[0]->getHideDownload() + : $legacyShares[0]->getAttributes()?->getAttribute('permissions', 'download') === true; + $enabledPermissions = $this->legacyPermissionsToPermissions($legacyShares[0]->getPermissions(), $allowDownload); + $permissions = []; + foreach ([ + NodeReadSharePermissionType::class, + NodeUpdateSharePermissionType::class, + NodeCreateSharePermissionType::class, + NodeDeleteSharePermissionType::class, + ReshareSharePermissionType::class, + NodeDownloadSharePermissionType::class, + ] as $permissionTypeClass) { + $permissions[$permissionTypeClass] = new SharePermission($permissionTypeClass, in_array($permissionTypeClass, $enabledPermissions, true)); + } + + /** @psalm-suppress ArgumentTypeCoercion */ + return new Share( + $id, + $owner, + // TODO + 0, + // TODO + ShareState::Active, + array_values($sources), + $recipients, + $properties, + $permissions, + ); + } + + #[\Override] + public function getUnmappedShares(?IUser $user): array { + // TODO: Make it work with all providers + // TODO: Filter by user + $qb = $this->connection->getQueryBuilder(); + $result = $qb + ->select('s.id') + ->from('share', 's') + ->leftJoin('s', 'share_legacy_mapping', 'l', $qb->expr()->eq('s.id', 'l.legacy_id')) + ->where($qb->expr()->isNull('l.legacy_id')) + ->andWhere($qb->expr()->in('s.share_type', $qb->createNamedParameter([ + IShare::TYPE_USER, + IShare::TYPE_REMOTE, + IShare::TYPE_GROUP, + IShare::TYPE_REMOTE_GROUP, + IShare::TYPE_LINK, + IShare::TYPE_EMAIL, + IShare::TYPE_CIRCLE, + ], IQueryBuilder::PARAM_INT_ARRAY))) + ->executeQuery(); + + /** @var list $legacyIds */ + $legacyIds = $result->fetchFirstColumn(); + if ($legacyIds === []) { + return []; + } + + $ids = []; + foreach ($legacyIds as $legacyId) { + $id = $this->snowflakeGenerator->nextId(); + $this->addLegacyFullId($id, 'ocinternal', (string)$legacyId); + $ids[] = $id; + } + + return array_map($this->getShare(...), $ids); + } + + /** + * @return list + */ + private function getLegacyFullIds(string $id): array { + $qb = $this->connection->getQueryBuilder(); + $result = $qb + ->select('legacy_provider', 'legacy_id') + ->from('share_legacy_mapping') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->executeQuery(); + + /** @var list $rows */ + $rows = $result->fetchAllAssociative(); + + return array_map(static fn (array $row): string => $row['legacy_provider'] . ':' . $row['legacy_id'], $rows); + } + + /** + * @return array + */ + private function getLegacyShares(string $id): array { + $legacyIds = $this->getLegacyFullIds($id); + + $shares = []; + foreach ($legacyIds as $legacyFullId) { + try { + $shares[$legacyFullId] = $this->legacyManager->getShareById($legacyFullId, null, false); + } catch (ShareNotFound) { + $this->removeLegacyFullId($legacyFullId); + } + } + + return $shares; + } + + private function removeLegacyFullId(string $legacyFullId): void { + [$providerId, $shareId] = explode(':', $legacyFullId); + + $qb = $this->connection->getQueryBuilder(); + $qb + ->delete('share_legacy_mapping') + ->where($qb->expr()->eq('legacy_provder', $qb->createNamedParameter($providerId))) + ->where($qb->expr()->eq('legacy_id', $qb->createNamedParameter($shareId))) + ->executeStatement(); + } + + private function addLegacyFullId(string $id, string $legacyProvider, string $legacyId): void { + $qb = $this->connection->getQueryBuilder(); + $qb + ->insert('share_legacy_mapping') + ->values([ + 'id' => $qb->createNamedParameter($id), + 'legacy_provider' => $qb->createNamedParameter($legacyProvider), + 'legacy_id' => $qb->createNamedParameter($legacyId), + ]) + ->executeStatement(); + } + + /** + * @param IShare::TYPE_* $legacyShareType + * @return class-string + */ + private function legacyShareTypeToRecipientTypeClass(int $legacyShareType): string { + return match ($legacyShareType) { + IShare::TYPE_USER, IShare::TYPE_REMOTE => UserShareRecipientType::class, + IShare::TYPE_GROUP, IShare::TYPE_REMOTE_GROUP => GroupShareRecipientType::class, + IShare::TYPE_LINK => TokenShareRecipientType::class, + IShare::TYPE_EMAIL => EmailShareRecipientType::class, + IShare::TYPE_CIRCLE => TeamShareRecipientType::class, + default => throw new RuntimeException('Unsupported legacy share type: ' . $legacyShareType), + }; + } + + /** + * @param class-string $recipientTypeClass + * @return IShare::TYPE_* + */ + private function recipientTypeClassToLegacyShareType(string $recipientTypeClass, ?string $instance): int { + return match ($recipientTypeClass) { + UserShareRecipientType::class => $instance === null ? IShare::TYPE_USER : IShare::TYPE_REMOTE, + GroupShareRecipientType::class => $instance === null ? IShare::TYPE_GROUP : IShare::TYPE_REMOTE_GROUP, + TokenShareRecipientType::class => IShare::TYPE_LINK, + EmailShareRecipientType::class => IShare::TYPE_EMAIL, + TeamShareRecipientType::class => IShare::TYPE_CIRCLE, + default => throw new RuntimeException('Unsupported recipient type: ' . $recipientTypeClass), + }; + } +} diff --git a/apps/files_sharing/tests/Sharing/LegacyBackendTest.php b/apps/files_sharing/tests/Sharing/LegacyBackendTest.php new file mode 100644 index 0000000000000..946aded209fd2 --- /dev/null +++ b/apps/files_sharing/tests/Sharing/LegacyBackendTest.php @@ -0,0 +1,659 @@ +legacyManager = Server::get(IManager::class); + $this->dbConnection = Server::get(IDBConnection::class); + + $this->legacyBackend = new LegacyBackend( + Server::get(IFactory::class), + $this->dbConnection, + Server::get(IRootFolder::class), + $this->legacyManager, + Server::get(ISnowflakeGenerator::class), + Server::get(ISharingManager::class), + ); + } + + #[\Override] + protected function tearDown(): void { + foreach ([ + 'share', + 'share_external', + 'share_legacy_mapping', + ] as $table) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb + ->select($qb->func()->count('*')) + ->from($table); + $this->assertEquals(0, $qb->executeQuery()->fetchOne(), $table); + } + + parent::tearDown(); + } + + public function testUpdateShare(): void { + $userManager = Server::get(IUserManager::class); + $groupManager = Server::get(IGroupManager::class); + + $owner = $userManager->createUser('owner', 'password'); + $this->assertNotFalse($owner); + $this->assertTrue($owner->setDisplayName('Owner')); + + $user = $userManager->createUser('user', 'password'); + $this->assertNotFalse($user); + $this->assertTrue($user->setDisplayName('User')); + + $group = $groupManager->createGroup('group'); + $this->assertNotNull($group); + $this->assertTrue($group->setDisplayName('Group')); + // Clear display name cache, because setting the display name on the group doesn't update it in the cache of the manager + self::invokePrivate(self::invokePrivate($groupManager, 'displayNameCache'), 'clear'); + + $circle = null; + if (class_exists(CirclesManager::class)) { + $circlesManager = Server::get(CirclesManager::class); + $circlesManager->startSession($circlesManager->getLocalFederatedUser($owner->getUID())); + /** @psalm-suppress MixedAssignment */ + $circle = $circlesManager->createCircle('circle'); + /** @psalm-suppress MixedMethodCall, UndefinedClass */ + Server::get(CircleService::class)->updateName($circle->getSingleId(), 'Circle'); + /** @psalm-suppress MixedMethodCall, MixedAssignment */ + $circle = $circlesManager->getCircle($circle->getSingleId()); + } + + $node1 = Server::get(IRootFolder::class)->getUserFolder($owner->getUID())->newFolder('foo'); + + $node2 = Server::get(IRootFolder::class)->getUserFolder($owner->getUID())->newFile('foo.txt'); + + // TODO: Accept arbitrary expiration dates. + $expirationDate = (new DateTimeImmutable())->add(new DateInterval('P1D'))->setTime(23, 59, 59)->format(DateTimeInterface::ATOM); + + // TODO: Test federated owner. + /** @psalm-suppress MixedMethodCall, MixedArgument */ + $share = new Share( + '123', + new ShareUser( + $owner->getUID(), + null, + ), + 0, + ShareState::Active, + [ + new ShareSource( + NodeShareSourceType::class, + (string)$node1->getId(), + ), + new ShareSource( + NodeShareSourceType::class, + (string)$node2->getId(), + ), + ], + array_merge( + [ + // TODO: Test federation + new ShareRecipient( + UserShareRecipientType::class, + $user->getUID(), + null, + 'secret', + new ShareUser( + $owner->getUID(), + null, + ), + ), + // TODO: Test federation + new ShareRecipient( + GroupShareRecipientType::class, + $group->getGID(), + null, + 'secret', + new ShareUser( + $owner->getUID(), + null, + ), + ), + new ShareRecipient( + TokenShareRecipientType::class, + 'token', + null, + 'secret', + new ShareUser( + $owner->getUID(), + null, + ), + ), + new ShareRecipient( + EmailShareRecipientType::class, + 'example@example.com', + null, + 'secret', + new ShareUser( + $owner->getUID(), + null, + ), + ), + ], + $circle !== null ? [ + new ShareRecipient( + TeamShareRecipientType::class, + $circle->getSingleId(), + null, + 'secret', + new ShareUser( + $owner->getUID(), + null, + ), + ), + ] : [], + ), + [ + NoteSharePropertyType::class => new ShareProperty( + NoteSharePropertyType::class, + 'Note', + ), + ExpirationDateSharePropertyType::class => new ShareProperty( + ExpirationDateSharePropertyType::class, + $expirationDate, + ), + LabelSharePropertyType::class => new ShareProperty( + LabelSharePropertyType::class, + 'Label', + ), + // TODO: Test all property types + ], + [ + NodeReadSharePermissionType::class => new SharePermission( + NodeReadSharePermissionType::class, + true, + ), + NodeUpdateSharePermissionType::class => new SharePermission( + NodeUpdateSharePermissionType::class, + true, + ), + NodeCreateSharePermissionType::class => new SharePermission( + NodeCreateSharePermissionType::class, + true, + ), + NodeDeleteSharePermissionType::class => new SharePermission( + NodeDeleteSharePermissionType::class, + true, + ), + ReshareSharePermissionType::class => new SharePermission( + ReshareSharePermissionType::class, + true, + ), + NodeDownloadSharePermissionType::class => new SharePermission( + NodeDownloadSharePermissionType::class, + true, + ), + ], + ); + + $this->legacyBackend->updateShare($share); + + $legacyIds = $this->getLegacyIds($share->id); + usort($legacyIds, static fn (string $a, string $b): int => explode(':', $a)[1] <=> explode(':', $b)[1]); + $legacyShares = array_map(fn (string $legacyId): array => $this->formatLegacyShare($this->legacyManager->getShareById($legacyId)), $legacyIds); + /** @psalm-suppress MixedMethodCall, MixedOperand */ + $this->assertEquals(array_merge( + [ + [ + 'id' => explode(':', $legacyIds[0])[1], + 'full_id' => $legacyIds[0], + 'node_id' => $node1->getId(), + 'node_type' => 'folder', + 'share_type' => IShare::TYPE_USER, + 'shared_with' => $user->getUID(), + 'shared_with_display_name' => $user->getDisplayName(), + 'shared_with_avatar' => null, + 'permissions' => Constants::PERMISSION_ALL, + 'attributes' => [ + [ + 'scope' => 'permissions', + 'key' => 'download', + 'value' => true, + ] + ], + 'status' => IShare::STATUS_ACCEPTED, + 'note' => 'Note', + 'expiration_date' => $expirationDate, + 'no_expiration_date' => false, + 'label' => '', + 'shared_by' => 'owner', + 'share_owner' => 'owner', + 'password' => null, + 'password_expiration_time' => null, + 'send_password_by_talk' => false, + 'token' => null, + 'parent' => null, + 'original_target' => null, + 'target' => '/foo', + 'mail_send' => false, + 'hide_download' => false, + 'reminder_sent' => false, + ], + [ + 'id' => explode(':', $legacyIds[1])[1], + 'full_id' => $legacyIds[1], + 'node_id' => $node2->getId(), + 'node_type' => 'file', + 'share_type' => IShare::TYPE_USER, + 'shared_with' => $user->getUID(), + 'shared_with_display_name' => $user->getDisplayName(), + 'shared_with_avatar' => null, + 'permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_CREATE & ~Constants::PERMISSION_DELETE, + 'attributes' => [ + [ + 'scope' => 'permissions', + 'key' => 'download', + 'value' => true, + ] + ], + 'status' => IShare::STATUS_ACCEPTED, + 'note' => 'Note', + 'expiration_date' => $expirationDate, + 'no_expiration_date' => false, + 'label' => '', + 'shared_by' => 'owner', + 'share_owner' => 'owner', + 'password' => null, + 'password_expiration_time' => null, + 'send_password_by_talk' => false, + 'token' => null, + 'parent' => null, + 'original_target' => null, + 'target' => '/foo.txt', + 'mail_send' => false, + 'hide_download' => false, + 'reminder_sent' => false, + ], + [ + 'id' => explode(':', $legacyIds[2])[1], + 'full_id' => $legacyIds[2], + 'node_id' => $node1->getId(), + 'node_type' => 'folder', + 'share_type' => IShare::TYPE_GROUP, + 'shared_with' => $group->getGID(), + 'shared_with_display_name' => $group->getDisplayName(), + 'shared_with_avatar' => null, + 'permissions' => Constants::PERMISSION_ALL, + 'attributes' => [ + [ + 'scope' => 'permissions', + 'key' => 'download', + 'value' => true, + ] + ], + 'status' => IShare::STATUS_PENDING, + 'note' => 'Note', + 'expiration_date' => $expirationDate, + 'no_expiration_date' => false, + 'label' => '', + 'shared_by' => 'owner', + 'share_owner' => 'owner', + 'password' => null, + 'password_expiration_time' => null, + 'send_password_by_talk' => false, + 'token' => null, + 'parent' => null, + 'original_target' => null, + 'target' => '/foo', + 'mail_send' => false, + 'hide_download' => false, + 'reminder_sent' => false, + ], + [ + 'id' => explode(':', $legacyIds[3])[1], + 'full_id' => $legacyIds[3], + 'node_id' => $node2->getId(), + 'node_type' => 'file', + 'share_type' => IShare::TYPE_GROUP, + 'shared_with' => $group->getGID(), + 'shared_with_display_name' => $group->getDisplayName(), + 'shared_with_avatar' => null, + 'permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_CREATE & ~Constants::PERMISSION_DELETE, + 'attributes' => [ + [ + 'scope' => 'permissions', + 'key' => 'download', + 'value' => true, + ] + ], + 'status' => IShare::STATUS_PENDING, + 'note' => 'Note', + 'expiration_date' => $expirationDate, + 'no_expiration_date' => false, + 'label' => '', + 'shared_by' => 'owner', + 'share_owner' => 'owner', + 'password' => null, + 'password_expiration_time' => null, + 'send_password_by_talk' => false, + 'token' => null, + 'parent' => null, + 'original_target' => null, + 'target' => '/foo.txt', + 'mail_send' => false, + 'hide_download' => false, + 'reminder_sent' => false, + ], + [ + 'id' => explode(':', $legacyIds[4])[1], + 'full_id' => $legacyIds[4], + 'node_id' => $node1->getId(), + 'node_type' => 'folder', + 'share_type' => IShare::TYPE_LINK, + 'shared_with' => null, + 'shared_with_display_name' => '', + 'shared_with_avatar' => null, + 'permissions' => Constants::PERMISSION_ALL, + 'attributes' => null, + 'status' => IShare::STATUS_PENDING, + 'note' => 'Note', + 'expiration_date' => $expirationDate, + 'no_expiration_date' => false, + 'label' => 'Label', + 'shared_by' => 'owner', + 'share_owner' => 'owner', + 'password' => null, + 'password_expiration_time' => null, + 'send_password_by_talk' => false, + 'token' => 'secret', + 'parent' => null, + 'original_target' => null, + 'target' => '/foo', + 'mail_send' => false, + 'hide_download' => false, + 'reminder_sent' => false, + ], + [ + 'id' => explode(':', $legacyIds[5])[1], + 'full_id' => $legacyIds[5], + 'node_id' => $node2->getId(), + 'node_type' => 'file', + 'share_type' => IShare::TYPE_LINK, + 'shared_with' => null, + 'shared_with_display_name' => '', + 'shared_with_avatar' => null, + 'permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_CREATE & ~Constants::PERMISSION_DELETE, + 'attributes' => null, + 'status' => IShare::STATUS_PENDING, + 'note' => 'Note', + 'expiration_date' => $expirationDate, + 'no_expiration_date' => false, + 'label' => 'Label', + 'shared_by' => 'owner', + 'share_owner' => 'owner', + 'password' => null, + 'password_expiration_time' => null, + 'send_password_by_talk' => false, + 'token' => 'secret', + 'parent' => null, + 'original_target' => null, + 'target' => '/foo.txt', + 'mail_send' => false, + 'hide_download' => false, + 'reminder_sent' => false, + ], + [ + 'id' => explode(':', $legacyIds[6])[1], + 'full_id' => $legacyIds[6], + 'node_id' => $node1->getId(), + 'node_type' => 'folder', + 'share_type' => IShare::TYPE_EMAIL, + 'shared_with' => 'example@example.com', + 'shared_with_display_name' => '', + 'shared_with_avatar' => null, + 'permissions' => Constants::PERMISSION_ALL, + 'attributes' => null, + 'status' => null, + 'note' => 'Note', + 'expiration_date' => $expirationDate, + 'no_expiration_date' => false, + 'label' => 'Label', + 'shared_by' => 'owner', + 'share_owner' => 'owner', + 'password' => null, + 'password_expiration_time' => null, + 'send_password_by_talk' => false, + 'token' => 'secret', + 'parent' => null, + 'original_target' => null, + 'target' => null, + 'mail_send' => false, + 'hide_download' => false, + 'reminder_sent' => false, + ], + [ + 'id' => explode(':', $legacyIds[7])[1], + 'full_id' => $legacyIds[7], + 'node_id' => $node2->getId(), + 'node_type' => 'file', + 'share_type' => IShare::TYPE_EMAIL, + 'shared_with' => 'example@example.com', + 'shared_with_display_name' => '', + 'shared_with_avatar' => null, + 'permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_CREATE & ~Constants::PERMISSION_DELETE, + 'attributes' => null, + 'status' => null, + 'note' => 'Note', + 'expiration_date' => $expirationDate, + 'no_expiration_date' => false, + 'label' => 'Label', + 'shared_by' => 'owner', + 'share_owner' => 'owner', + 'password' => null, + 'password_expiration_time' => null, + 'send_password_by_talk' => false, + 'token' => 'secret', + 'parent' => null, + 'original_target' => null, + 'target' => null, + 'mail_send' => false, + 'hide_download' => false, + 'reminder_sent' => false, + ], + ], + $circle !== null ? [ + [ + 'id' => explode(':', $legacyIds[8])[1], + 'full_id' => $legacyIds[8], + 'node_id' => $node1->getId(), + 'node_type' => 'folder', + 'share_type' => IShare::TYPE_CIRCLE, + 'shared_with' => $circle->getSingleId(), + 'shared_with_display_name' => $circle->getDisplayName() . ' (Team owned by ' . $owner->getDisplayName() . ')', + 'shared_with_avatar' => 'http://localhost/apps/circles/img/circles.svg', + 'permissions' => Constants::PERMISSION_ALL, + 'attributes' => [ + [ + 'scope' => 'permissions', + 'key' => 'download', + 'value' => true, + ] + ], + 'status' => IShare::STATUS_ACCEPTED, + 'note' => 'Note', + 'expiration_date' => $expirationDate, + 'no_expiration_date' => false, + 'label' => '', + 'shared_by' => 'owner', + 'share_owner' => 'owner', + 'password' => null, + 'password_expiration_time' => null, + 'send_password_by_talk' => false, + 'token' => '', + 'parent' => null, + 'original_target' => null, + 'target' => '/foo', + 'mail_send' => true, + 'hide_download' => false, + 'reminder_sent' => false, + ], + [ + 'id' => explode(':', $legacyIds[9])[1], + 'full_id' => $legacyIds[9], + 'node_id' => $node2->getId(), + 'node_type' => 'file', + 'share_type' => IShare::TYPE_CIRCLE, + 'shared_with' => $circle->getSingleId(), + 'shared_with_display_name' => $circle->getDisplayName() . ' (Team owned by ' . $owner->getDisplayName() . ')', + 'shared_with_avatar' => 'http://localhost/apps/circles/img/circles.svg', + 'permissions' => Constants::PERMISSION_ALL & ~Constants::PERMISSION_CREATE & ~Constants::PERMISSION_DELETE, + 'attributes' => [ + [ + 'scope' => 'permissions', + 'key' => 'download', + 'value' => true, + ] + ], + 'status' => IShare::STATUS_ACCEPTED, + 'note' => 'Note', + 'expiration_date' => $expirationDate, + 'no_expiration_date' => false, + 'label' => '', + 'shared_by' => 'owner', + 'share_owner' => 'owner', + 'password' => null, + 'password_expiration_time' => null, + 'send_password_by_talk' => false, + 'token' => '', + 'parent' => null, + 'original_target' => null, + 'target' => '/foo.txt', + 'mail_send' => true, + 'hide_download' => false, + 'reminder_sent' => false, + ], + ] : [], + ), $legacyShares); + + $this->legacyBackend->deleteShare($share->id); + if (class_exists(CirclesManager::class) && $circle !== null) { + $circlesManager = Server::get(CirclesManager::class); + $circlesManager->startSession($circlesManager->getLocalFederatedUser($owner->getUID())); + /** @psalm-suppress MixedMethodCall */ + $circlesManager->destroyCircle($circle->getSingleId()); + } + + $this->assertTrue($group->delete()); + $this->assertTrue($user->delete()); + $this->assertTrue($owner->delete()); + } + + /** + * @return array + */ + private function formatLegacyShare(IShare $legacyShare): array { + // Omitted, because it is not predictable + $this->assertGreaterThan(0, $legacyShare->getShareTime()->getTimestamp()); + + return [ + 'id' => $legacyShare->getId(), + 'full_id' => $legacyShare->getFullId(), + 'node_id' => $legacyShare->getNodeId(), + 'node_type' => $legacyShare->getNodeType(), + 'share_type' => $legacyShare->getShareType(), + 'shared_with' => $legacyShare->getSharedWith(), + 'shared_with_display_name' => $legacyShare->getSharedWithDisplayName(), + 'shared_with_avatar' => $legacyShare->getSharedWithAvatar(), + 'permissions' => $legacyShare->getPermissions(), + 'attributes' => $legacyShare->getAttributes()?->toArray(), + 'status' => $legacyShare->getStatus(), + 'note' => $legacyShare->getNote(), + 'expiration_date' => $legacyShare->getExpirationDate()?->format(DateTimeInterface::ATOM), + 'no_expiration_date' => $legacyShare->getNoExpirationDate(), + 'label' => $legacyShare->getLabel(), + 'shared_by' => $legacyShare->getSharedBy(), + 'share_owner' => $legacyShare->getShareOwner(), + 'password' => $legacyShare->getPassword(), + 'password_expiration_time' => $legacyShare->getPasswordExpirationTime()?->format(DateTimeInterface::ATOM), + 'send_password_by_talk' => $legacyShare->getSendPasswordByTalk(), + 'token' => $legacyShare->getToken(), + 'parent' => $legacyShare->getParent(), + 'original_target' => $legacyShare->getOriginalTarget(), + 'target' => $legacyShare->getTarget(), + 'mail_send' => $legacyShare->getMailSend(), + 'hide_download' => $legacyShare->getHideDownload(), + 'reminder_sent' => $legacyShare->getReminderSent(), + ]; + } + + /** + * @return list + */ + private function getLegacyIds(string $id): array { + $qb = $this->dbConnection->getQueryBuilder(); + $result = $qb + ->select('legacy_id') + ->from('share_legacy_mapping') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->executeQuery(); + /** @var list $ids */ + $ids = $result->fetchFirstColumn(); + return $ids; + } +} diff --git a/build/rector-strict.php b/build/rector-strict.php index db161bfd07bb0..bc547d1f267d7 100644 --- a/build/rector-strict.php +++ b/build/rector-strict.php @@ -47,6 +47,8 @@ $nextcloudDir . '/tests/Core/Sharing', $nextcloudDir . '/apps/files/lib/Sharing', $nextcloudDir . '/apps/files/tests/Sharing', + $nextcloudDir . '/apps/files_sharing/lib/Sharing', + $nextcloudDir . '/apps/files_sharing/tests/Sharing', ]) ->withAutoloadPaths([ // ensure rector properly autoload the public interfaces diff --git a/psalm-strict.xml b/psalm-strict.xml index c1b5dc95b0a22..fffa842517d16 100644 --- a/psalm-strict.xml +++ b/psalm-strict.xml @@ -53,6 +53,8 @@ + + From edb7da80039aaa29094e3ae043a60ac1f2543a70 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Wed, 29 Jul 2026 11:32:55 +0200 Subject: [PATCH 2/7] wip Signed-off-by: provokateurin --- .../files_sharing/lib/AppInfo/Application.php | 4 +- .../lib/Sharing/LegacyBackend.php | 63 ++++-- .../tests/Sharing/LegacyBackendTest.php | 10 +- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + lib/private/Share20/Manager.php | 11 +- lib/private/Share20/Share.php | 9 + lib/private/Sharing/ISharingLegacyBackend.php | 22 +- lib/private/Sharing/SharingManager.php | 212 +++++++++++++++--- .../Share/Events/AfterShareModifiedEvent.php | 37 +++ lib/public/Share/IShare.php | 7 + lib/unstable/Sharing/ISharingManager.php | 12 + unified-sharing-check.sh | 10 + unified-sharing-test.sh | 9 + 14 files changed, 349 insertions(+), 59 deletions(-) create mode 100644 lib/public/Share/Events/AfterShareModifiedEvent.php create mode 100755 unified-sharing-check.sh create mode 100755 unified-sharing-test.sh diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php index dd908449d9d74..3c81efea6fee8 100644 --- a/apps/files_sharing/lib/AppInfo/Application.php +++ b/apps/files_sharing/lib/AppInfo/Application.php @@ -8,6 +8,8 @@ namespace OCA\Files_Sharing\AppInfo; +use NCU\Sharing\ISharingManager; +use NCU\Sharing\ISharingRegistry; use OC\Group\DisplayNameCache as GroupDisplayNameCache; use OC\User\DisplayNameCache; use OCA\Files\Event\LoadAdditionalScriptsEvent; @@ -67,8 +69,6 @@ use OCP\Share\Events\ShareMovedEvent; use OCP\Share\Events\ShareTransferredEvent; use OCP\Share\IManager; -use OCP\Sharing\ISharingManager; -use OCP\Sharing\ISharingRegistry; use OCP\Snowflake\ISnowflakeGenerator; use OCP\User\Events\UserChangedEvent; use OCP\User\Events\UserDeletedEvent; diff --git a/apps/files_sharing/lib/Sharing/LegacyBackend.php b/apps/files_sharing/lib/Sharing/LegacyBackend.php index 43e9a2fd451f0..c96ec46e53e19 100644 --- a/apps/files_sharing/lib/Sharing/LegacyBackend.php +++ b/apps/files_sharing/lib/Sharing/LegacyBackend.php @@ -11,6 +11,16 @@ use DateTime; use DateTimeInterface; +use NCU\Sharing\Exception\ShareNotFoundException; +use NCU\Sharing\ISharingManager; +use NCU\Sharing\Permission\ISharePermissionType; +use NCU\Sharing\Permission\SharePermission; +use NCU\Sharing\Recipient\IShareRecipientType; +use NCU\Sharing\Recipient\ShareRecipient; +use NCU\Sharing\Share; +use NCU\Sharing\ShareState; +use NCU\Sharing\ShareUser; +use NCU\Sharing\Source\ShareSource; use OC\Core\Sharing\Permission\ReshareSharePermissionType; use OC\Core\Sharing\Property\ExpirationDateSharePropertyType; use OC\Core\Sharing\Property\LabelSharePropertyType; @@ -20,6 +30,7 @@ use OC\Core\Sharing\Recipient\TeamShareRecipientType; use OC\Core\Sharing\Recipient\TokenShareRecipientType; use OC\Core\Sharing\Recipient\UserShareRecipientType; +use OC\Sharing\ISharingLegacyBackend; use OCA\Files\Sharing\Permission\NodeCreateSharePermissionType; use OCA\Files\Sharing\Permission\NodeDeleteSharePermissionType; use OCA\Files\Sharing\Permission\NodeDownloadSharePermissionType; @@ -37,17 +48,6 @@ use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; use OCP\Share\IShare; -use OCP\Sharing\Exception\ShareNotFoundException; -use OC\Sharing\ISharingLegacyBackend; -use OCP\Sharing\ISharingManager; -use OCP\Sharing\Permission\ISharePermissionType; -use OCP\Sharing\Permission\SharePermission; -use OCP\Sharing\Recipient\IShareRecipientType; -use OCP\Sharing\Recipient\ShareRecipient; -use OCP\Sharing\Share; -use OCP\Sharing\ShareState; -use OCP\Sharing\ShareUser; -use OCP\Sharing\Source\ShareSource; use OCP\Snowflake\ISnowflakeGenerator; use RuntimeException; @@ -152,7 +152,7 @@ public function updateShare(Share $share): void { if ($create) { $legacyShare = $this->legacyManager->createShare($legacyShare); - $this->addLegacyFullId($share->id, explode(':', $legacyShare->getFullId())[0], $legacyShare->getId()); + // No need to insert the legacy full id, because the listener in the SharingManager will already trigger this process. $legacyShares[$legacyShare->getFullId()] = $legacyShare; } elseif ($update) { $updatedLegacyShares[$legacyShare->getFullId()] = true; @@ -327,8 +327,10 @@ public function getShare(string $id): Share { $recipientTypeClass = $this->legacyShareTypeToRecipientTypeClass($legacyShare->getShareType()); $isTokenRecipient = $recipientTypeClass === TokenShareRecipientType::class; + $recipients[$recipientTypeClass] ??= []; + $uniqueId = $isTokenRecipient ? $legacyShare->getToken() : $legacyShare->getSharedWith(); /** @psalm-suppress ArgumentTypeCoercion */ - $recipients[] = new ShareRecipient( + $recipients[$recipientTypeClass][$uniqueId] ??= new ShareRecipient( $recipientTypeClass, $isTokenRecipient ? '' : $legacyShare->getSharedWith(), // TODO: Support federation @@ -380,14 +382,24 @@ public function getShare(string $id): Share { // TODO ShareState::Active, array_values($sources), - $recipients, + array_merge(...array_values($recipients)), $properties, $permissions, ); } #[\Override] - public function getUnmappedShares(?IUser $user): array { + public function getShareByLegacyProviderAndId(string $legacyProvider, string $legacyId): Share { + $id = $this->getId($legacyProvider, $legacyId); + if ($id === null) { + throw new ShareNotFoundException(); + } + + return $this->getShare($id); + } + + #[\Override] + public function getUnmappedShares(IUser $user): array { // TODO: Make it work with all providers // TODO: Filter by user $qb = $this->connection->getQueryBuilder(); @@ -426,7 +438,8 @@ public function getUnmappedShares(?IUser $user): array { /** * @return list */ - private function getLegacyFullIds(string $id): array { + #[\Override] + public function getLegacyFullIds(string $id): array { $qb = $this->connection->getQueryBuilder(); $result = $qb ->select('legacy_provider', 'legacy_id') @@ -440,6 +453,24 @@ private function getLegacyFullIds(string $id): array { return array_map(static fn (array $row): string => $row['legacy_provider'] . ':' . $row['legacy_id'], $rows); } + private function getId(string $legacyProvider, string $legacyId): ?string { + $qb = $this->connection->getQueryBuilder(); + $result = $qb + ->select('id') + ->from('share_legacy_mapping') + ->where($qb->expr()->eq('legacy_provider', $qb->createNamedParameter($legacyProvider))) + ->andWhere($qb->expr()->eq('legacy_id', $qb->createNamedParameter($legacyId))) + ->executeQuery(); + + /** @var int|false $id */ + $id = $result->fetchOne(); + if ($id === false) { + return null; + } + + return (string)$id; + } + /** * @return array */ diff --git a/apps/files_sharing/tests/Sharing/LegacyBackendTest.php b/apps/files_sharing/tests/Sharing/LegacyBackendTest.php index 946aded209fd2..7c56625e6434d 100644 --- a/apps/files_sharing/tests/Sharing/LegacyBackendTest.php +++ b/apps/files_sharing/tests/Sharing/LegacyBackendTest.php @@ -158,7 +158,7 @@ public function testUpdateShare(): void { UserShareRecipientType::class, $user->getUID(), null, - 'secret', + 'user secret', new ShareUser( $owner->getUID(), null, @@ -169,7 +169,7 @@ public function testUpdateShare(): void { GroupShareRecipientType::class, $group->getGID(), null, - 'secret', + 'group secret', new ShareUser( $owner->getUID(), null, @@ -179,7 +179,7 @@ public function testUpdateShare(): void { TokenShareRecipientType::class, 'token', null, - 'secret', + 'token secret', new ShareUser( $owner->getUID(), null, @@ -189,7 +189,7 @@ public function testUpdateShare(): void { EmailShareRecipientType::class, 'example@example.com', null, - 'secret', + 'email secret', new ShareUser( $owner->getUID(), null, @@ -201,7 +201,7 @@ public function testUpdateShare(): void { TeamShareRecipientType::class, $circle->getSingleId(), null, - 'secret', + 'team secret', new ShareUser( $owner->getUID(), null, diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 79825014fdffc..f661fe8152658 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -896,6 +896,7 @@ 'OCP\\SetupCheck\\ISetupCheckManager' => $baseDir . '/lib/public/SetupCheck/ISetupCheckManager.php', 'OCP\\SetupCheck\\SetupResult' => $baseDir . '/lib/public/SetupCheck/SetupResult.php', 'OCP\\Share' => $baseDir . '/lib/public/Share.php', + 'OCP\\Share\\Events\\AfterShareModifiedEvent' => $baseDir . '/lib/public/Share/Events/AfterShareModifiedEvent.php', 'OCP\\Share\\Events\\BeforeShareCreatedEvent' => $baseDir . '/lib/public/Share/Events/BeforeShareCreatedEvent.php', 'OCP\\Share\\Events\\BeforeShareDeletedEvent' => $baseDir . '/lib/public/Share/Events/BeforeShareDeletedEvent.php', 'OCP\\Share\\Events\\ShareAcceptedEvent' => $baseDir . '/lib/public/Share/Events/ShareAcceptedEvent.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 260349ab1fdac..422bf9ccaf828 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -937,6 +937,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\SetupCheck\\ISetupCheckManager' => __DIR__ . '/../../..' . '/lib/public/SetupCheck/ISetupCheckManager.php', 'OCP\\SetupCheck\\SetupResult' => __DIR__ . '/../../..' . '/lib/public/SetupCheck/SetupResult.php', 'OCP\\Share' => __DIR__ . '/../../..' . '/lib/public/Share.php', + 'OCP\\Share\\Events\\AfterShareModifiedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/AfterShareModifiedEvent.php', 'OCP\\Share\\Events\\BeforeShareCreatedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/BeforeShareCreatedEvent.php', 'OCP\\Share\\Events\\BeforeShareDeletedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/BeforeShareDeletedEvent.php', 'OCP\\Share\\Events\\ShareAcceptedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareAcceptedEvent.php', diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index f1b8dbc4ce97e..0278c5605230e 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -60,6 +60,7 @@ use OCP\Security\PasswordContext; use OCP\Server; use OCP\Share; +use OCP\Share\Events\AfterShareModifiedEvent; use OCP\Share\Events\BeforeShareCreatedEvent; use OCP\Share\Events\BeforeShareDeletedEvent; use OCP\Share\Events\ShareAcceptedEvent; @@ -642,6 +643,7 @@ public function createShare(IShare $share): IShare { $provider = $this->factory->getProviderForType($share->getShareType()); if ($provider instanceof IShareProviderWithNotification) { $provider->sendMailNotification($share); + $this->dispatcher->dispatchTyped(new AfterShareModifiedEvent($share)); } else { $this->logger->debug('Share notification not sent because the provider does not support it.', ['app' => 'share']); } @@ -753,6 +755,7 @@ public function updateShare(IShare $share, bool $onlyValid = true): IShare { } else { $share = $provider->update($share); } + $this->dispatcher->dispatchTyped(new AfterShareModifiedEvent($share)); if ($expirationDateUpdated === true) { \OC_Hook::emit(Share::class, 'post_set_expiration_date', [ @@ -1067,7 +1070,11 @@ public function restoreShare(IShare $share, string $recipientId): IShare { [$providerId,] = $this->splitFullId($share->getFullId()); $provider = $this->factory->getProvider($providerId); - return $provider->restore($share, $recipientId); + $provider->restore($share, $recipientId); + + $this->dispatcher->dispatchTyped(new AfterShareModifiedEvent($share)); + + return $share; } #[Override] @@ -1449,6 +1456,8 @@ public function checkPassword(IShare $share, ?string $password): bool { $share->setPassword($newHash); $provider = $this->factory->getProviderForType($share->getShareType()); $provider->update($share); + + $this->dispatcher->dispatchTyped(new AfterShareModifiedEvent($share)); } return true; diff --git a/lib/private/Share20/Share.php b/lib/private/Share20/Share.php index d88710e4e191f..e018c3324d936 100644 --- a/lib/private/Share20/Share.php +++ b/lib/private/Share20/Share.php @@ -120,6 +120,15 @@ public function setProviderId(string $id): self { return $this; } + #[Override] + public function getProviderId(): string { + if ($this->providerId === null) { + throw new \UnexpectedValueException; + } + + return $this->providerId; + } + #[Override] public function setNode(Node $node): self { $this->fileId = null; diff --git a/lib/private/Sharing/ISharingLegacyBackend.php b/lib/private/Sharing/ISharingLegacyBackend.php index 5aa9f45271e75..b87cd3bb5947a 100644 --- a/lib/private/Sharing/ISharingLegacyBackend.php +++ b/lib/private/Sharing/ISharingLegacyBackend.php @@ -12,8 +12,8 @@ use NCU\Sharing\Exception\ShareNotFoundException; use NCU\Sharing\Recipient\IShareRecipientType; use NCU\Sharing\Share; -use NCU\Sharing\ShareAccessContext; use NCU\Sharing\Source\IShareSourceType; +use OCP\IUser; /** * This interface is only temporary and implemented in the files_sharing app. @@ -46,14 +46,24 @@ public function deleteShare(string $id): void; * * @throws ShareNotFoundException */ - public function getShare(ShareAccessContext $accessContext, string $id): Share; + public function getShare(string $id): Share; /** - * Get multiple shares. + * Get a share by it's legacy provider and id. + * + * @throws ShareNotFoundException + */ + public function getShareByLegacyProviderAndId(string $legacyProvider, string $legacyId): Share; + + /** + * Get unmapped shares. * - * @param ?class-string $filterSourceTypeClass - * @param ?positive-int $limit * @return list */ - public function getShares(ShareAccessContext $accessContext, ?string $filterSourceTypeClass, ?string $filterSourceTypeValue, ?string $lastShareID, ?int $limit): array; + public function getUnmappedShares(IUser $user): array; + + /** + * @return list + */ + public function getLegacyFullIds(string $id): array; } diff --git a/lib/private/Sharing/SharingManager.php b/lib/private/Sharing/SharingManager.php index 76593a8f4bc0b..8415af37b8f01 100644 --- a/lib/private/Sharing/SharingManager.php +++ b/lib/private/Sharing/SharingManager.php @@ -11,6 +11,7 @@ use Exception; use NCU\Sharing\Exception\ShareInvalidException; +use NCU\Sharing\Exception\ShareNotFoundException; use NCU\Sharing\Exception\ShareOperationForbiddenException; use NCU\Sharing\ISharingBackend; use NCU\Sharing\ISharingManager; @@ -40,6 +41,13 @@ use OCP\IUserManager; use OCP\L10N\IFactory; use OCP\Security\ISecureRandom; +use OCP\Share\Events\AfterShareModifiedEvent; +use OCP\Share\Events\ShareAcceptedEvent; +use OCP\Share\Events\ShareCreatedEvent; +use OCP\Share\Events\ShareDeletedEvent; +use OCP\Share\Events\ShareDeletedFromSelfEvent; +use OCP\Share\Events\ShareMovedEvent; +use OCP\Share\IManager; use OCP\Snowflake\ISnowflakeGenerator; use OCP\User\Events\BeforeUserDeletedEvent; use Random\Randomizer; @@ -53,22 +61,26 @@ /** * @psalm-import-type SharingShare from Share - * @template-implements IEventListener + * @template-implements IEventListener */ -final readonly class SharingManager implements ISharingManager, IEventListener { - private Randomizer $randomizer; +final class SharingManager implements ISharingManager, IEventListener { + private readonly Randomizer $randomizer; - private IL10N $l10n; + private readonly IL10N $l10n; - private ISharingBackend $backend; + private readonly ISharingBackend $backend; + + /** @var array */ + private array $currentlyUpdatingLegacyShares; public function __construct( IEventDispatcher $eventDispatcher, - private IUserManager $userManager, - private IFactory $l10nFactory, - private ISnowflakeGenerator $snowflakeGenerator, - private IDBConnection $dbConnection, - private ISharingRegistry $registry, + private readonly IUserManager $userManager, + private readonly IFactory $l10nFactory, + private readonly ISnowflakeGenerator $snowflakeGenerator, + private readonly IDBConnection $dbConnection, + private readonly ISharingRegistry $registry, + private readonly IManager $legacyManager, IAppConfig $appConfig, ) { $this->randomizer = new Randomizer(); @@ -83,6 +95,14 @@ public function __construct( ); $eventDispatcher->addServiceListener(BeforeUserDeletedEvent::class, self::class); + $eventDispatcher->addServiceListener(AfterShareModifiedEvent::class, self::class); + $eventDispatcher->addServiceListener(ShareAcceptedEvent::class, self::class); + $eventDispatcher->addServiceListener(ShareCreatedEvent::class, self::class); + $eventDispatcher->addServiceListener(ShareDeletedEvent::class, self::class); + $eventDispatcher->addServiceListener(ShareDeletedFromSelfEvent::class, self::class); + $eventDispatcher->addServiceListener(ShareMovedEvent::class, self::class); + + $this->currentlyUpdatingLegacyShares = []; } #[\Override] @@ -617,22 +637,124 @@ public function getShare(ShareAccessContext $accessContext, string $id): Share { public function getShares(ShareAccessContext $accessContext, ?string $filterSourceTypeClass, ?string $filterSourceTypeValue, ?string $lastShareID, ?int $limit): array { $this->assertInTransaction(); + if (($currentUser = $accessContext->currentUser) instanceof IUser) { + $this->importSharesFromLegacyBackend($currentUser); + } + return $this->backend->getShares($accessContext, $filterSourceTypeClass, $filterSourceTypeValue, $lastShareID, $limit); } + #[\Override] + public function importSharesFromLegacyBackend(IUser $user): array { + $this->assertInTransaction(); + + $shareIds = []; + + $legacyBackend = $this->registry->getLegacyBackend(); + if ($legacyBackend instanceof ISharingLegacyBackend) { + foreach ($legacyBackend->getUnmappedShares($user) as $unmappedShare) { + $this->updateShare($unmappedShare); + $shareIds[] = $unmappedShare->id; + } + } + + return $shareIds; + } + + #[\Override] + public function exportShareToLegacyBackend(string $id): void { + $this->assertInTransaction(); + + $share = $this->getShare(new ShareAccessContext(overrideChecks: true), $id); + + $this->updateShareInLegacyBackend($share); + } + #[\Override] public function handle(Event $event): void { - $shareUser = new ShareUser($event->getUser()->getUID(), null); + if ($event instanceof BeforeUserDeletedEvent) { + $shareUser = new ShareUser($event->getUser()->getUID(), null); + + try { + $this->dbConnection->beginTransaction(); + $this->onOwnerDeleted(new ShareAccessContext(overrideChecks: true), $shareUser); + $this->onInitiatorDeleted(new ShareAccessContext(overrideChecks: true), $shareUser); + $this->dbConnection->commit(); + } catch (Exception $exception) { + $this->dbConnection->rollBack(); + throw $exception; + } + + return; + } + + $legacyShare = $event->getShare(); + $legacyBackend = $this->registry->getLegacyBackend(); + if (!$legacyBackend instanceof ISharingLegacyBackend) { + return; + } + $ids = []; try { $this->dbConnection->beginTransaction(); - $this->onOwnerDeleted(new ShareAccessContext(overrideChecks: true), $shareUser); - $this->onInitiatorDeleted(new ShareAccessContext(overrideChecks: true), $shareUser); + try { + $share = $legacyBackend->getShareByLegacyProviderAndId($legacyShare->getProviderId(), $legacyShare->getId()); + if (!isset($this->currentlyUpdatingLegacyShares[$share->id])) { + $this->updateShare($share); + $ids[] = $share->id; + } + } catch (ShareNotFoundException) { + // TODO: Support federation + $owner = $this->userManager->get($legacyShare->getShareOwner()); + if ($owner instanceof IUser) { + $ids = array_merge($ids, $this->importSharesFromLegacyBackend($owner)); + } + } + $this->dbConnection->commit(); } catch (Exception $exception) { $this->dbConnection->rollBack(); throw $exception; } + + // TODO: Only run in behat tests + return; + foreach ($ids as $id) { + try { + $this->dbConnection->beginTransaction(); + + $legacyIds = $legacyBackend->getLegacyFullIds($id); + $legacySharesBefore = array_map(fn (string $legacyId): IShare => $this->legacyManager->getShareById($legacyId), $legacyIds); + + $this->exportShareToLegacyBackend($id); + + $legacySharesAfter = array_map(fn (string $legacyId): IShare => $this->legacyManager->getShareById($legacyId), $legacyIds); + + for ($i = 0, $iMax = count($legacyIds); $i < $iMax; ++$i) { + $legacyShareBefore = $legacySharesBefore[$i]; + $legacyShareAfter = $legacySharesAfter[$i]; + + $comparisons = [ + 'id' => static fn (IShare $share): mixed => $share->getId(), + 'full_id' => static fn (IShare $share): mixed => $share->getFullId(), + // TODO + ]; + + foreach ($comparisons as $field => $getter) { + $valueBefore = $getter($legacyShareBefore); + $valueAfter = $getter($legacyShareAfter); + if ($valueBefore !== $valueAfter) { + throw new RuntimeException('Values for ' . $field . ' do not match:\\nBefore: ' . var_export($valueBefore, true) . '\\nAfter: ' . var_export($valueAfter, true)); + } + } + } + + $this->dbConnection->rollBack(); + } catch (Exception $exception) { + $this->dbConnection->rollBack(); + throw $exception; + } + } } private function assertInTransaction(): void { @@ -802,28 +924,60 @@ private function processShareUpdates(array $sharesOrIds): array { } } - $legacyBackend = $this->registry->getLegacyBackend(); - if ($legacyBackend instanceof ISharingLegacyBackend) { - $compatibleSourceTypes = array_fill_keys($legacyBackend->getCompatibleSourceTypes(), true); - foreach ($share->sources as $source) { - if (!isset($compatibleSourceTypes[$source->class])) { - throw new RuntimeException('The legacy backend ' . $legacyBackend::class . ' does not support this source type: ' . $source->class); - } - } + $this->currentlyUpdatingLegacyShares[$share->id] = true; + $this->updateShareInLegacyBackend($share); + unset($this->currentlyUpdatingLegacyShares[$share->id]); - $compatibleRecipientTypes = array_fill_keys($legacyBackend->getCompatibleRecipientTypes(), true); - foreach ($share->recipients as $recipient) { - if (!isset($compatibleRecipientTypes[$recipient->class])) { - throw new RuntimeException('The legacy backend ' . $legacyBackend::class . ' does not support this recipient type: ' . $recipient->class); - } + $shares[] = $share; + } + + return $shares; + } + + private function updateShareInLegacyBackend(Share $share): void { + $legacyBackend = $this->registry->getLegacyBackend(); + if ($legacyBackend instanceof ISharingLegacyBackend) { + $compatibleSourceTypes = array_fill_keys($legacyBackend->getCompatibleSourceTypes(), true); + foreach ($share->sources as $source) { + if (!isset($compatibleSourceTypes[$source->class])) { + throw new RuntimeException('The legacy backend ' . $legacyBackend::class . ' does not support this source type: ' . $source->class); } + } - $legacyBackend->updateShare($share); + $compatibleRecipientTypes = array_fill_keys($legacyBackend->getCompatibleRecipientTypes(), true); + foreach ($share->recipients as $recipient) { + if (!isset($compatibleRecipientTypes[$recipient->class])) { + throw new RuntimeException('The legacy backend ' . $legacyBackend::class . ' does not support this recipient type: ' . $recipient->class); + } } - $shares[] = $share; + $legacyBackend->updateShare($share); } + } - return $shares; + private function updateShare(Share $share): void { + // To avoid diffing the shares, we just delete and create it. + try { + $this->backend->deleteShare($share->id); + } catch (ShareNotFoundException) { + } + + $this->backend->createShare($share->id, $share->owner, $share->lastUpdated); + $this->backend->updateShareState($share->id, $share->state); + foreach ($share->sources as $source) { + $this->backend->addShareSource($share->id, $source); + } + + foreach ($share->recipients as $recipient) { + $this->backend->addShareRecipient($share->id, $recipient); + } + + foreach ($share->properties as $property) { + $this->backend->createShareProperty($share->id, $property); + } + + foreach ($share->permissions as $permission) { + $this->backend->createSharePermission($share->id, $permission); + } } } diff --git a/lib/public/Share/Events/AfterShareModifiedEvent.php b/lib/public/Share/Events/AfterShareModifiedEvent.php new file mode 100644 index 0000000000000..df9aa24f38611 --- /dev/null +++ b/lib/public/Share/Events/AfterShareModifiedEvent.php @@ -0,0 +1,37 @@ +share; + } +} diff --git a/lib/public/Share/IShare.php b/lib/public/Share/IShare.php index 5e5647f52aa10..8310bb5862040 100644 --- a/lib/public/Share/IShare.php +++ b/lib/public/Share/IShare.php @@ -155,6 +155,13 @@ public function getFullId(): string; */ public function setProviderId(string $id): self; + /** + * Get the provider id. + * + * @since 35.0.0 + */ + public function getProviderId(): string; + /** * Set the node of the file/folder that is shared * diff --git a/lib/unstable/Sharing/ISharingManager.php b/lib/unstable/Sharing/ISharingManager.php index 21128680aaefc..a6ed2d33da6de 100644 --- a/lib/unstable/Sharing/ISharingManager.php +++ b/lib/unstable/Sharing/ISharingManager.php @@ -22,6 +22,7 @@ use NCU\Sharing\Source\IShareSourceType; use NCU\Sharing\Source\ShareSource; use OCP\AppFramework\Attribute\Consumable; +use OCP\IUser; /** * @experimental 35.0.0 @@ -221,4 +222,15 @@ public function getShare(ShareAccessContext $accessContext, string $id): Share; * @experimental 35.0.0 */ public function getShares(ShareAccessContext $accessContext, ?string $filterSourceTypeClass, ?string $filterSourceTypeValue, ?string $lastShareID, ?int $limit): array; + + /** + * @return list + * @since 35.0.0 + */ + public function importSharesFromLegacyBackend(IUser $user): array; + + /** + * @since 35.0.0 + */ + public function exportShareToLegacyBackend(string $id): void; } diff --git a/unified-sharing-check.sh b/unified-sharing-check.sh new file mode 100755 index 0000000000000..c1d5f50e70447 --- /dev/null +++ b/unified-sharing-check.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euxo pipefail + +composer psalm +composer psalm:ocp +composer psalm:ncu +composer psalm:strict +composer rector:strict +composer openapi +./build/autoloaderchecker.sh diff --git a/unified-sharing-test.sh b/unified-sharing-test.sh new file mode 100755 index 0000000000000..2d965aa7088e4 --- /dev/null +++ b/unified-sharing-test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +for target in tests/lib/Sharing tests/Core/Sharing apps/sharing apps/files/tests/Sharing apps/files_sharing/tests/Listener/RestrictInteractionListenerTest.php apps/files_sharing/tests/Sharing; do + if [ -d "$target" ] || [ -f "$target" ]; then + ./autotest.sh sqlite "$target" + fi +done From f38344eafe794bc6109174f284e2c55fd5915b7f Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 31 Jul 2026 00:39:17 +0200 Subject: [PATCH 3/7] fix: ensure all legacy shares for a unified id have the same permissions Signed-off-by: Robin Appelman --- .../lib/Sharing/LegacyBackend.php | 34 ++++++++++++++++--- lib/private/Sharing/ISharingLegacyBackend.php | 5 --- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/apps/files_sharing/lib/Sharing/LegacyBackend.php b/apps/files_sharing/lib/Sharing/LegacyBackend.php index c96ec46e53e19..e57172fd35c6e 100644 --- a/apps/files_sharing/lib/Sharing/LegacyBackend.php +++ b/apps/files_sharing/lib/Sharing/LegacyBackend.php @@ -342,8 +342,6 @@ public function getShare(string $id): Share { null, ), ); - - // TODO: Support per-recipient permissions } /** @psalm-suppress ArgumentTypeCoercion */ @@ -356,7 +354,13 @@ public function getShare(string $id): Share { // TODO $properties = []; - // TODO: Support per-recipient permissions + if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getAttributes())) { + throw new \Exception('All legacy shares sharing a share id don\'t have the same attributes'); + } + if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getPermissions())) { + throw new \Exception('All legacy shares sharing a share id don\'t have the same permissions'); + } + $allowDownload = $legacyShares[0]->getShareType() === IShare::TYPE_LINK || $legacyShares[0]->getShareType() === IShare::TYPE_EMAIL ? !$legacyShares[0]->getHideDownload() : $legacyShares[0]->getAttributes()?->getAttribute('permissions', 'download') === true; @@ -388,6 +392,29 @@ public function getShare(string $id): Share { ); } + /** + * Check that all items return the same result when used as argument to a function + * + * @template T + * @param iterable $items + * @param callable(T):mixed $fn + * @return bool + */ + private function checkAllSame(iterable $items, callable $fn): bool { + $first = true; + $commonValue = null; + foreach ($items as $item) { + $value = $fn($item); + if ($first) { + $commonValue = $value; + $first = false; + } elseif ($value !== $commonValue) { + return false; + } + } + return true; + } + #[\Override] public function getShareByLegacyProviderAndId(string $legacyProvider, string $legacyId): Share { $id = $this->getId($legacyProvider, $legacyId); @@ -438,7 +465,6 @@ public function getUnmappedShares(IUser $user): array { /** * @return list */ - #[\Override] public function getLegacyFullIds(string $id): array { $qb = $this->connection->getQueryBuilder(); $result = $qb diff --git a/lib/private/Sharing/ISharingLegacyBackend.php b/lib/private/Sharing/ISharingLegacyBackend.php index b87cd3bb5947a..9c7774031ff19 100644 --- a/lib/private/Sharing/ISharingLegacyBackend.php +++ b/lib/private/Sharing/ISharingLegacyBackend.php @@ -61,9 +61,4 @@ public function getShareByLegacyProviderAndId(string $legacyProvider, string $le * @return list */ public function getUnmappedShares(IUser $user): array; - - /** - * @return list - */ - public function getLegacyFullIds(string $id): array; } From 18cc06c74252624e52a6d6a0a399a472d02c5bee Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 31 Jul 2026 00:56:33 +0200 Subject: [PATCH 4/7] fix: handle outgoing federated share in legacy share backend Signed-off-by: Robin Appelman --- .../files_sharing/lib/AppInfo/Application.php | 9 +-- .../lib/Sharing/LegacyBackend.php | 58 +++++++++++++++---- .../tests/Sharing/LegacyBackendTest.php | 2 + 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php index 3c81efea6fee8..c9affd3fcf759 100644 --- a/apps/files_sharing/lib/AppInfo/Application.php +++ b/apps/files_sharing/lib/AppInfo/Application.php @@ -146,14 +146,7 @@ function () use ($c) { $context->registerEventListener(RestrictInteractionEvent::class, RestrictInteractionListener::class); $registry = Server::get(ISharingRegistry::class); - $registry->registerLegacyBackend(new LegacyBackend( - Server::get(IFactory::class), - Server::get(IDBConnection::class), - Server::get(IRootFolder::class), - Server::get(IManager::class), - Server::get(ISnowflakeGenerator::class), - Server::get(ISharingManager::class), - )); + $registry->registerLegacyBackend(Server::get(LegacyBackend::class)); } #[\Override] diff --git a/apps/files_sharing/lib/Sharing/LegacyBackend.php b/apps/files_sharing/lib/Sharing/LegacyBackend.php index e57172fd35c6e..9a15f2a1e3642 100644 --- a/apps/files_sharing/lib/Sharing/LegacyBackend.php +++ b/apps/files_sharing/lib/Sharing/LegacyBackend.php @@ -39,6 +39,7 @@ use OCA\Files\Sharing\Source\NodeShareSourceType; use OCP\Constants; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Federation\ICloudIdManager; use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\Node; @@ -60,6 +61,7 @@ public function __construct( private IManager $legacyManager, private ISnowflakeGenerator $snowflakeGenerator, private ISharingManager $sharingManager, + private ICloudIdManager $cloudIdManager, ) { } @@ -107,10 +109,7 @@ public function updateShare(Share $share): void { $legacyShare = $this->legacyManager->newShare(); $legacyShare->setShareType($legacyShareType); $legacyShare->setNodeId((int)$source->value); - // TODO: Support federation - if ($recipient->class !== TokenShareRecipientType::class) { - $legacyShare->setSharedWith($recipient->value); - } + $legacyShare->setSharedWith($this->recipientToLegacySharedWith($recipient)); $this->setCommonFields($share, $legacyShare); $create = true; @@ -118,7 +117,11 @@ public function updateShare(Share $share): void { $update = false; - // TODO: Support federation + + if ($recipient->instance !== null) { + throw new \Exception('Incoming remote shares aren\'t handled by ' . self::class); + } + if ($recipient->initiator !== null && $legacyShare->getSharedBy() !== $recipient->initiator->userId) { $legacyShare->setSharedBy($recipient->initiator->userId); $update = true; @@ -181,7 +184,9 @@ public function updateShare(Share $share): void { private function setCommonFields(Share $share, IShare $legacyShare): bool { $update = false; - // TODO: Support federation + if ($share->owner->instance !== null) { + throw new \Exception('Incoming remote shares aren\'t handled by ' . self::class); + } if ($legacyShare->getShareOwner() !== $share->owner->userId) { $legacyShare->setShareOwner($share->owner->userId); $update = true; @@ -329,25 +334,30 @@ public function getShare(string $id): Share { $isTokenRecipient = $recipientTypeClass === TokenShareRecipientType::class; $recipients[$recipientTypeClass] ??= []; $uniqueId = $isTokenRecipient ? $legacyShare->getToken() : $legacyShare->getSharedWith(); + $recipient = $this->splitLegacySharedWith($legacyShare->getShareType(), $legacyShare->getSharedWith()); /** @psalm-suppress ArgumentTypeCoercion */ $recipients[$recipientTypeClass][$uniqueId] ??= new ShareRecipient( $recipientTypeClass, - $isTokenRecipient ? '' : $legacyShare->getSharedWith(), - // TODO: Support federation - null, + $recipient['value'], + $recipient['remote'], $isTokenRecipient ? $legacyShare->getToken() : $this->sharingManager->generateSecret(), new ShareUser( $legacyShare->getSharedBy(), - // TODO: Support federation + // Incoming remote shares aren't handled by this null, ), ); } + + if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getShareOwner())) { + throw new \Exception('All legacy shares sharing a share id don\'t have the same owner'); + } + /** @psalm-suppress ArgumentTypeCoercion */ $owner = new ShareUser( $legacyShares[0]->getShareOwner(), - // TODO: Support federation + // Incoming remote shares aren't handled by this null, ); @@ -567,4 +577,30 @@ private function recipientTypeClassToLegacyShareType(string $recipientTypeClass, default => throw new RuntimeException('Unsupported recipient type: ' . $recipientTypeClass), }; } + + private function recipientToLegacySharedWith(ShareRecipient $recipient): string { + if ($recipient->class === TokenShareRecipientType::class) { + return $this->cloudIdManager->getCloudId($recipient->value, $recipient->instance)->getId(); + } else { + return $recipient->value; + } + } + + /** + * @return array{value: string, remote: string|null} + */ + private function splitLegacySharedWith(int $shareType, string $sharedWith): array { + if ($shareType === IShare::TYPE_REMOTE || $shareType === IShare::TYPE_REMOTE_GROUP) { + $cloudId = $this->cloudIdManager->resolveCloudId($sharedWith); + return [ + 'value' => $cloudId->getUser(), + 'remote' => $cloudId->getRemote(), + ]; + } else { + return [ + 'value' => $sharedWith, + 'remote' => null, + ]; + } + } } diff --git a/apps/files_sharing/tests/Sharing/LegacyBackendTest.php b/apps/files_sharing/tests/Sharing/LegacyBackendTest.php index 7c56625e6434d..b5a29b953a00f 100644 --- a/apps/files_sharing/tests/Sharing/LegacyBackendTest.php +++ b/apps/files_sharing/tests/Sharing/LegacyBackendTest.php @@ -32,6 +32,7 @@ use OCA\Files\Sharing\Source\NodeShareSourceType; use OCA\Files_Sharing\Sharing\LegacyBackend; use OCP\Constants; +use OCP\Federation\ICloudIdManager; use OCP\Files\IRootFolder; use OCP\IDBConnection; use OCP\IGroupManager; @@ -74,6 +75,7 @@ protected function setUp(): void { $this->legacyManager, Server::get(ISnowflakeGenerator::class), Server::get(ISharingManager::class), + Server::get(ICloudIdManager::class), ); } From 227ed61223b530b62316f9f225839449373e2c36 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 31 Jul 2026 01:24:04 +0200 Subject: [PATCH 5/7] refactor: use DateTimeImmutable in unified sharing api instead of timestamp Signed-off-by: Robin Appelman --- .../Source/NodeShareSourceTypeTest.php | 9 +- .../files_sharing/lib/AppInfo/Application.php | 5 - .../lib/Sharing/LegacyBackend.php | 5 +- lib/private/Sharing/SharingBackend.php | 10 +- lib/private/Sharing/SharingManager.php | 58 +-- lib/unstable/Sharing/ISharingBackend.php | 5 +- lib/unstable/Sharing/ISharingManager.php | 7 +- lib/unstable/Sharing/Share.php | 6 +- .../Recipient/GroupShareRecipientTypeTest.php | 9 +- .../Recipient/TeamShareRecipientTypeTest.php | 8 +- .../Recipient/UserShareRecipientTypeTest.php | 9 +- .../Sharing/AbstractSharingManagerTests.php | 333 ++++++++---------- 12 files changed, 214 insertions(+), 250 deletions(-) diff --git a/apps/files/tests/Sharing/Source/NodeShareSourceTypeTest.php b/apps/files/tests/Sharing/Source/NodeShareSourceTypeTest.php index 31d699361cfd0..ee1e3a5a35414 100644 --- a/apps/files/tests/Sharing/Source/NodeShareSourceTypeTest.php +++ b/apps/files/tests/Sharing/Source/NodeShareSourceTypeTest.php @@ -12,6 +12,7 @@ use NCU\Sharing\ShareAccessContext; use NCU\Sharing\Source\ShareSource; use OC\Files\Filesystem; +use OC\Sharing\SharingManager; use OC\User\Database; use OCA\Files\Sharing\Source\NodeShareSourceType; use OCP\EventDispatcher\IEventDispatcher; @@ -103,14 +104,14 @@ public function testDelete(): void { $this->manager->addShareSource($accessContext, $id, new ShareSource($this->sourceType::class, (string)$this->node->getId())); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->node->delete(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $share = $this->manager->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share->lastUpdated); - $this->assertLessThanOrEqual($after, $share->lastUpdated); + $this->assertGreaterThanOrEqual($before, SharingManager::timeToMs($share->lastUpdated)); + $this->assertLessThanOrEqual($after, SharingManager::timeToMs($share->lastUpdated)); $this->assertEquals([], $share->sources); $this->manager->deleteShare($accessContext, $id); diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php index c9affd3fcf759..05fb202ea9cae 100644 --- a/apps/files_sharing/lib/AppInfo/Application.php +++ b/apps/files_sharing/lib/AppInfo/Application.php @@ -8,7 +8,6 @@ namespace OCA\Files_Sharing\AppInfo; -use NCU\Sharing\ISharingManager; use NCU\Sharing\ISharingRegistry; use OC\Group\DisplayNameCache as GroupDisplayNameCache; use OC\User\DisplayNameCache; @@ -52,7 +51,6 @@ use OCP\Files\Events\BeforeZipCreatedEvent; use OCP\Files\Events\Node\BeforeNodeReadEvent; use OCP\Files\Events\UserHomeSetupEvent; -use OCP\Files\IRootFolder; use OCP\Group\Events\BeforeGroupDeletedEvent; use OCP\Group\Events\GroupChangedEvent; use OCP\Group\Events\GroupDeletedEvent; @@ -62,14 +60,11 @@ use OCP\IDBConnection; use OCP\IGroup; use OCP\Interaction\RestrictInteractionEvent; -use OCP\L10N\IFactory; use OCP\Server; use OCP\Share\Events\BeforeShareDeletedEvent; use OCP\Share\Events\ShareCreatedEvent; use OCP\Share\Events\ShareMovedEvent; use OCP\Share\Events\ShareTransferredEvent; -use OCP\Share\IManager; -use OCP\Snowflake\ISnowflakeGenerator; use OCP\User\Events\UserChangedEvent; use OCP\User\Events\UserDeletedEvent; use OCP\Util; diff --git a/apps/files_sharing/lib/Sharing/LegacyBackend.php b/apps/files_sharing/lib/Sharing/LegacyBackend.php index 9a15f2a1e3642..d3c8c06346c06 100644 --- a/apps/files_sharing/lib/Sharing/LegacyBackend.php +++ b/apps/files_sharing/lib/Sharing/LegacyBackend.php @@ -117,7 +117,6 @@ public function updateShare(Share $share): void { $update = false; - if ($recipient->instance !== null) { throw new \Exception('Incoming remote shares aren\'t handled by ' . self::class); } @@ -349,7 +348,6 @@ public function getShare(string $id): Share { ); } - if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getShareOwner())) { throw new \Exception('All legacy shares sharing a share id don\'t have the same owner'); } @@ -391,8 +389,7 @@ public function getShare(string $id): Share { return new Share( $id, $owner, - // TODO - 0, + \DateTimeImmutable::createFromMutable($legacyShares[0]->getShareTime()), // TODO ShareState::Active, array_values($sources), diff --git a/lib/private/Sharing/SharingBackend.php b/lib/private/Sharing/SharingBackend.php index 5d6cb5d4017d2..d6c4b0f473240 100644 --- a/lib/private/Sharing/SharingBackend.php +++ b/lib/private/Sharing/SharingBackend.php @@ -58,7 +58,7 @@ public function __construct( } #[\Override] - public function createShare(string $id, ShareUser $owner, int $lastUpdated): void { + public function createShare(string $id, ShareUser $owner, \DateTimeImmutable $lastUpdated): void { $qb = $this->connection->getQueryBuilder(); $qb ->insert('sharing_share') @@ -66,7 +66,7 @@ public function createShare(string $id, ShareUser $owner, int $lastUpdated): voi 'id' => $qb->createNamedParameter($id), 'owner_user_id' => $qb->createNamedParameter($owner->userId), 'owner_instance' => $qb->createNamedParameter($owner->instance), - 'last_updated' => $qb->createNamedParameter($lastUpdated), + 'last_updated' => $qb->createNamedParameter(SharingManager::timeToMs($lastUpdated)), 'state' => $qb->createNamedParameter(ShareState::Draft->value), ]) ->executeStatement(); @@ -533,13 +533,13 @@ public function getShareOwner(string $id): ShareUser { * @param non-empty-list $ids */ #[\Override] - public function setLastUpdated(array $ids, int $lastUpdated): void { + public function setLastUpdated(array $ids, \DateTimeImmutable $lastUpdated): void { foreach (array_chunk($ids, 1000) as $chunk) { $qb = $this->connection->getQueryBuilder(); $rowCount = $qb ->update('sharing_share') - ->set('last_updated', $qb->createNamedParameter($lastUpdated, IQueryBuilder::PARAM_INT)) + ->set('last_updated', $qb->createNamedParameter(SharingManager::timeToMs($lastUpdated), IQueryBuilder::PARAM_INT)) ->where($qb->expr()->in('id', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))) ->executeStatement(); if ($rowCount !== count($chunk)) { @@ -954,7 +954,7 @@ private function list(ShareAccessContext $accessContext, ?string $filterShareID, $shares = array_map(static fn (array $share): Share => new Share( $share['id'], $share['owner'], - $share['last_updated'], + new \DateTimeImmutable('@' . $share['last_updated'] / 1000), // timestamp is stored in milliseconds $share['state'], $share['sources'], $share['recipients'], diff --git a/lib/private/Sharing/SharingManager.php b/lib/private/Sharing/SharingManager.php index 8415af37b8f01..542a3c09251f0 100644 --- a/lib/private/Sharing/SharingManager.php +++ b/lib/private/Sharing/SharingManager.php @@ -50,6 +50,7 @@ use OCP\Share\IManager; use OCP\Snowflake\ISnowflakeGenerator; use OCP\User\Events\BeforeUserDeletedEvent; +use Psr\Clock\ClockInterface; use Random\Randomizer; use RuntimeException; @@ -81,6 +82,7 @@ public function __construct( private readonly IDBConnection $dbConnection, private readonly ISharingRegistry $registry, private readonly IManager $legacyManager, + private readonly ClockInterface $clock, IAppConfig $appConfig, ) { $this->randomizer = new Randomizer(); @@ -160,13 +162,8 @@ public function generateSecret(): string { } #[\Override] - public function generateTimestamp(): int { - $time = (int)(microtime(true) * 1000.0); - if ($time < 0) { - throw new RuntimeException('Have you invented time travel?'); - } - - return $time; + public function getTime(): \DateTimeImmutable { + return $this->clock->now(); } #[\Override] @@ -178,7 +175,7 @@ public function createShare(ShareAccessContext $accessContext): string { $this->assertInTransaction(); $id = $this->snowflakeGenerator->nextId(); - $lastUpdated = $this->generateTimestamp(); + $lastUpdated = $this->getTime(); $this->backend->createShare($id, new ShareUser($currentUser->getUID(), null), $lastUpdated); $this->processShareUpdates([$id]); @@ -210,7 +207,7 @@ public function onOwnerDeleted(ShareAccessContext $accessContext, ShareUser $own public function updateShareState(ShareAccessContext $accessContext, string $id, ShareState $state): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); $this->validateShareOwnerOperation($accessContext, $owner); @@ -229,7 +226,7 @@ public function updateShareState(ShareAccessContext $accessContext, string $id, public function addShareSource(ShareAccessContext $accessContext, string $id, ShareSource $source): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); $this->validateShareOwnerOperation($accessContext, $owner); @@ -270,7 +267,7 @@ public function addShareSource(ShareAccessContext $accessContext, string $id, Sh public function removeShareSource(ShareAccessContext $accessContext, string $id, ShareSource $source): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); $this->validateShareOwnerOperation($accessContext, $owner); @@ -288,7 +285,7 @@ public function onSourceDeleted(ShareAccessContext $accessContext, ShareSource $ $this->assertInTransaction(); - $timestamp = $this->generateTimestamp(); + $timestamp = $this->getTime(); $updatedIds = $this->backend->onSourceDeleted($source); if ($updatedIds === []) { @@ -308,7 +305,7 @@ public function addShareRecipient(ShareAccessContext $accessContext, string $id, $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); @@ -371,7 +368,7 @@ public function removeShareRecipient(ShareAccessContext $accessContext, string $ $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); @@ -396,7 +393,7 @@ public function onRecipientDeleted(ShareAccessContext $accessContext, ShareRecip $this->assertInTransaction(); - $timestamp = $this->generateTimestamp(); + $timestamp = $this->getTime(); $updatedIds = $this->backend->onRecipientDeleted($recipient); if ($updatedIds === []) { @@ -416,7 +413,7 @@ public function onInitiatorDeleted(ShareAccessContext $accessContext, ShareUser $this->assertInTransaction(); - $timestamp = $this->generateTimestamp(); + $timestamp = $this->getTime(); $updatedIds = $this->backend->onInitiatorDeleted($initiator); if ($updatedIds === []) { @@ -432,7 +429,7 @@ public function onInitiatorDeleted(ShareAccessContext $accessContext, ShareUser public function updateShareRecipientSecret(ShareAccessContext $accessContext, string $id, ShareRecipient $recipient, string $secret): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); @@ -464,7 +461,7 @@ public function updateShareRecipientSecret(ShareAccessContext $accessContext, st public function createSharePropertyDefaultValue(Share $share, string $propertyTypeClass): Share { $this->assertInTransaction(); - $timestamp = $this->generateTimestamp(); + $timestamp = $this->getTime(); $this->backend->setLastUpdated([$share->id], $timestamp); if (($propertyType = $this->registry->getPropertyTypes()[$propertyTypeClass] ?? null) === null) { @@ -498,7 +495,7 @@ public function createSharePropertyDefaultValue(Share $share, string $propertyTy public function updateShareProperty(ShareAccessContext $accessContext, string $id, ShareProperty $property): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); $this->validateShareOwnerOperation($accessContext, $owner); @@ -523,8 +520,8 @@ public function updateShareProperty(ShareAccessContext $accessContext, string $i public function createSharePermissionDefaultValue(Share $share, string $permissionTypeClass): Share { $this->assertInTransaction(); - $timestamp = $this->generateTimestamp(); - $this->backend->setLastUpdated([$share->id], $timestamp); + $time = $this->getTime(); + $this->backend->setLastUpdated([$share->id], $time); if (($permissionType = $this->registry->getPermissionTypes()[$permissionTypeClass] ?? null) === null) { throw new RuntimeException('The permission is not registered: ' . $permissionTypeClass); @@ -540,7 +537,7 @@ public function createSharePermissionDefaultValue(Share $share, string $permissi $share = new Share( $share->id, $share->owner, - $timestamp, + $time, $share->state, $share->sources, $share->recipients, @@ -557,7 +554,7 @@ public function createSharePermissionDefaultValue(Share $share, string $permissi public function updateSharePermission(ShareAccessContext $accessContext, string $id, SharePermission $permission): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); $this->validateShareOwnerOperation($accessContext, $owner); @@ -594,7 +591,7 @@ public function updateSharePermission(ShareAccessContext $accessContext, string public function selectSharePermissionPreset(ShareAccessContext $accessContext, string $id, string $permissionPresetClass): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); $this->validateShareOwnerOperation($accessContext, $owner); @@ -980,4 +977,17 @@ private function updateShare(Share $share): void { $this->backend->createSharePermission($share->id, $permission); } } + + /** + * @param \DateTimeImmutable $time + * @return non-negative-int + */ + public static function timeToMs(\DateTimeImmutable $time): int { + if (method_exists($time, 'getMicrosecond')) { + $micros = $time->getMicrosecond(); + } else { + $micros = (int)$time->format('u'); + } + return $time->getTimestamp() * 1000 + (int)floor($micros / 1000); + } } diff --git a/lib/unstable/Sharing/ISharingBackend.php b/lib/unstable/Sharing/ISharingBackend.php index a4cb1401dd9cc..5fbc5a7149abd 100644 --- a/lib/unstable/Sharing/ISharingBackend.php +++ b/lib/unstable/Sharing/ISharingBackend.php @@ -29,7 +29,7 @@ interface ISharingBackend { * * @experimental 35.0.0 */ - public function createShare(string $id, ShareUser $owner, int $lastUpdated): void; + public function createShare(string $id, ShareUser $owner, \DateTimeImmutable $lastUpdated): void; /** * Perform all updates when the owner was deleted. @@ -199,9 +199,8 @@ public function getShareOwner(string $id): ShareUser; * Set the last updated timestamp for multiple shares. * * @param non-empty-list $ids - * @param non-negative-int $lastUpdated * @throws ShareNotFoundException * @experimental 35.0.0 */ - public function setLastUpdated(array $ids, int $lastUpdated): void; + public function setLastUpdated(array $ids, \DateTimeImmutable $lastUpdated): void; } diff --git a/lib/unstable/Sharing/ISharingManager.php b/lib/unstable/Sharing/ISharingManager.php index a6ed2d33da6de..46965673904f1 100644 --- a/lib/unstable/Sharing/ISharingManager.php +++ b/lib/unstable/Sharing/ISharingManager.php @@ -51,12 +51,11 @@ public function searchRecipients(ShareAccessContext $accessContext, ?array $filt public function generateSecret(): string; /** - * Generate a new timestamp in milliseconds since the UNIX epoch. - * - * @return non-negative-int + * Get the current time + * * @experimental 35.0.0 */ - public function generateTimestamp(): int; + public function getTime(): \DateTimeImmutable; /** * Create a new share. diff --git a/lib/unstable/Sharing/Share.php b/lib/unstable/Sharing/Share.php index ed74a4f2bcb8f..feed3031aa370 100644 --- a/lib/unstable/Sharing/Share.php +++ b/lib/unstable/Sharing/Share.php @@ -18,6 +18,7 @@ use NCU\Sharing\Recipient\ShareRecipient; use NCU\Sharing\Source\IShareSourceType; use NCU\Sharing\Source\ShareSource; +use OC\Sharing\SharingManager; use OCP\AppFramework\Attribute\Consumable; use OCP\IURLGenerator; use OCP\IUserManager; @@ -154,8 +155,7 @@ public function __construct( /** @var non-empty-string $id */ public readonly string $id, public readonly ShareUser $owner, - /** @var non-negative-int $lastUpdated Unix time in milliseconds */ - public readonly int $lastUpdated, + public readonly \DateTimeImmutable $lastUpdated, public readonly ShareState $state, /** @var list $sources */ public readonly array $sources, @@ -240,7 +240,7 @@ public function format(ISharingRegistry $registry, IFactory $l10nFactory, IURLGe return [ 'id' => $this->id, 'owner' => $this->owner->format($userManager), - 'last_updated' => $this->lastUpdated, + 'last_updated' => SharingManager::timeToMs($this->lastUpdated), 'state' => $this->state->value, 'sources' => ShareSource::formatMultiple($registry, $l10nFactory, $this->sources), 'recipients' => ShareRecipient::formatMultiple($registry, $l10nFactory, $urlGenerator, $userManager, $this->recipients), diff --git a/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php b/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php index abfd54bc2a847..9866137487205 100644 --- a/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php +++ b/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php @@ -15,6 +15,7 @@ use NCU\Sharing\ShareAccessContext; use OC\Core\Sharing\Recipient\GroupShareRecipientType; use OC\Group\Database; +use OC\Sharing\SharingManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; use OCP\IGroup; @@ -140,14 +141,14 @@ public function testDelete(): void { $this->manager->addShareRecipient($accessContext, $id, new ShareRecipient($this->recipientType::class, $this->group1->getGID(), null)); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->group1->delete(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $share = $this->manager->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share->lastUpdated); - $this->assertLessThanOrEqual($after, $share->lastUpdated); + $this->assertGreaterThanOrEqual($before, SharingManager::timeToMs($share->lastUpdated)); + $this->assertLessThanOrEqual($after, SharingManager::timeToMs($share->lastUpdated)); $this->assertEquals([], $share->recipients); $this->manager->deleteShare($accessContext, $id); diff --git a/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php b/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php index 8b24d567c1655..31bbfd3458a55 100644 --- a/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php +++ b/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php @@ -171,14 +171,14 @@ public function testDelete(): void { $circlesManager = Server::get(CirclesManager::class); $circlesManager->startSession($circlesManager->getLocalFederatedUser($this->user1->getUID())); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $circlesManager->destroyCircle($this->team1->getId()); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $share = $this->manager->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share->lastUpdated); - $this->assertLessThanOrEqual($after, $share->lastUpdated); + $this->assertGreaterThanOrEqual($before, $share->lastUpdated->getTimestamp()); + $this->assertLessThanOrEqual($after, $share->lastUpdated->getTimestamp()); $this->assertEquals([], $share->recipients); $this->manager->deleteShare($accessContext, $id); diff --git a/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php b/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php index 453c0ecd12ac4..49935cd13d980 100644 --- a/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php +++ b/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php @@ -14,6 +14,7 @@ use NCU\Sharing\Recipient\ShareRecipient; use NCU\Sharing\ShareAccessContext; use OC\Core\Sharing\Recipient\UserShareRecipientType; +use OC\Sharing\SharingManager; use OC\User\Database; use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; @@ -144,14 +145,14 @@ public function testDelete(): void { $this->manager->addShareRecipient($accessContext, $id, new ShareRecipient($this->recipientType::class, $this->user2->getUID(), null)); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->user2->delete(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $share = $this->manager->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share->lastUpdated); - $this->assertLessThanOrEqual($after, $share->lastUpdated); + $this->assertGreaterThanOrEqual($before, SharingManager::timeToMs($share->lastUpdated)); + $this->assertLessThanOrEqual($after, SharingManager::timeToMs($share->lastUpdated)); $this->assertEquals([], $share->recipients); $this->manager->deleteShare($accessContext, $id); diff --git a/tests/lib/Sharing/AbstractSharingManagerTests.php b/tests/lib/Sharing/AbstractSharingManagerTests.php index 8f4522dc86ff2..806a8965ae572 100644 --- a/tests/lib/Sharing/AbstractSharingManagerTests.php +++ b/tests/lib/Sharing/AbstractSharingManagerTests.php @@ -19,6 +19,7 @@ use NCU\Sharing\ShareState; use NCU\Sharing\Source\ShareSource; use OC\Core\Sharing\Permission\ReshareSharePermissionType; +use OC\Sharing\SharingManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\HintException; use OCP\IDBConnection; @@ -85,6 +86,15 @@ abstract protected function getShares(ShareAccessContext $accessContext, ?string protected IUser $user2; + private function parseTime($timestampMs): \DateTimeImmutable { + return new \DateTimeImmutable('@' . (int)$timestampMs / 1000); + } + + private function assertDateBetween(\DateTimeImmutable $before, \DateTimeImmutable $after, \DateTimeImmutable $time) { + $this->assertGreaterThanOrEqual(SharingManager::timeToMs($before), SharingManager::timeToMs($time)); + $this->assertLessThanOrEqual(SharingManager::timeToMs($after), SharingManager::timeToMs($time)); + } + #[\Override] public function setUp(): void { parent::setUp(); @@ -567,12 +577,11 @@ public function testSearchRecipientsOmitExisting(): void { public function testCreateShare(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->createShare($accessContext); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); unset($share['id']); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); unset($share['last_updated']); $this->assertEquals([ 'owner' => [ @@ -690,11 +699,10 @@ public function testUpdateShareState(array $sources, array $recipients, array $p $this->assertEquals($errorMessage, $exception->getHint()); } } else { - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareState($accessContext, $id, ShareState::Active); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(ShareState::Active->value, $share['state']); } } @@ -706,11 +714,10 @@ public function testAddShareSource(): void { $id = $this->manager->createShare($accessContext); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals([ [ 'class' => TestShareSourceType1::class, @@ -765,11 +772,10 @@ public function testRemoveShareSource(): void { $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->removeShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(ShareState::Active->value, $share['state']); $this->assertEquals([ [ @@ -782,11 +788,10 @@ public function testRemoveShareSource(): void { ], ], $share['sources']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->removeShareSource($accessContext, $id, new ShareSource(TestShareSourceType2::class, 'source2')); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(ShareState::Draft->value, $share['state']); $this->assertEquals([], $share['sources']); } @@ -798,11 +803,10 @@ public function testAddShareRecipient(): void { $id = $this->manager->createShare($accessContext); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->addShareRecipient($accessContext, $id, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals([ [ 'class' => TestShareRecipientType1::class, @@ -890,11 +894,10 @@ public function testAddChildShareRecipientWithResharePermission(): void { $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->addShareRecipient(new ShareAccessContext($this->user1), $id, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals([ [ 'class' => TestShareRecipientType1::class, @@ -955,11 +958,10 @@ public function testRemoveShareRecipient(): void { $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->removeShareRecipient($accessContext, $id, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(ShareState::Active->value, $share['state']); $this->assertEquals([ [ @@ -985,11 +987,10 @@ public function testRemoveShareRecipient(): void { ], ], $share['recipients']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->removeShareRecipient($accessContext, $id, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(ShareState::Draft->value, $share['state']); $this->assertEquals([], $share['recipients']); } @@ -1074,11 +1075,10 @@ public function testRemoveChildShareRecipientWithResharePermission(): void { $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->removeShareRecipient(new ShareAccessContext($this->user1), $id, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals([ [ 'class' => TestShareRecipientType1::class, @@ -1232,11 +1232,10 @@ public function testUpdateShareRecipientSecret(bool $isSecretUpdatable): void { $this->assertEquals('You are not allowed to edit this share.', $exception->getHint()); } } else { - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareRecipientSecret($accessContext, $id, $recipient, 'mysecret'); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); unset($share['last_updated']); $this->assertEquals([ [ @@ -1292,11 +1291,10 @@ public function testUpdateShareProperty(array $values): void { $this->dbConnection->commit(); foreach ($values as $value) { - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyType1::class, $value)); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals([ [ 'class' => TestSharePropertyType1::class, @@ -1329,11 +1327,10 @@ public function testUpdateSharePropertyRequired(): void { $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeRequired::class, 'valid1')); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(ShareState::Draft->value, $share['state']); $this->assertEquals([ [ @@ -1364,11 +1361,10 @@ public function testUpdateSharePropertyRequired(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeRequired::class, 'valid2')); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(ShareState::Active->value, $share['state']); $this->assertEquals([ [ @@ -1395,11 +1391,10 @@ public function testUpdateSharePropertyRequired(): void { ], ], $share['properties']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeRequired::class, null)); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(ShareState::Draft->value, $share['state']); $this->assertEquals([ [ @@ -1497,11 +1492,10 @@ public function testUpdateSharePropertyModifyProperties(): void { $this->manager->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'old-value')); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-save-old-value')); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals([ [ 'class' => TestSharePropertyType1::class, @@ -1527,11 +1521,10 @@ public function testUpdateSharePropertyModifyProperties(): void { ], ], $share['properties']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-save')); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals([ [ 'class' => TestSharePropertyType1::class, @@ -1557,11 +1550,10 @@ public function testUpdateSharePropertyModifyProperties(): void { ], ], $share['properties']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-load')); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals([ [ 'class' => TestSharePropertyType1::class, @@ -1599,12 +1591,11 @@ public function testUpdateSharePermission(): void { $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->updateSharePermission($accessContext, $id, new SharePermission(ReshareSharePermissionType::class, true)); $share = $this->updateSharePermission($accessContext, $id, new SharePermission(TestSharePermissionType1::class, true)); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(ShareState::Draft->value, $share['state']); $this->assertEquals([ [ @@ -1631,11 +1622,10 @@ public function testUpdateSharePermission(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateSharePermission($accessContext, $id, new SharePermission(ReshareSharePermissionType::class, false)); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(ShareState::Active->value, $share['state']); $this->assertEquals([ [ @@ -1658,11 +1648,10 @@ public function testUpdateSharePermission(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateSharePermission($accessContext, $id, new SharePermission(TestSharePermissionType1::class, false)); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(ShareState::Draft->value, $share['state']); $this->assertEquals([ [ @@ -1727,16 +1716,15 @@ public function testSelectSharePermissionPreset(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->getShare($accessContext, $id); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertNull($share['permission_preset']); $this->assertEquals([ [ @@ -1768,11 +1756,10 @@ public function testSelectSharePermissionPreset(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->selectSharePermissionPreset($accessContext, $id, TestSharePermissionPreset2::class); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(TestSharePermissionPreset2::class, $share['permission_preset']); $this->assertEquals([ [ @@ -1804,11 +1791,10 @@ public function testSelectSharePermissionPreset(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateSharePermission($accessContext, $id, new SharePermission(TestSharePermissionType3::class, true)); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertNull($share['permission_preset']); $this->assertEquals([ [ @@ -1840,11 +1826,10 @@ public function testSelectSharePermissionPreset(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->selectSharePermissionPreset($accessContext, $id, TestSharePermissionPreset1::class); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(TestSharePermissionPreset1::class, $share['permission_preset']); $this->assertEquals([ [ @@ -1876,11 +1861,10 @@ public function testSelectSharePermissionPreset(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateSharePermission($accessContext, $id, new SharePermission(TestSharePermissionType1::class, false)); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertNull($share['permission_preset']); $this->assertEquals([ [ @@ -1926,16 +1910,15 @@ public function testSelectSharePermissionPresetCompatible(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->getShare($accessContext, $id); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertNull($share['permission_preset']); $this->assertEquals([ [ @@ -1949,11 +1932,10 @@ public function testSelectSharePermissionPresetCompatible(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->selectSharePermissionPreset($accessContext, $id, TestSharePermissionPreset2::class); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(TestSharePermissionPreset2::class, $share['permission_preset']); $this->assertEquals([ [ @@ -1967,11 +1949,10 @@ public function testSelectSharePermissionPresetCompatible(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertNull($share['permission_preset']); $this->assertEquals([ [ @@ -1994,11 +1975,10 @@ public function testSelectSharePermissionPresetCompatible(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->selectSharePermissionPreset($accessContext, $id, TestSharePermissionPreset2::class); - $after = $this->manager->generateTimestamp(); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $after = $this->manager->getTime(); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals(TestSharePermissionPreset2::class, $share['permission_preset']); $this->assertEquals([ [ @@ -2043,7 +2023,7 @@ public function testDeleteShare(): void { public function testGetShare(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -2051,11 +2031,10 @@ public function testGetShare(): void { $this->manager->getShare($accessContext, $id); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); unset($share['last_updated']); $this->assertEquals([ 'id' => $id, @@ -2160,7 +2139,7 @@ public function testGetShareAsRecipientNotActive(): void { public function testGetShareAsRecipientActive(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -2171,11 +2150,10 @@ public function testGetShareAsRecipientActive(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare(new ShareAccessContext(currentUser: $this->user1), $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); unset($share['last_updated']); $this->assertEquals([ 'id' => $id, @@ -2285,7 +2263,7 @@ public function testGetShareAsRecipientWithArguments(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -2295,11 +2273,10 @@ public function testGetShareAsRecipientWithArguments(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare(new ShareAccessContext(currentUser: $this->user1, arguments: [TestShareRecipientTypeArguments::class => 'secret']), $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); unset($share['last_updated']); $this->assertEquals([ 'id' => $id, @@ -2409,7 +2386,7 @@ public function testGetShareWithSecretActive(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -2418,7 +2395,7 @@ public function testGetShareWithSecretActive(): void { $this->manager->updateSharePermission($accessContext, $id, new SharePermission(TestSharePermissionType1::class, true)); $this->manager->updateShareState($accessContext, $id, ShareState::Active); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->manager->getShare($accessContext, $id); $this->dbConnection->commit(); @@ -2426,8 +2403,7 @@ public function testGetShareWithSecretActive(): void { $this->assertNotNull($secret); $share = $this->getShare(new ShareAccessContext(secret: $secret), $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); unset($share['last_updated']); $this->assertEquals([ 'id' => $id, @@ -2535,7 +2511,7 @@ public function testGetShareAsRecipientFilteredProperties(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -2546,11 +2522,10 @@ public function testGetShareAsRecipientFilteredProperties(): void { $this->manager->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeFilter::class, 'visible')); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare(new ShareAccessContext(currentUser: $this->user1), $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); unset($share['last_updated']); $this->assertEquals([ 'id' => $id, @@ -2644,15 +2619,14 @@ public function testGetShareAsRecipientFilteredProperties(): void { 'permission_preset' => TestSharePermissionPreset1::class, ], $share); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $this->manager->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeFilter::class, 'filtered')); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare(new ShareAccessContext(currentUser: $this->owner), $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); unset($share['last_updated']); $this->assertEquals([ 'id' => $id, @@ -2761,7 +2735,7 @@ public function testGetShareAsRecipientFilteredArguments(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -2771,11 +2745,10 @@ public function testGetShareAsRecipientFilteredArguments(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare(new ShareAccessContext(currentUser: $this->user1), $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); unset($share['last_updated']); $this->assertEquals([ 'id' => $id, @@ -2870,8 +2843,7 @@ public function testGetShareAsRecipientFilteredArguments(): void { ], $share); $share = $this->getShare(new ShareAccessContext(currentUser: $this->owner, arguments: [TestSharePropertyTypeFilter::class => 'filtered']), $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); unset($share['last_updated']); $this->assertEquals([ 'id' => $id, @@ -3004,18 +2976,17 @@ public function testGetShareWithPublicSecret(bool $isSecretPublic): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareRecipient($accessContext, $id, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); $this->manager->addShareRecipient($accessContext, $id, new ShareRecipient(TestShareRecipientTypePublicSecret::class, 'recipient2', null)); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); unset($share['last_updated']); $this->assertIsList($share['recipients']); $this->assertCount(2, $share['recipients']); @@ -3081,7 +3052,7 @@ public function testGetShareWithSecret(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -3094,11 +3065,10 @@ public function testGetShareWithSecret(): void { $this->manager->addShareRecipient(new ShareAccessContext($this->user2), $id, new ShareRecipient(TestShareRecipientTypePublicSecret::class, 'recipient4', null)); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare(new ShareAccessContext($this->user2), $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); usort($share['recipients'], fn (array $a, array $b): int => $a['value'] <=> $b['value']); $this->assertArrayHasKey('recipients', $share); @@ -3255,7 +3225,7 @@ public function testGetShareUniqueDisplayNames(): void { public function testGetShareDisabledOwner(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -3266,7 +3236,7 @@ public function testGetShareDisabledOwner(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->owner->setEnabled(false); @@ -3278,8 +3248,7 @@ public function testGetShareDisabledOwner(): void { } $share = $this->getShare(new ShareAccessContext(overrideChecks: true), $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals([ 'user_id' => 'owner', 'instance' => null, @@ -3294,7 +3263,7 @@ public function testGetShareDisabledOwner(): void { public function testGetShareDisabledInitiator(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -3304,7 +3273,7 @@ public function testGetShareDisabledInitiator(): void { $this->manager->addShareRecipient(new ShareAccessContext(currentUser: $this->user1), $id, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->user1->setEnabled(false); @@ -3316,8 +3285,7 @@ public function testGetShareDisabledInitiator(): void { } $share = $this->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals([ [ 'class' => TestShareRecipientType1::class, @@ -3367,7 +3335,7 @@ public function testGetShareDisabledInitiator(): void { public function testGetShares(): void { $accessContext = new ShareAccessContext($this->owner); - $before1 = $this->manager->generateTimestamp(); + $before1 = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id1 = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id1, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -3375,9 +3343,9 @@ public function testGetShares(): void { $this->manager->getShare($accessContext, $id1); $this->dbConnection->commit(); - $after1 = $this->manager->generateTimestamp(); + $after1 = $this->manager->getTime(); - $before2 = $this->manager->generateTimestamp(); + $before2 = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id2 = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id2, new ShareSource(TestShareSourceType2::class, 'source2')); @@ -3385,16 +3353,14 @@ public function testGetShares(): void { $this->manager->getShare($accessContext, $id2); $this->dbConnection->commit(); - $after2 = $this->manager->generateTimestamp(); + $after2 = $this->manager->getTime(); $shares = $this->getShares($accessContext, null, null, null, null); $this->assertCount(2, $shares); $this->assertIsArray($shares[0]); - $this->assertGreaterThanOrEqual($before1, $shares[0]['last_updated']); - $this->assertLessThanOrEqual($after1, $shares[0]['last_updated']); + $this->assertDateBetween($before1, $after1, $this->parseTime($shares[0]['last_updated'])); $this->assertIsArray($shares[1]); - $this->assertGreaterThanOrEqual($before2, $shares[1]['last_updated']); - $this->assertLessThanOrEqual($after2, $shares[1]['last_updated']); + $this->assertDateBetween($before2, $after2, $this->parseTime($shares[1]['last_updated'])); unset($shares[0]['last_updated'], $shares[1]['last_updated']); $this->assertEquals([ [ @@ -3562,8 +3528,7 @@ public function testGetShares(): void { $shares = $this->getShares($accessContext, TestShareSourceType1::class, null, null, null); $this->assertCount(1, $shares); $this->assertIsArray($shares[0]); - $this->assertGreaterThanOrEqual($before1, $shares[0]['last_updated']); - $this->assertLessThanOrEqual($after1, $shares[0]['last_updated']); + $this->assertDateBetween($before1, $after1, $this->parseTime($shares[0]['last_updated'])); unset($shares[0]['last_updated']); $this->assertEquals([ [ @@ -3651,8 +3616,7 @@ public function testGetShares(): void { $shares = $this->getShares($accessContext, TestShareSourceType1::class, 'source1', null, null); $this->assertCount(1, $shares); $this->assertIsArray($shares[0]); - $this->assertGreaterThanOrEqual($before1, $shares[0]['last_updated']); - $this->assertLessThanOrEqual($after1, $shares[0]['last_updated']); + $this->assertDateBetween($before1, $after1, $this->parseTime($shares[0]['last_updated'])); unset($shares[0]['last_updated']); $this->assertEquals([ [ @@ -3743,8 +3707,7 @@ public function testGetShares(): void { $shares = $this->getShares($accessContext, null, null, $id1, null); $this->assertCount(1, $shares); $this->assertIsArray($shares[0]); - $this->assertGreaterThanOrEqual($before2, $shares[0]['last_updated']); - $this->assertLessThanOrEqual($after2, $shares[0]['last_updated']); + $this->assertDateBetween($before2, $after2, $this->parseTime($shares[0]['last_updated'])); unset($shares[0]['last_updated']); $this->assertEquals([ [ @@ -3832,8 +3795,7 @@ public function testGetShares(): void { $shares = $this->getShares($accessContext, null, null, null, 1); $this->assertCount(1, $shares); $this->assertIsArray($shares[0]); - $this->assertGreaterThanOrEqual($before1, $shares[0]['last_updated']); - $this->assertLessThanOrEqual($after1, $shares[0]['last_updated']); + $this->assertDateBetween($before1, $after1, $this->parseTime($shares[0]['last_updated'])); unset($shares[0]['last_updated']); $this->assertEquals([ [ @@ -3978,14 +3940,13 @@ public function testInitiatorDeleted(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->manager->addShareRecipient(new ShareAccessContext(currentUser: $this->user1), $id, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->user1->delete(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->dbConnection->commit(); $share = $this->getShare(new ShareAccessContext(overrideChecks: true), $id); - $this->assertGreaterThanOrEqual($before, $share['last_updated']); - $this->assertLessThanOrEqual($after, $share['last_updated']); + $this->assertDateBetween($before, $after, $this->parseTime($share['last_updated'])); $this->assertEquals([ [ 'class' => TestShareRecipientType1::class, From 30a2c84509079d5e6b4a873bb4ce59d0d3eb4f6e Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 31 Jul 2026 18:08:26 +0200 Subject: [PATCH 6/7] fix: load share properties from legacy shares Signed-off-by: Robin Appelman --- .../lib/Sharing/LegacyBackend.php | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/apps/files_sharing/lib/Sharing/LegacyBackend.php b/apps/files_sharing/lib/Sharing/LegacyBackend.php index d3c8c06346c06..d8234eeb62a03 100644 --- a/apps/files_sharing/lib/Sharing/LegacyBackend.php +++ b/apps/files_sharing/lib/Sharing/LegacyBackend.php @@ -15,6 +15,8 @@ use NCU\Sharing\ISharingManager; use NCU\Sharing\Permission\ISharePermissionType; use NCU\Sharing\Permission\SharePermission; +use NCU\Sharing\Property\ISharePropertyType; +use NCU\Sharing\Property\ShareProperty; use NCU\Sharing\Recipient\IShareRecipientType; use NCU\Sharing\Recipient\ShareRecipient; use NCU\Sharing\Share; @@ -25,6 +27,7 @@ use OC\Core\Sharing\Property\ExpirationDateSharePropertyType; use OC\Core\Sharing\Property\LabelSharePropertyType; use OC\Core\Sharing\Property\NoteSharePropertyType; +use OC\Core\Sharing\Property\PasswordSharePropertyType; use OC\Core\Sharing\Recipient\EmailShareRecipientType; use OC\Core\Sharing\Recipient\GroupShareRecipientType; use OC\Core\Sharing\Recipient\TeamShareRecipientType; @@ -359,8 +362,19 @@ public function getShare(string $id): Share { null, ); - // TODO - $properties = []; + if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getExpirationDate())) { + throw new \Exception('All legacy shares sharing a share id don\'t have the expiration date'); + } + if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getPassword())) { + throw new \Exception('All legacy shares sharing a share id don\'t have the password'); + } + if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getLabel())) { + throw new \Exception('All legacy shares sharing a share id don\'t have the label'); + } + if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getNote())) { + throw new \Exception('All legacy shares sharing a share id don\'t have the note'); + } + $properties = $this->extractProperties($legacyShares[0]); if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getAttributes())) { throw new \Exception('All legacy shares sharing a share id don\'t have the same attributes'); @@ -600,4 +614,27 @@ private function splitLegacySharedWith(int $shareType, string $sharedWith): arra ]; } } + + /** + * @param IShare $share + * @return ShareProperty[] + */ + private function extractProperties(IShare $share): array { + $properties = []; + + if ($expire = $share->getExpirationDate()) { + $properties[] = new ShareProperty(ExpirationDateSharePropertyType::class, $expire->format(DateTimeInterface::ATOM)); + } + if ($password = $share->getPassword()) { + $properties[] = new ShareProperty(PasswordSharePropertyType::class, $password); + } + if ($label = $share->getLabel()) { + $properties[] = new ShareProperty(LabelSharePropertyType::class, $label); + } + if ($note = $share->getNote()) { + $properties[] = new ShareProperty(NoteSharePropertyType::class, $note); + } + + return $properties; + } } From ab890417df4e34181e26161e2cd0031da0409796 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sat, 1 Aug 2026 00:46:44 +0200 Subject: [PATCH 7/7] feat: keep a classname mapping instead of always storing the full name Signed-off-by: Robin Appelman --- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../Version1000Date20250929161325.php | 30 ++-- .../Version1000Date20260731171922.php | 85 ++++++++++++ lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + lib/private/Server.php | 1 + lib/private/Sharing/ClassMapper.php | 131 ++++++++++++++++++ lib/private/Sharing/SharingBackend.php | 54 ++++---- lib/private/Sharing/SharingManager.php | 12 +- tests/lib/Sharing/ClassMapperTest.php | 63 +++++++++ 11 files changed, 333 insertions(+), 47 deletions(-) create mode 100644 apps/sharing/lib/Migration/Version1000Date20260731171922.php create mode 100644 lib/private/Sharing/ClassMapper.php create mode 100644 tests/lib/Sharing/ClassMapperTest.php diff --git a/apps/sharing/composer/composer/autoload_classmap.php b/apps/sharing/composer/composer/autoload_classmap.php index 0d6e87b5cfd0d..103888349620f 100644 --- a/apps/sharing/composer/composer/autoload_classmap.php +++ b/apps/sharing/composer/composer/autoload_classmap.php @@ -26,5 +26,6 @@ 'OCA\\Sharing\\Controller\\ApiV1Controller' => $baseDir . '/../lib/Controller/ApiV1Controller.php', 'OCA\\Sharing\\Middleware\\ShareApiEnabledMiddleware' => $baseDir . '/../lib/Middleware/ShareApiEnabledMiddleware.php', 'OCA\\Sharing\\Migration\\Version1000Date20250929161325' => $baseDir . '/../lib/Migration/Version1000Date20250929161325.php', + 'OCA\\Sharing\\Migration\\Version1000Date20260731171922' => $baseDir . '/../lib/Migration/Version1000Date20260731171922.php', 'OCA\\Sharing\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', ); diff --git a/apps/sharing/composer/composer/autoload_static.php b/apps/sharing/composer/composer/autoload_static.php index 22a3d83d9e4d0..f8dcfbd61f918 100644 --- a/apps/sharing/composer/composer/autoload_static.php +++ b/apps/sharing/composer/composer/autoload_static.php @@ -41,6 +41,7 @@ class ComposerStaticInitSharing 'OCA\\Sharing\\Controller\\ApiV1Controller' => __DIR__ . '/..' . '/../lib/Controller/ApiV1Controller.php', 'OCA\\Sharing\\Middleware\\ShareApiEnabledMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/ShareApiEnabledMiddleware.php', 'OCA\\Sharing\\Migration\\Version1000Date20250929161325' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20250929161325.php', + 'OCA\\Sharing\\Migration\\Version1000Date20260731171922' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20260731171922.php', 'OCA\\Sharing\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', ); diff --git a/apps/sharing/lib/Migration/Version1000Date20250929161325.php b/apps/sharing/lib/Migration/Version1000Date20250929161325.php index 1cea07e223102..9f59295a82db7 100644 --- a/apps/sharing/lib/Migration/Version1000Date20250929161325.php +++ b/apps/sharing/lib/Migration/Version1000Date20250929161325.php @@ -26,9 +26,17 @@ final class Version1000Date20250929161325 extends SimpleMigrationStep { public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { $schema = $schemaClosure(); - // TODO: Add mapping table for class names // TODO: Check indexes + $mappingTable = $schema->createTable('sharing_classmap'); + $mappingTable->addColumn('class_id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $mappingTable->addColumn('class_name', Types::STRING, ['length' => 64]); + $mappingTable->setPrimaryKey(['class_id']); + $mappingTable->addUniqueIndex(['class_name']); + $shareTable = $schema->createTable('sharing_share'); $shareTable->addColumn('id', Types::BIGINT); $shareTable->addColumn('owner_user_id', Types::STRING, ['length' => 64]); @@ -39,38 +47,42 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $sourcesTable = $schema->createTable('sharing_share_sources'); $sourcesTable->addColumn('share_id', Types::BIGINT); - $sourcesTable->addColumn('source_class', Types::STRING, ['length' => 64]); + $sourcesTable->addColumn('source_class_id', Types::INTEGER); $sourcesTable->addColumn('source_value', Types::STRING, ['length' => 255]); - $sourcesTable->setPrimaryKey(['share_id', 'source_class', 'source_value']); + $sourcesTable->setPrimaryKey(['share_id', 'source_class_id', 'source_value']); $sourcesTable->addForeignKeyConstraint($shareTable->getName(), ['share_id'], ['id'], ['onDelete' => 'CASCADE']); + $sourcesTable->addForeignKeyConstraint($mappingTable->getName(), ['source_class_id'], ['class_id']); // TODO: Add possibility to mask permissions for recipients. For reshares the user may only mask permissions for their child recipients, not their self recipients $recipientsTable = $schema->createTable('sharing_share_recipients'); $recipientsTable->addColumn('share_id', Types::BIGINT); - $recipientsTable->addColumn('recipient_class', Types::STRING, ['length' => 64]); + $recipientsTable->addColumn('recipient_class_id', Types::INTEGER); $recipientsTable->addColumn('recipient_value', Types::STRING, ['length' => 255]); $recipientsTable->addColumn('recipient_instance', Types::STRING, ['length' => 128, 'notnull' => false]); $recipientsTable->addColumn('recipient_secret', Types::STRING, ['length' => 32]); $recipientsTable->addColumn('initiator_user_id', Types::STRING, ['length' => 64]); $recipientsTable->addColumn('initiator_instance', Types::STRING, ['length' => 128, 'notnull' => false]); - $recipientsTable->setPrimaryKey(['share_id', 'recipient_class', 'recipient_value']); + $recipientsTable->setPrimaryKey(['share_id', 'recipient_class_id', 'recipient_value']); $recipientsTable->addForeignKeyConstraint($shareTable->getName(), ['share_id'], ['id'], ['onDelete' => 'CASCADE']); // TODO: Maybe needs composite index with share_id $recipientsTable->addUniqueIndex(['recipient_secret']); + $recipientsTable->addForeignKeyConstraint($mappingTable->getName(), ['recipient_class_id'], ['class_id']); $propertiesTable = $schema->createTable('sharing_share_properties'); $propertiesTable->addColumn('share_id', Types::BIGINT); - $propertiesTable->addColumn('property_class', Types::STRING, ['length' => 64]); + $propertiesTable->addColumn('property_class_id', Types::INTEGER); $propertiesTable->addColumn('property_value', Types::STRING, ['length' => 1000, 'notnull' => false]); - $propertiesTable->setPrimaryKey(['share_id', 'property_class']); + $propertiesTable->setPrimaryKey(['share_id', 'property_class_id']); $propertiesTable->addForeignKeyConstraint($shareTable->getName(), ['share_id'], ['id'], ['onDelete' => 'CASCADE']); + $propertiesTable->addForeignKeyConstraint($mappingTable->getName(), ['property_class_id'], ['class_id']); $permissionsTable = $schema->createTable('sharing_share_permissions'); $permissionsTable->addColumn('share_id', Types::BIGINT); - $permissionsTable->addColumn('permission_class', Types::STRING, ['length' => 64]); + $permissionsTable->addColumn('permission_class_id', Types::INTEGER); $permissionsTable->addColumn('permission_enabled', Types::BOOLEAN); - $permissionsTable->setPrimaryKey(['share_id', 'permission_class']); + $permissionsTable->setPrimaryKey(['share_id', 'permission_class_id']); $permissionsTable->addForeignKeyConstraint($shareTable->getName(), ['share_id'], ['id'], ['onDelete' => 'CASCADE']); + $permissionsTable->addForeignKeyConstraint($mappingTable->getName(), ['permission_class_id'], ['class_id']); return $schema; } diff --git a/apps/sharing/lib/Migration/Version1000Date20260731171922.php b/apps/sharing/lib/Migration/Version1000Date20260731171922.php new file mode 100644 index 0000000000000..78ee1ef1e1844 --- /dev/null +++ b/apps/sharing/lib/Migration/Version1000Date20260731171922.php @@ -0,0 +1,85 @@ +hasTable('sharing_classmap')) { + $table = $schema->createTable('sharing_classmap'); + $table->addColumn('class_id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('class_name', Types::STRING, ['length' => 64]); + $table->setPrimaryKey(['class_id']); + $table->addUniqueIndex(['class_name']); + } + + $sourcesTable = $schema->getTable('sharing_share_sources'); + if ($sourcesTable->hasColumn('source_class')) { + $sourcesTable->dropColumn('source_class'); + $sourcesTable->addColumn('source_class_id', Types::INTEGER); + $sourcesTable->dropPrimaryKey(); + $sourcesTable->setPrimaryKey(['share_id', 'source_class_id', 'source_value']); + $sourcesTable->addForeignKeyConstraint('sharing_classmap', ['source_class_id'], ['class_id']); + } + + $recipientsTable = $schema->getTable('sharing_share_recipients'); + if ($recipientsTable->hasColumn('recipient_class')) { + $recipientsTable->dropColumn('recipient_class'); + $recipientsTable->addColumn('recipient_class_id', Types::INTEGER); + $recipientsTable->dropPrimaryKey(); + $recipientsTable->setPrimaryKey(['share_id', 'recipient_class_id', 'recipient_value']); + $recipientsTable->addForeignKeyConstraint('sharing_classmap', ['recipient_class_id'], ['class_id']); + } + + $propertiesTable = $schema->getTable('sharing_share_properties'); + if ($propertiesTable->hasColumn('property_class')) { + $propertiesTable->dropColumn('property_class'); + $propertiesTable->addColumn('property_class_id', Types::INTEGER); + $propertiesTable->dropPrimaryKey(); + $propertiesTable->setPrimaryKey(['share_id', 'property_class_id']); + $propertiesTable->addForeignKeyConstraint('sharing_classmap', ['property_class_id'], ['class_id']); + } + + $permissionsTable = $schema->getTable('sharing_share_permissions'); + if ($permissionsTable->hasColumn('permission_class')) { + $permissionsTable->dropColumn('permission_class'); + $permissionsTable->addColumn('permission_class_id', Types::INTEGER); + $permissionsTable->dropPrimaryKey(); + $permissionsTable->setPrimaryKey(['share_id', 'permission_class_id']); + $permissionsTable->addForeignKeyConstraint('sharing_classmap', ['permission_class_id'], ['class_id']); + } + + return $schema; + } + + #[Override] + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index f661fe8152658..eff2d4654732f 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -2297,6 +2297,7 @@ 'OC\\Share20\\UserDeletedListener' => $baseDir . '/lib/private/Share20/UserDeletedListener.php', 'OC\\Share20\\UserRemovedListener' => $baseDir . '/lib/private/Share20/UserRemovedListener.php', 'OC\\Share\\Constants' => $baseDir . '/lib/private/Share/Constants.php', + 'OC\\Sharing\\ClassMapper' => $baseDir . '/lib/private/Sharing/ClassMapper.php', 'OC\\Sharing\\ISharingLegacyBackend' => $baseDir . '/lib/private/Sharing/ISharingLegacyBackend.php', 'OC\\Sharing\\SharingBackend' => $baseDir . '/lib/private/Sharing/SharingBackend.php', 'OC\\Sharing\\SharingManager' => $baseDir . '/lib/private/Sharing/SharingManager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 422bf9ccaf828..6c9abefe98c4f 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -2338,6 +2338,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Share20\\UserDeletedListener' => __DIR__ . '/../../..' . '/lib/private/Share20/UserDeletedListener.php', 'OC\\Share20\\UserRemovedListener' => __DIR__ . '/../../..' . '/lib/private/Share20/UserRemovedListener.php', 'OC\\Share\\Constants' => __DIR__ . '/../../..' . '/lib/private/Share/Constants.php', + 'OC\\Sharing\\ClassMapper' => __DIR__ . '/../../..' . '/lib/private/Sharing/ClassMapper.php', 'OC\\Sharing\\ISharingLegacyBackend' => __DIR__ . '/../../..' . '/lib/private/Sharing/ISharingLegacyBackend.php', 'OC\\Sharing\\SharingBackend' => __DIR__ . '/../../..' . '/lib/private/Sharing/SharingBackend.php', 'OC\\Sharing\\SharingManager' => __DIR__ . '/../../..' . '/lib/private/Sharing/SharingManager.php', diff --git a/lib/private/Server.php b/lib/private/Server.php index 5b56ba4cd4b01..d054b84330ba7 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -1153,6 +1153,7 @@ function () use ($c) { $this->registerAlias(\NCU\Sharing\ISharingRegistry::class, \OC\Sharing\SharingRegistry::class); $this->registerAlias(\NCU\Sharing\ISharingManager::class, \OC\Sharing\SharingManager::class); + $this->registerAlias(\NCU\Sharing\ISharingBackend::class, \OC\Sharing\SharingBackend::class); $this->connectDispatcher(); } diff --git a/lib/private/Sharing/ClassMapper.php b/lib/private/Sharing/ClassMapper.php new file mode 100644 index 0000000000000..3525ca90ebda2 --- /dev/null +++ b/lib/private/Sharing/ClassMapper.php @@ -0,0 +1,131 @@ + $map */ + private array $map = []; + private array $reverseMap = []; + private bool $loaded = false; + + public function __construct( + private readonly IDBConnection $connection, + ) { + } + + /** + * @param array{class_id: int|string, class_name: string} $row + */ + private function insertRow(array $row): void { + $id = (int)$row['class_id']; + $class = $row['class_name']; + $this->map[$id] = $class; + $this->reverseMap[$class] = $id; + } + + private function loadFromDb(): void { + if ($this->loaded) { + return; + } + + $query = $this->connection->getTypedQueryBuilder(); + $query->select('class_id', 'class_name') + ->from('sharing_classmap'); + $rows = $query->executeQuery()->fetchAll(); + + foreach ($rows as $row) { + $this->insertRow($row); + } + + $this->loaded = true; + } + + private function loadFromDbByName(string $className): ?int { + $query = $this->connection->getTypedQueryBuilder(); + $query->select('class_id', 'class_name') + ->from('sharing_classmap') + ->where($query->expr()->eq('class_name', $query->createNamedParameter($className))); + $row = $query->executeQuery()->fetchAssociative(); + + if ($row) { + $this->insertRow($row); + return (int)$row['class_id']; + } else { + return null; + } + } + + private function loadFromDbById(int $id): ?string { + $query = $this->connection->getTypedQueryBuilder(); + $query->select('class_id', 'class_name') + ->from('sharing_classmap') + ->where($query->expr()->eq('class_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + $row = $query->executeQuery()->fetchAssociative(); + + if ($row) { + $this->insertRow($row); + return $row['class_name']; + } else { + return null; + } + } + + private function insert(string $className): int { + $query = $this->connection->getTypedQueryBuilder(); + $query->insert('sharing_classmap') + ->values([ + 'class_name' => $query->createNamedParameter($className) + ]); + try { + $query->executeStatement(); + $id = $query->getLastInsertId(); + $this->map[$id] = $className; + $this->reverseMap[$className] = $id; + return $id; + } catch (Exception $exception) { + // handle concurrent inserts + if ($exception->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + $id = $this->loadFromDbByName($className); + if (!$id) { + throw new \Exception("Failed to insert '$className' into sharing_classmap, duplicate on insert but can't fetch it either"); + } else { + return $id; + } + } else { + throw $exception; + } + } + } + + public function getClassId(string $class): int { + $this->loadFromDb(); + if (isset($this->reverseMap[$class])) { + return $this->reverseMap[$class]; + } + return $this->insert($class); + } + + public function getClassName(int $id): string { + $this->loadFromDb(); + if (isset($this->map[$id])) { + return $this->map[$id]; + } + $class = $this->loadFromDbById($id); + if ($class) { + return $class; + } else { + throw new \Exception("Unknown mapped class '$id'"); + } + } + +} diff --git a/lib/private/Sharing/SharingBackend.php b/lib/private/Sharing/SharingBackend.php index d6c4b0f473240..d2df7fddc74f0 100644 --- a/lib/private/Sharing/SharingBackend.php +++ b/lib/private/Sharing/SharingBackend.php @@ -38,8 +38,6 @@ use OCP\L10N\IFactory; use RuntimeException; -// TODO: Add mapping table for class names in sources, recipients, permissions and properties - /** * @psalm-import-type SharingShare from Share */ @@ -53,6 +51,7 @@ public function __construct( private IAppConfig $appConfig, private ISharingRegistry $registry, private ISharingManager $manager, + private ClassMapper $classMapper, ) { $this->l10n = $factory->get('sharing'); } @@ -127,7 +126,7 @@ public function addShareSource(string $id, ShareSource $source): void { ->insert('sharing_share_sources') ->values([ 'share_id' => $qb->createNamedParameter($id), - 'source_class' => $qb->createNamedParameter($source->class), + 'source_class_id' => $qb->createNamedParameter($this->classMapper->getClassId($source->class), IQueryBuilder::PARAM_INT), 'source_value' => $qb->createNamedParameter($source->value), ]) ->executeStatement(); @@ -146,7 +145,7 @@ public function removeShareSource(string $id, ShareSource $source): void { $rowCount = $qb ->delete('sharing_share_sources') ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('source_class', $qb->createNamedParameter($source->class))) + ->andWhere($qb->expr()->eq('source_class_id', $qb->createNamedParameter($this->classMapper->getClassId($source->class), IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('source_value', $qb->createNamedParameter($source->value))) ->executeStatement(); if ($rowCount === 0) { @@ -160,7 +159,7 @@ public function onSourceDeleted(ShareSource $source): array { $result = $qb ->selectDistinct('share_id') ->from('sharing_share_sources') - ->where($qb->expr()->eq('source_class', $qb->createNamedParameter($source->class))) + ->where($qb->expr()->eq('source_class_id', $qb->createNamedParameter($this->classMapper->getClassId($source->class), IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('source_value', $qb->createNamedParameter($source->value))) ->executeQuery(); @@ -175,7 +174,7 @@ public function onSourceDeleted(ShareSource $source): array { $qb = $this->connection->getQueryBuilder(); $qb ->delete('sharing_share_sources') - ->where($qb->expr()->eq('source_class', $qb->createNamedParameter($source->class))) + ->where($qb->expr()->eq('source_class_id', $qb->createNamedParameter($this->classMapper->getClassId($source->class), IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('source_value', $qb->createNamedParameter($source->value))) ->executeStatement(); @@ -197,7 +196,7 @@ public function addShareRecipient(string $id, ShareRecipient $recipient): void { $values = [ 'share_id' => $qb->createNamedParameter($id), - 'recipient_class' => $qb->createNamedParameter($recipient->class), + 'recipient_class_id' => $qb->createNamedParameter($this->classMapper->getClassId($recipient->class), IQueryBuilder::PARAM_INT), 'recipient_value' => $qb->createNamedParameter($recipient->value), 'recipient_instance' => $qb->createNamedParameter($recipient->instance), 'recipient_secret' => $qb->createNamedParameter($recipient->secret), @@ -224,7 +223,7 @@ public function removeShareRecipient(string $id, ShareRecipient $recipient): voi $rowCount = $qb ->delete('sharing_share_recipients') ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('recipient_class', $qb->createNamedParameter($recipient->class))) + ->andWhere($qb->expr()->eq('recipient_class_id', $qb->createNamedParameter($this->classMapper->getClassId($recipient->class), IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('recipient_value', $qb->createNamedParameter($recipient->value))) ->andWhere( $recipient->instance === null @@ -243,7 +242,7 @@ public function onRecipientDeleted(ShareRecipient $recipient): array { $result = $qb ->selectDistinct('share_id') ->from('sharing_share_recipients') - ->where($qb->expr()->eq('recipient_class', $qb->createNamedParameter($recipient->class))) + ->where($qb->expr()->eq('recipient_class_id', $qb->createNamedParameter($this->classMapper->getClassId($recipient->class), IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('recipient_value', $qb->createNamedParameter($recipient->value))) ->andWhere( $recipient->instance === null @@ -263,7 +262,7 @@ public function onRecipientDeleted(ShareRecipient $recipient): array { $qb = $this->connection->getQueryBuilder(); $qb ->delete('sharing_share_recipients') - ->where($qb->expr()->eq('recipient_class', $qb->createNamedParameter($recipient->class))) + ->where($qb->expr()->eq('recipient_class_id', $qb->createNamedParameter($this->classMapper->getClassId($recipient->class), IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('recipient_value', $qb->createNamedParameter($recipient->value))) ->andWhere( $recipient->instance === null @@ -327,7 +326,7 @@ public function updateShareRecipientSecret(string $id, ShareRecipient $recipient ->update('sharing_share_recipients') ->set('recipient_secret', $qb->createNamedParameter($secret)) ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('recipient_class', $qb->createNamedParameter($recipient->class))) + ->andWhere($qb->expr()->eq('recipient_class_id', $qb->createNamedParameter($this->classMapper->getClassId($recipient->class), IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('recipient_value', $qb->createNamedParameter($recipient->value))) ->andWhere( $recipient->instance === null @@ -356,7 +355,7 @@ public function createShareProperty(string $id, ShareProperty $property): void { ->insert('sharing_share_properties') ->values([ 'share_id' => $qb->createNamedParameter($id), - 'property_class' => $qb->createNamedParameter($property->class), + 'property_class_id' => $qb->createNamedParameter($this->classMapper->getClassId($property->class), IQueryBuilder::PARAM_INT), 'property_value' => $qb->createNamedParameter($value), ]) ->executeStatement(); @@ -381,7 +380,7 @@ public function updateShareProperty(string $id, ShareProperty $property): void { ->select('sp.property_value') ->from('sharing_share_properties', 'sp') ->where($qb->expr()->eq('sp.share_id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('sp.property_class', $qb->createNamedParameter($property->class))); + ->andWhere($qb->expr()->eq('sp.property_class_id', $qb->createNamedParameter($this->classMapper->getClassId($property->class), IQueryBuilder::PARAM_INT))); /** @var string|false $oldValue */ $oldValue = $qb->executeQuery()->fetchOne(); @@ -397,7 +396,7 @@ public function updateShareProperty(string $id, ShareProperty $property): void { ->update('sharing_share_properties') ->set('property_value', $qb->createNamedParameter($value)) ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('property_class', $qb->createNamedParameter($property->class))) + ->andWhere($qb->expr()->eq('property_class_id', $qb->createNamedParameter($this->classMapper->getClassId($property->class), IQueryBuilder::PARAM_INT))) ->executeStatement(); if ($rowCount === 0) { throw new ShareNotFoundException(); @@ -412,7 +411,7 @@ public function createSharePermission(string $id, SharePermission $permission): ->insert('sharing_share_permissions') ->values([ 'share_id' => $qb->createNamedParameter($id), - 'permission_class' => $qb->createNamedParameter($permission->class), + 'permission_class_id' => $qb->createNamedParameter($this->classMapper->getClassId($permission->class), IQueryBuilder::PARAM_INT), 'permission_enabled' => $qb->createNamedParameter($permission->enabled, IQueryBuilder::PARAM_BOOL), ]) ->executeStatement(); @@ -432,7 +431,7 @@ public function updateSharePermission(string $id, SharePermission $permission): ->update('sharing_share_permissions') ->set('permission_enabled', $qb->createNamedParameter($permission->enabled, IQueryBuilder::PARAM_BOOL)) ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('permission_class', $qb->createNamedParameter($permission->class))) + ->andWhere($qb->expr()->eq('permission_class_id', $qb->createNamedParameter($this->classMapper->getClassId($permission->class), IQueryBuilder::PARAM_INT))) ->executeStatement(); if ($rowCount === 0) { throw new ShareNotFoundException(); @@ -450,13 +449,14 @@ public function selectSharePermissionPreset(string $id, string $permissionPreset $permissionPresetCompatiblePermissionTypeClasses = $this->registry->getPermissionPresetCompatiblePermissionTypeClasses()[$permissionPresetClass]; foreach (array_chunk($permissionPresetCompatiblePermissionTypeClasses, 1000) as $chunk) { + $chunkIds = array_map($this->classMapper->getClassId(...), $chunk); // Some permissions might not be compatible with the share, just ignore it and update the ones that are present. $qb = $this->connection->getQueryBuilder(); $qb ->update('sharing_share_permissions') ->set('permission_enabled', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)) ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->in('permission_class', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY))) + ->andWhere($qb->expr()->in('permission_class_id', $qb->createNamedParameter($chunkIds, IQueryBuilder::PARAM_INT_ARRAY))) ->executeStatement(); } @@ -590,7 +590,7 @@ private function list(ShareAccessContext $accessContext, ?string $filterShareID, foreach ($recipientTypeValues as $recipientTypeClass => $recipientValues) { $qb->orWhere($qb->expr()->andX( - $qb->expr()->eq('sr.recipient_class', $qb->createNamedParameter($recipientTypeClass)), + $qb->expr()->eq('sr.recipient_class_id', $qb->createNamedParameter($this->classMapper->getClassId($recipientTypeClass), IQueryBuilder::PARAM_INT)), // TODO: Add chunking $qb->expr()->in('sr.recipient_value', $qb->createNamedParameter($recipientValues, IQueryBuilder::PARAM_STR_ARRAY)), $qb->expr()->isNull('sr.recipient_instance'), @@ -634,7 +634,7 @@ private function list(ShareAccessContext $accessContext, ?string $filterShareID, if ($filterSourceTypeClass !== null) { $sourceTypeFilters = [ $qb->expr()->eq('s.id', 'ss.share_id'), - $qb->expr()->eq('ss.source_class', $qb->createNamedParameter($filterSourceTypeClass)), + $qb->expr()->eq('ss.source_class_id', $qb->createNamedParameter($this->classMapper->getClassId($filterSourceTypeClass), IQueryBuilder::PARAM_INT)), ]; if ($filterSourceTypeValue !== null) { @@ -704,7 +704,7 @@ private function list(ShareAccessContext $accessContext, ?string $filterShareID, $qb ->select( 'ss.share_id', - 'ss.source_class', + 'ss.source_class_id', 'ss.source_value', ) ->from('sharing_share_sources', 'ss') @@ -713,7 +713,7 @@ private function list(ShareAccessContext $accessContext, ?string $filterShareID, $result = $qb->executeQuery(); foreach ($result->fetchAll() as $row) { /** @var class-string $typeClass */ - $typeClass = $row['source_class']; + $typeClass = $this->classMapper->getClassName((int)$row['source_class_id']); if (!isset($registrySourceTypes[$typeClass])) { // Skip sources that are currently not compatible, but don't remove them. continue; @@ -741,7 +741,7 @@ private function list(ShareAccessContext $accessContext, ?string $filterShareID, $qb ->select( 'sr.share_id', - 'sr.recipient_class', + 'sr.recipient_class_id', 'sr.recipient_value', 'sr.recipient_instance', 'sr.recipient_secret', @@ -753,7 +753,7 @@ private function list(ShareAccessContext $accessContext, ?string $filterShareID, foreach ($qb->executeQuery()->fetchAll() as $row) { /** @var class-string $typeClass */ - $typeClass = $row['recipient_class']; + $typeClass = $this->classMapper->getClassName((int)$row['recipient_class_id']); if (!isset($registryRecipientTypes[$typeClass])) { // Skip recipients that are currently not compatible, but don't remove them. continue; @@ -858,7 +858,7 @@ private function list(ShareAccessContext $accessContext, ?string $filterShareID, $qb ->select( 'sp.share_id', - 'sp.property_class', + 'sp.property_class_id', 'sp.property_value', ) ->from('sharing_share_properties', 'sp') @@ -873,7 +873,7 @@ private function list(ShareAccessContext $accessContext, ?string $filterShareID, } /** @var class-string $propertyTypeClass */ - $propertyTypeClass = $row['property_class']; + $propertyTypeClass = $this->classMapper->getClassName((int)$row['property_class_id']); if (!isset($registryPropertyTypeCompatibleSourceTypeClasses[$propertyTypeClass], $registryPropertyTypeCompatibleRecipientTypeClasses[$propertyTypeClass])) { // Skip properties that are currently not compatible, but don't remove them. continue; @@ -928,7 +928,7 @@ private function list(ShareAccessContext $accessContext, ?string $filterShareID, $qb ->select( 'sp.share_id', - 'sp.permission_class', + 'sp.permission_class_id', 'sp.permission_enabled', ) ->from('sharing_share_permissions', 'sp') @@ -940,7 +940,7 @@ private function list(ShareAccessContext $accessContext, ?string $filterShareID, $id = (string)$row['share_id']; /** @var class-string $permissionTypeClass */ - $permissionTypeClass = $row['permission_class']; + $permissionTypeClass = $this->classMapper->getClassName((int)$row['permission_class_id']); if (!isset($shareCompatiblePermissionTypeClasses[$id][$permissionTypeClass])) { // Skip permissions that are currently not compatible, but don't remove them. continue; diff --git a/lib/private/Sharing/SharingManager.php b/lib/private/Sharing/SharingManager.php index 542a3c09251f0..95acf03238b89 100644 --- a/lib/private/Sharing/SharingManager.php +++ b/lib/private/Sharing/SharingManager.php @@ -69,8 +69,6 @@ final class SharingManager implements ISharingManager, IEventListener { private readonly IL10N $l10n; - private readonly ISharingBackend $backend; - /** @var array */ private array $currentlyUpdatingLegacyShares; @@ -83,18 +81,10 @@ public function __construct( private readonly ISharingRegistry $registry, private readonly IManager $legacyManager, private readonly ClockInterface $clock, - IAppConfig $appConfig, + private readonly ISharingBackend $backend, ) { $this->randomizer = new Randomizer(); $this->l10n = $l10nFactory->get('sharing'); - $this->backend = new SharingBackend( - $l10nFactory, - $dbConnection, - $userManager, - $appConfig, - $registry, - $this, - ); $eventDispatcher->addServiceListener(BeforeUserDeletedEvent::class, self::class); $eventDispatcher->addServiceListener(AfterShareModifiedEvent::class, self::class); diff --git a/tests/lib/Sharing/ClassMapperTest.php b/tests/lib/Sharing/ClassMapperTest.php new file mode 100644 index 0000000000000..c1d324aafceaa --- /dev/null +++ b/tests/lib/Sharing/ClassMapperTest.php @@ -0,0 +1,63 @@ +connection = Server::get(IDBConnection::class); + $this->clearMappings(); + $this->classMapper = $this->getMapper(); + } + + private function clearMappings() { + $query = $this->connection->getTypedQueryBuilder(); + $query->delete('sharing_classmap'); + $query->executeStatement(); + } + + private function getMapper(): ClassMapper { + return new ClassMapper($this->connection); + } + + public function testGetInsert() { + $id = $this->classMapper->getClassId('foobar'); + $this->assertEquals($id, $this->classMapper->getClassId('foobar')); + $this->assertEquals('foobar', $this->classMapper->getClassName($id)); + + $this->assertEquals($id, $this->getMapper()->getClassId('foobar')); + } + + public function testInsertConcurrent() { + $concurrentMapper = $this->getMapper(); + + // trigger a load + $this->classMapper->getClassId('other'); + + $id = $concurrentMapper->getClassId('foobar'); + $this->assertEquals($id, $this->classMapper->getClassId('foobar')); + $this->assertEquals('foobar', $this->classMapper->getClassName($id)); + } + + public function testGetUnknownId() { + $this->expectException(\Exception::class); + $this->classMapper->getClassName(1); + } +}