From 82a9a45c86e7ca746f528d54e13359e85f81247c Mon Sep 17 00:00:00 2001 From: provokateurin Date: Thu, 23 Jul 2026 08:57:46 +0200 Subject: [PATCH 1/8] feat(files_sharing): Add LegacyBackend for Sharing Signed-off-by: provokateurin # Conflicts: # lib/private/Sharing/SharingManager.php # Conflicts: # build/rector-strict.php # psalm-strict.xml --- .../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 | 550 +++++++++++++++ .../tests/Sharing/LegacyBackendTest.php | 659 ++++++++++++++++++ build/rector-strict.php | 2 + 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 | 232 +++++- .../Share/Events/AfterShareModifiedEvent.php | 37 + lib/public/Share/IShare.php | 7 + lib/unstable/Sharing/ISharingManager.php | 12 + psalm-strict.xml | 2 + 17 files changed, 1564 insertions(+), 37 deletions(-) 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 create mode 100644 lib/public/Share/Events/AfterShareModifiedEvent.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..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; @@ -36,6 +38,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 +52,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 +62,14 @@ 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; @@ -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..06033b4f5870e --- /dev/null +++ b/apps/files_sharing/lib/Sharing/LegacyBackend.php @@ -0,0 +1,550 @@ +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); + // 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; + $legacyShares[$legacyShare->getFullId()] = $legacyShare; + } + + $validLegacyShares[$legacyShare->getFullId()] = true; + } + } + + $invalidLegacyShares = array_diff(array_keys($legacyShares), array_keys($validLegacyShares)); + foreach ($invalidLegacyShares as $invalidLegacyShareId) { + $this->legacyManager->deleteShare($legacyShares[$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; + $recipients[$recipientTypeClass] ??= []; + $uniqueId = $isTokenRecipient ? $legacyShare->getToken() : $legacyShare->getSharedWith(); + /** @psalm-suppress ArgumentTypeCoercion */ + $recipients[$recipientTypeClass][$uniqueId] ??= 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), + array_merge(...array_values($recipients)), + $properties, + $permissions, + ); + } + + #[\Override] + 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(); + $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 + */ + #[\Override] + public 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); + } + + /** + * Get the unified share if for a legacy share id + * + * @return non-empty-string|null + */ + 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; + } + + /** + * @param non-empty-string $id + * @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..7c56625e6434d --- /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, + 'user secret', + new ShareUser( + $owner->getUID(), + null, + ), + ), + // TODO: Test federation + new ShareRecipient( + GroupShareRecipientType::class, + $group->getGID(), + null, + 'group secret', + new ShareUser( + $owner->getUID(), + null, + ), + ), + new ShareRecipient( + TokenShareRecipientType::class, + 'token', + null, + 'token secret', + new ShareUser( + $owner->getUID(), + null, + ), + ), + new ShareRecipient( + EmailShareRecipientType::class, + 'example@example.com', + null, + 'email secret', + new ShareUser( + $owner->getUID(), + null, + ), + ), + ], + $circle !== null ? [ + new ShareRecipient( + TeamShareRecipientType::class, + $circle->getSingleId(), + null, + 'team 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 a0c89b6b04ddb..9ee0fd43dae5a 100644 --- a/build/rector-strict.php +++ b/build/rector-strict.php @@ -49,6 +49,8 @@ $nextcloudDir . '/apps/files/tests/Sharing', $nextcloudDir . '/lib/public/AppFramework/ORM', $nextcloudDir . '/lib/private/AppFramework/ORM', + $nextcloudDir . '/apps/files_sharing/lib/Sharing', + $nextcloudDir . '/apps/files_sharing/tests/Sharing', ]) ->withAutoloadPaths([ // ensure rector properly autoload the public interfaces diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index d139d22363fb2..3dc718285bad2 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -924,6 +924,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 eb6d9a58341c1..b482739a62338 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -965,6 +965,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 703087a53a405..c743d75d74493 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; @@ -643,6 +644,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']); } @@ -754,6 +756,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', [ @@ -1068,7 +1071,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] @@ -1461,6 +1468,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 3ea440a885a68..4606f432ad389 100644 --- a/lib/private/Share20/Share.php +++ b/lib/private/Share20/Share.php @@ -121,6 +121,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 468be32cb0dee..9b2dc81a26c4e 100644 --- a/lib/private/Sharing/SharingManager.php +++ b/lib/private/Sharing/SharingManager.php @@ -12,6 +12,7 @@ use Exception; use NCU\Sharing\Event\SharesDefaultSetEvent; 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,14 @@ 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\Share\IShare; use OCP\Snowflake\ISnowflakeGenerator; use OCP\User\Events\BeforeUserDeletedEvent; use Psr\Clock\ClockInterface; @@ -54,27 +63,39 @@ /** * @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 { + /** @var array $currentlyUpdatingLegacyShares */ + public array $currentlyUpdatingLegacyShares = []; - private IL10N $l10n; + private readonly Randomizer $randomizer; + + private readonly IL10N $l10n; + + public bool $validateLegacySharesOnUpdate = false; public function __construct( IEventDispatcher $eventDispatcher, - private IUserManager $userManager, - private IFactory $l10nFactory, - private ISnowflakeGenerator $snowflakeGenerator, - private IDBConnection $dbConnection, - private ISharingRegistry $registry, - private ISharingBackend $backend, - private ClockInterface $clock, + private readonly IUserManager $userManager, + private readonly IFactory $l10nFactory, + private readonly ISnowflakeGenerator $snowflakeGenerator, + private readonly IDBConnection $dbConnection, + private readonly ISharingRegistry $registry, + private readonly IManager $legacyManager, + private readonly ISharingBackend $backend, + private readonly ClockInterface $clock, ) { $this->randomizer = new Randomizer(); $this->l10n = $l10nFactory->get('sharing'); $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); } #[\Override] @@ -178,7 +199,11 @@ public function onOwnerDeleted(ShareAccessContext $accessContext, ShareUser $own $legacyBackend = $this->registry->getLegacyBackend(); if ($legacyBackend instanceof ISharingLegacyBackend) { foreach ($ids as $id) { - $legacyBackend->deleteShare($id); + try { + $legacyBackend->deleteShare($id); + } catch (ShareNotFoundException) { + // ignore + } } } } @@ -677,9 +702,40 @@ public function getShares( ): array { $this->assertInTransaction(); + if (($currentUser = $accessContext->currentUser) instanceof IUser) { + $this->importSharesFromLegacyBackend($currentUser); + } + return $this->backend->getShares($accessContext, $filterSourceTypeClass, $filterSourceTypeValue, $lastShareID, $limit); } + /** + * @return list + */ + #[\Override] + public function importSharesFromLegacyBackend(IUser $user): array { + $this->assertInTransaction(); + + $share = []; + + $legacyBackend = $this->registry->getLegacyBackend(); + if ($legacyBackend instanceof ISharingLegacyBackend) { + foreach ($legacyBackend->getUnmappedShares($user) as $unmappedShare) { + $this->updateShare($unmappedShare); + $share[] = $unmappedShare; + } + } + + return $share; + } + + #[\Override] + public function exportShareToLegacyBackend(Share $share): void { + $this->assertInTransaction(); + + $this->updateShareInLegacyBackend($share); + } + #[\Override] public function handle(Event $event): void { if ($event instanceof SharesDefaultSetEvent) { @@ -701,6 +757,91 @@ public function handle(Event $event): void { $this->dbConnection->rollBack(); throw $exception; } + + return; + } + + if ( + $event instanceof AfterShareModifiedEvent + || $event instanceof ShareAcceptedEvent + || $event instanceof ShareCreatedEvent + || $event instanceof ShareDeletedEvent + || $event instanceof ShareDeletedFromSelfEvent + || $event instanceof ShareMovedEvent + ) { + + $legacyShare = $event->getShare(); + $legacyBackend = $this->registry->getLegacyBackend(); + if (!$legacyBackend instanceof ISharingLegacyBackend) { + return; + } + + $shares = []; + try { + $this->dbConnection->beginTransaction(); + try { + $share = $legacyBackend->getShareByLegacyProviderAndId($legacyShare->getProviderId(), $legacyShare->getId()); + if (!isset($this->currentlyUpdatingLegacyShares[$share->id])) { + $this->updateShare($share); + $shares[] = $share; + } + } catch (ShareNotFoundException) { + // TODO: Support federation + $owner = $this->userManager->get($legacyShare->getShareOwner()); + if ($owner instanceof IUser) { + $shares = array_merge($shares, $this->importSharesFromLegacyBackend($owner)); + } + } + + $this->dbConnection->commit(); + } catch (Exception $exception) { + $this->dbConnection->rollBack(); + throw $exception; + } + + // TODO: Only enable in behat tests + if ($this->validateLegacySharesOnUpdate) { + foreach ($shares as $share) { + try { + $this->dbConnection->beginTransaction(); + + $legacyIds = $legacyBackend->getLegacyFullIds($share->id); + $legacySharesBefore = array_map(fn (string $legacyId): IShare => $this->legacyManager->getShareById($legacyId), $legacyIds); + + $this->exportShareToLegacyBackend($share); + + $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): string => $share->getId(), + 'full_id' => static fn (IShare $share): string => $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; + } + } + } } } @@ -876,31 +1017,62 @@ 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 214c93b4881ba..4d493b7f58fa0 100644 --- a/lib/unstable/Sharing/ISharingManager.php +++ b/lib/unstable/Sharing/ISharingManager.php @@ -20,6 +20,7 @@ use NCU\Sharing\Source\IShareSourceType; use NCU\Sharing\Source\ShareSource; use OCP\AppFramework\Attribute\Consumable; +use OCP\IUser; /** * @experimental 35.0.0 @@ -205,4 +206,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(Share $share): void; } diff --git a/psalm-strict.xml b/psalm-strict.xml index 0f217d5b9bada..e32f6921c52f9 100644 --- a/psalm-strict.xml +++ b/psalm-strict.xml @@ -55,6 +55,8 @@ + + From 1f93e68166e63ae67ec8e142f7cde7143e2a9234 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 31 Jul 2026 00:39:17 +0200 Subject: [PATCH 2/8] fix: ensure all legacy shares for a unified id have the same permissions Signed-off-by: Robin Appelman --- .../lib/Sharing/LegacyBackend.php | 35 ++++++++++++++++--- lib/private/Sharing/ISharingLegacyBackend.php | 2 ++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/apps/files_sharing/lib/Sharing/LegacyBackend.php b/apps/files_sharing/lib/Sharing/LegacyBackend.php index 06033b4f5870e..0116e1b084f81 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,14 @@ public function getShare(string $id): Share { // TODO $properties = []; - // TODO: Support per-recipient permissions + if (!$this->checkAllSame($legacyShares, fn (IShare $share): ?IAttributes => $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 +393,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 + */ + 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 +466,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..0d6193fe3339b 100644 --- a/lib/private/Sharing/ISharingLegacyBackend.php +++ b/lib/private/Sharing/ISharingLegacyBackend.php @@ -63,6 +63,8 @@ public function getShareByLegacyProviderAndId(string $legacyProvider, string $le public function getUnmappedShares(IUser $user): array; /** + * Get the legacy share ids belonging to a unified share id + * * @return list */ public function getLegacyFullIds(string $id): array; From bb3403757acf311f263defe99508b8accf96da48 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 31 Jul 2026 00:56:33 +0200 Subject: [PATCH 3/8] fix: handle outgoing federated share in legacy share backend Signed-off-by: Robin Appelman --- .../files_sharing/lib/AppInfo/Application.php | 14 +---- .../lib/Sharing/LegacyBackend.php | 55 +++++++++++++++---- .../tests/Sharing/LegacyBackendTest.php | 54 ++++++++++-------- 3 files changed, 77 insertions(+), 46 deletions(-) diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php index 3c81efea6fee8..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; @@ -146,14 +141,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 0116e1b084f81..f54afea4a9370 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,10 @@ 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 +183,10 @@ 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,29 @@ 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, ); @@ -574,4 +583,28 @@ 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(); + } + 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(), + ]; + } + 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..0cf5afa1522c9 100644 --- a/apps/files_sharing/tests/Sharing/LegacyBackendTest.php +++ b/apps/files_sharing/tests/Sharing/LegacyBackendTest.php @@ -12,6 +12,14 @@ use DateInterval; use DateTimeImmutable; use DateTimeInterface; +use NCU\Sharing\ISharingManager; +use NCU\Sharing\Permission\SharePermission; +use NCU\Sharing\Property\ShareProperty; +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; @@ -32,6 +40,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; @@ -40,14 +49,6 @@ use OCP\Server; use OCP\Share\IManager; use OCP\Share\IShare; -use OCP\Sharing\ISharingManager; -use OCP\Sharing\Permission\SharePermission; -use OCP\Sharing\Property\ShareProperty; -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 PHPUnit\Framework\Attributes\Group; use Test\TestCase; @@ -74,6 +75,7 @@ protected function setUp(): void { $this->legacyManager, Server::get(ISnowflakeGenerator::class), Server::get(ISharingManager::class), + Server::get(ICloudIdManager::class), ); } @@ -112,7 +114,8 @@ public function testUpdateShare(): void { // 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; + $circleId = null; + $circleName = null; if (class_exists(CirclesManager::class)) { $circlesManager = Server::get(CirclesManager::class); $circlesManager->startSession($circlesManager->getLocalFederatedUser($owner->getUID())); @@ -122,6 +125,16 @@ public function testUpdateShare(): void { Server::get(CircleService::class)->updateName($circle->getSingleId(), 'Circle'); /** @psalm-suppress MixedMethodCall, MixedAssignment */ $circle = $circlesManager->getCircle($circle->getSingleId()); + /** + * @psalm-suppress MixedMethodCall + * @var non-empty-string $circleId + */ + $circleId = $circle->getSingleId(); + /** + * @psalm-suppress MixedMethodCall + * @var non-empty-string $circleName + */ + $circleName = $circle->getDisplayName(); } $node1 = Server::get(IRootFolder::class)->getUserFolder($owner->getUID())->newFolder('foo'); @@ -132,14 +145,13 @@ public function testUpdateShare(): void { $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, + new DateTimeImmutable(), ShareState::Active, [ new ShareSource( @@ -196,10 +208,10 @@ public function testUpdateShare(): void { ), ), ], - $circle !== null ? [ + $circleId !== null ? [ new ShareRecipient( TeamShareRecipientType::class, - $circle->getSingleId(), + $circleId, null, 'team secret', new ShareUser( @@ -257,7 +269,6 @@ public function testUpdateShare(): void { $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( [ [ @@ -517,15 +528,15 @@ public function testUpdateShare(): void { 'reminder_sent' => false, ], ], - $circle !== null ? [ + ($circleId !== null && $circleName !== 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' => $circleId, + 'shared_with_display_name' => $circleName . ' (Team owned by ' . $owner->getDisplayName() . ')', 'shared_with_avatar' => 'http://localhost/apps/circles/img/circles.svg', 'permissions' => Constants::PERMISSION_ALL, 'attributes' => [ @@ -559,8 +570,8 @@ public function testUpdateShare(): void { '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' => $circleId, + 'shared_with_display_name' => $circleName . ' (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' => [ @@ -592,11 +603,10 @@ public function testUpdateShare(): void { ), $legacyShares); $this->legacyBackend->deleteShare($share->id); - if (class_exists(CirclesManager::class) && $circle !== null) { + if (class_exists(CirclesManager::class) && $circleId !== null) { $circlesManager = Server::get(CirclesManager::class); $circlesManager->startSession($circlesManager->getLocalFederatedUser($owner->getUID())); - /** @psalm-suppress MixedMethodCall */ - $circlesManager->destroyCircle($circle->getSingleId()); + $circlesManager->destroyCircle($circleId); } $this->assertTrue($group->delete()); From 71d13510d2585371aae8cb9a0aaf3eec89afc5c4 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 31 Jul 2026 18:08:26 +0200 Subject: [PATCH 4/8] fix: load share properties from legacy shares Signed-off-by: Robin Appelman --- .../lib/Sharing/LegacyBackend.php | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/apps/files_sharing/lib/Sharing/LegacyBackend.php b/apps/files_sharing/lib/Sharing/LegacyBackend.php index f54afea4a9370..c3ba852416ed3 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; @@ -47,6 +50,7 @@ use OCP\IUser; use OCP\L10N\IFactory; use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IAttributes; use OCP\Share\IManager; use OCP\Share\IShare; use OCP\Snowflake\ISnowflakeGenerator; @@ -360,8 +364,23 @@ 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): ?IAttributes => $share->getAttributes())) { throw new \Exception("All legacy shares sharing a share id don't have the same attributes"); @@ -392,7 +411,7 @@ public function getShare(string $id): Share { $id, $owner, // TODO - 0, + \DateTimeImmutable::createFromMutable($legacyShares[0]->getShareTime()), // TODO ShareState::Active, array_values($sources), @@ -406,8 +425,9 @@ public function getShare(string $id): Share { * Check that all items return the same result when used as argument to a function * * @template T + * @template U * @param iterable $items - * @param callable(T):mixed $fn + * @param callable(T):U $fn */ private function checkAllSame(iterable $items, callable $fn): bool { $first = true; @@ -588,6 +608,7 @@ private function recipientToLegacySharedWith(ShareRecipient $recipient): string if ($recipient->class === TokenShareRecipientType::class) { return $this->cloudIdManager->getCloudId($recipient->value, $recipient->instance)->getId(); } + return $recipient->value; } @@ -602,9 +623,37 @@ private function splitLegacySharedWith(int $shareType, string $sharedWith): arra 'remote' => $cloudId->getRemote(), ]; } + return [ 'value' => $sharedWith, 'remote' => null, ]; } + + /** + * @return array, ShareProperty> + */ + private function extractProperties(IShare $share): array { + /** @var array, ShareProperty> $properties */ + $properties = []; + + if ($expire = $share->getExpirationDate()) { + $properties[ExpirationDateSharePropertyType::class] = new ShareProperty(ExpirationDateSharePropertyType::class, $expire->format(DateTimeInterface::ATOM)); + } + + $password = $share->getPassword(); + if ($password !== null) { + $properties[PasswordSharePropertyType::class] = new ShareProperty(PasswordSharePropertyType::class, $password); + } + + if ($label = $share->getLabel()) { + $properties[LabelSharePropertyType::class] = new ShareProperty(LabelSharePropertyType::class, $label); + } + + if ($note = $share->getNote()) { + $properties[NoteSharePropertyType::class] = new ShareProperty(NoteSharePropertyType::class, $note); + } + + return $properties; + } } From 7d5edbf3b8a4f4c752aaa5526662ff9465ba6fe9 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 12 Aug 2026 22:18:40 +0200 Subject: [PATCH 5/8] test: don't fail test before cleanup when transaction was still open Signed-off-by: Robin Appelman --- .../lib/Sharing/AbstractSharingManagerTests.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/lib/Sharing/AbstractSharingManagerTests.php b/tests/lib/Sharing/AbstractSharingManagerTests.php index d1f79eb076427..1b75f5d7ff0ad 100644 --- a/tests/lib/Sharing/AbstractSharingManagerTests.php +++ b/tests/lib/Sharing/AbstractSharingManagerTests.php @@ -204,17 +204,22 @@ public function setUp(): void { #[\Override] protected function tearDown(): void { + $openTransaction = false; if ($this->dbConnection->inTransaction()) { $this->dbConnection->rollBack(); - $this->fail('Open transaction was not committed.'); + $openTransaction = true; } $accessContext = new ShareAccessContext(overrideChecks: true); $this->dbConnection->beginTransaction(); - foreach ($this->manager->getShares($accessContext, null, null, null, null) as $share) { - $this->manager->deleteShare($accessContext, $share); + try { + foreach ($this->manager->getShares($accessContext, null, null, null, null) as $share) { + $this->manager->deleteShare($accessContext, $share); + } + } catch (\Exception $e) { + $this->addWarning('Failed to delete share in cleanup: ' . $e->getMessage()); } $this->owner->delete(); @@ -223,6 +228,8 @@ protected function tearDown(): void { $this->dbConnection->commit(); + $this->registry->clear(); + foreach ([ 'sharing_share', 'sharing_share_permissions', @@ -237,7 +244,9 @@ protected function tearDown(): void { $this->assertEquals(0, $qb->executeQuery()->fetchOne(), $table); } - $this->registry->clear(); + if ($openTransaction) { + $this->fail('Open transaction was not committed.'); + } parent::tearDown(); } From 0017c43e6375ccdab47c88ac7f98dccbacee6cb8 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 13 Aug 2026 00:28:48 +0200 Subject: [PATCH 6/8] fix: fix legacy link share recipient logic Signed-off-by: Robin Appelman --- apps/files_sharing/lib/Sharing/LegacyBackend.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/files_sharing/lib/Sharing/LegacyBackend.php b/apps/files_sharing/lib/Sharing/LegacyBackend.php index c3ba852416ed3..e3880bb14169e 100644 --- a/apps/files_sharing/lib/Sharing/LegacyBackend.php +++ b/apps/files_sharing/lib/Sharing/LegacyBackend.php @@ -113,7 +113,10 @@ public function updateShare(Share $share): void { $legacyShare = $this->legacyManager->newShare(); $legacyShare->setShareType($legacyShareType); $legacyShare->setNodeId((int)$source->value); - $legacyShare->setSharedWith($this->recipientToLegacySharedWith($recipient)); + $legacyRecipient = $this->recipientToLegacySharedWith($recipient); + if ($legacyRecipient) { + $legacyShare->setSharedWith($legacyRecipient); + } $this->setCommonFields($share, $legacyShare); $create = true; @@ -604,9 +607,11 @@ private function recipientTypeClassToLegacyShareType(string $recipientTypeClass, }; } - private function recipientToLegacySharedWith(ShareRecipient $recipient): string { - if ($recipient->class === TokenShareRecipientType::class) { + private function recipientToLegacySharedWith(ShareRecipient $recipient): ?string { + if ($recipient->instance !== null) { return $this->cloudIdManager->getCloudId($recipient->value, $recipient->instance)->getId(); + } if ($recipient->class === TokenShareRecipientType::class) { + return null; } return $recipient->value; From 5605cf4f2339a1de6fe5323d814f3a543a812202 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 13 Aug 2026 18:04:38 +0200 Subject: [PATCH 7/8] fix: fix legacy share mapping table indexes Signed-off-by: Robin Appelman --- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../Version35000Date20260813121254.php | 43 +++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 apps/files_sharing/lib/Migration/Version35000Date20260813121254.php diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php index b9aad64edd185..b8a43d8d91afd 100644 --- a/apps/files_sharing/composer/composer/autoload_classmap.php +++ b/apps/files_sharing/composer/composer/autoload_classmap.php @@ -94,6 +94,7 @@ '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\\Migration\\Version35000Date20260813121254' => $baseDir . '/../lib/Migration/Version35000Date20260813121254.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', diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php index cf671c5fa5964..e84d9c6b7c3f8 100644 --- a/apps/files_sharing/composer/composer/autoload_static.php +++ b/apps/files_sharing/composer/composer/autoload_static.php @@ -109,6 +109,7 @@ class ComposerStaticInitFiles_Sharing '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\\Migration\\Version35000Date20260813121254' => __DIR__ . '/..' . '/../lib/Migration/Version35000Date20260813121254.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', diff --git a/apps/files_sharing/lib/Migration/Version35000Date20260813121254.php b/apps/files_sharing/lib/Migration/Version35000Date20260813121254.php new file mode 100644 index 0000000000000..a625f769a0c94 --- /dev/null +++ b/apps/files_sharing/lib/Migration/Version35000Date20260813121254.php @@ -0,0 +1,43 @@ +getTable('share_legacy_mapping'); + $shareLegacyMappingTable->dropPrimaryKey(); + $shareLegacyMappingTable->setPrimaryKey(['legacy_provider', 'legacy_id']); + $shareLegacyMappingTable->addIndex(['id']); + + return null; + } +} From eb31fc97268ffe907943186a9f1ee62c0a5975dd Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 13 Aug 2026 18:30:02 +0200 Subject: [PATCH 8/8] fix: store legacy id mapping when creating a legacy share from unified share Signed-off-by: Robin Appelman --- .../lib/Sharing/LegacyBackend.php | 35 +++++++++++++++---- .../tests/Sharing/LegacyBackendTest.php | 16 ++++----- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/apps/files_sharing/lib/Sharing/LegacyBackend.php b/apps/files_sharing/lib/Sharing/LegacyBackend.php index e3880bb14169e..fc3ae3ade7995 100644 --- a/apps/files_sharing/lib/Sharing/LegacyBackend.php +++ b/apps/files_sharing/lib/Sharing/LegacyBackend.php @@ -161,6 +161,9 @@ public function updateShare(Share $share): void { if ($create) { $legacyShare = $this->legacyManager->createShare($legacyShare); + + $this->addLegacyFullId($share->id, $legacyShare->getProviderId(), $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) { @@ -460,11 +463,11 @@ public function getShareByLegacyProviderAndId(string $legacyProvider, string $le #[\Override] public function getUnmappedShares(IUser $user): array { - // TODO: Make it work with all providers + // TODO: Make it work with all providers (deck, talk) // TODO: Filter by user $qb = $this->connection->getQueryBuilder(); $result = $qb - ->select('s.id') + ->select('s.id', 's.share_type') ->from('share', 's') ->leftJoin('s', 'share_legacy_mapping', 'l', $qb->expr()->eq('s.id', 'l.legacy_id')) ->where($qb->expr()->isNull('l.legacy_id')) @@ -479,16 +482,16 @@ public function getUnmappedShares(IUser $user): array { ], IQueryBuilder::PARAM_INT_ARRAY))) ->executeQuery(); - /** @var list $legacyIds */ - $legacyIds = $result->fetchFirstColumn(); - if ($legacyIds === []) { + /** @var list $rows */ + $rows = $result->fetchAll(); + if ($rows === []) { return []; } $ids = []; - foreach ($legacyIds as $legacyId) { + foreach ($rows as $row) { $id = $this->snowflakeGenerator->nextId(); - $this->addLegacyFullId($id, 'ocinternal', (string)$legacyId); + $this->addLegacyFullId($id, $this->legacyShareTypeToLegacyProvider($row['share_type']), (string)$row['id']); $ids[] = $id; } @@ -577,6 +580,22 @@ private function addLegacyFullId(string $id, string $legacyProvider, string $leg ->executeStatement(); } + /** + * @param IShare::TYPE_* $legacyShareType + * @return non-empty-string + */ + private function legacyShareTypeToLegacyProvider(int $legacyShareType): string { + return match ($legacyShareType) { + IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK => 'ocinternal', + IShare::TYPE_REMOTE, IShare::TYPE_REMOTE_GROUP => 'ocFederatedSharing', + IShare::TYPE_EMAIL => 'ocMailShare', + IShare::TYPE_CIRCLE => 'ocCircleShare', + IShare::TYPE_ROOM => 'ocRoomShare', + IShare::TYPE_DECK => 'deck', + default => throw new RuntimeException('Unsupported legacy share type: ' . $legacyShareType), + }; + } + /** * @param IShare::TYPE_* $legacyShareType * @return class-string @@ -588,6 +607,7 @@ private function legacyShareTypeToRecipientTypeClass(int $legacyShareType): stri IShare::TYPE_LINK => TokenShareRecipientType::class, IShare::TYPE_EMAIL => EmailShareRecipientType::class, IShare::TYPE_CIRCLE => TeamShareRecipientType::class, + // TODO talk, deck default => throw new RuntimeException('Unsupported legacy share type: ' . $legacyShareType), }; } @@ -603,6 +623,7 @@ private function recipientTypeClassToLegacyShareType(string $recipientTypeClass, TokenShareRecipientType::class => IShare::TYPE_LINK, EmailShareRecipientType::class => IShare::TYPE_EMAIL, TeamShareRecipientType::class => IShare::TYPE_CIRCLE, + // TODO talk, deck 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 index 0cf5afa1522c9..cc4ca7d3091f4 100644 --- a/apps/files_sharing/tests/Sharing/LegacyBackendTest.php +++ b/apps/files_sharing/tests/Sharing/LegacyBackendTest.php @@ -432,7 +432,7 @@ public function testUpdateShare(): void { 'password' => null, 'password_expiration_time' => null, 'send_password_by_talk' => false, - 'token' => 'secret', + 'token' => 'token secret', 'parent' => null, 'original_target' => null, 'target' => '/foo', @@ -461,7 +461,7 @@ public function testUpdateShare(): void { 'password' => null, 'password_expiration_time' => null, 'send_password_by_talk' => false, - 'token' => 'secret', + 'token' => 'token secret', 'parent' => null, 'original_target' => null, 'target' => '/foo.txt', @@ -490,7 +490,7 @@ public function testUpdateShare(): void { 'password' => null, 'password_expiration_time' => null, 'send_password_by_talk' => false, - 'token' => 'secret', + 'token' => 'email secret', 'parent' => null, 'original_target' => null, 'target' => null, @@ -519,7 +519,7 @@ public function testUpdateShare(): void { 'password' => null, 'password_expiration_time' => null, 'send_password_by_talk' => false, - 'token' => 'secret', + 'token' => 'email secret', 'parent' => null, 'original_target' => null, 'target' => null, @@ -658,12 +658,12 @@ private function formatLegacyShare(IShare $legacyShare): array { private function getLegacyIds(string $id): array { $qb = $this->dbConnection->getQueryBuilder(); $result = $qb - ->select('legacy_id') + ->select('legacy_provider', 'legacy_id') ->from('share_legacy_mapping') ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) ->executeQuery(); - /** @var list $ids */ - $ids = $result->fetchFirstColumn(); - return $ids; + /** @var list $rows */ + $rows = $result->fetchAll(); + return array_map(fn (array $row): string => $row['legacy_provider'] . ':' . $row['legacy_id'], $rows); } }