diff --git a/apps/files/tests/Sharing/Source/NodeShareSourceTypeTest.php b/apps/files/tests/Sharing/Source/NodeShareSourceTypeTest.php index 31d699361cfd0..ee1e3a5a35414 100644 --- a/apps/files/tests/Sharing/Source/NodeShareSourceTypeTest.php +++ b/apps/files/tests/Sharing/Source/NodeShareSourceTypeTest.php @@ -12,6 +12,7 @@ use NCU\Sharing\ShareAccessContext; use NCU\Sharing\Source\ShareSource; use OC\Files\Filesystem; +use OC\Sharing\SharingManager; use OC\User\Database; use OCA\Files\Sharing\Source\NodeShareSourceType; use OCP\EventDispatcher\IEventDispatcher; @@ -103,14 +104,14 @@ public function testDelete(): void { $this->manager->addShareSource($accessContext, $id, new ShareSource($this->sourceType::class, (string)$this->node->getId())); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->node->delete(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $share = $this->manager->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share->lastUpdated); - $this->assertLessThanOrEqual($after, $share->lastUpdated); + $this->assertGreaterThanOrEqual($before, SharingManager::timeToMs($share->lastUpdated)); + $this->assertLessThanOrEqual($after, SharingManager::timeToMs($share->lastUpdated)); $this->assertEquals([], $share->sources); $this->manager->deleteShare($accessContext, $id); diff --git a/apps/files_sharing/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..c9affd3fcf759 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,9 @@ function () use ($c) { $context->registerConfigLexicon(ConfigLexicon::class); $context->registerEventListener(RestrictInteractionEvent::class, RestrictInteractionListener::class); + + $registry = Server::get(ISharingRegistry::class); + $registry->registerLegacyBackend(Server::get(LegacyBackend::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..2bbd53dadd6b3 --- /dev/null +++ b/apps/files_sharing/lib/Sharing/LegacyBackend.php @@ -0,0 +1,605 @@ +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); + $legacyShare->setSharedWith($this->recipientToLegacySharedWith($recipient)); + + $this->setCommonFields($share, $legacyShare); + $create = true; + } + + $update = false; + + + 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; + } + + [$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->deleteShare($invalidLegacyShareId); + unset($legacyShares[$invalidLegacyShareId]); + } + + foreach ($legacyShares as $legacyShare) { + $update = $this->setCommonFields($share, $legacyShare); + + if ($update || ($updatedLegacyShares[$legacyShare->getFullId()] ?? false)) { + $this->legacyManager->updateShare($legacyShare, false); + } + } + } + + private function setCommonFields(Share $share, IShare $legacyShare): bool { + $update = false; + + 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; + } + + // 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(); + $recipient = $this->splitLegacySharedWith($legacyShare->getShareType(), $legacyShare->getSharedWith()); + /** @psalm-suppress ArgumentTypeCoercion */ + $recipients[$recipientTypeClass][$uniqueId] ??= new ShareRecipient( + $recipientTypeClass, + $recipient['value'], + $recipient['remote'], + $isTokenRecipient ? $legacyShare->getToken() : $this->sharingManager->generateSecret(), + new ShareUser( + $legacyShare->getSharedBy(), + // 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(), + // Incoming remote shares aren't handled by this + null, + ); + + // TODO + $properties = []; + + if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getAttributes())) { + throw new \Exception('All legacy shares sharing a share id don\'t have the same attributes'); + } + if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getPermissions())) { + throw new \Exception('All legacy shares sharing a share id don\'t have the same permissions'); + } + + $allowDownload = $legacyShares[0]->getShareType() === IShare::TYPE_LINK || $legacyShares[0]->getShareType() === IShare::TYPE_EMAIL + ? !$legacyShares[0]->getHideDownload() + : $legacyShares[0]->getAttributes()?->getAttribute('permissions', 'download') === true; + $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, + \DateTimeImmutable::createFromMutable($legacyShares[0]->getShareTime()), + // TODO + ShareState::Active, + array_values($sources), + array_merge(...array_values($recipients)), + $properties, + $permissions, + ); + } + + /** + * Check that all items return the same result when used as argument to a function + * + * @template T + * @param iterable $items + * @param callable(T):mixed $fn + * @return bool + */ + private function checkAllSame(iterable $items, callable $fn): bool { + $first = true; + $commonValue = null; + foreach ($items as $item) { + $value = $fn($item); + if ($first) { + $commonValue = $value; + $first = false; + } elseif ($value !== $commonValue) { + return false; + } + } + return true; + } + + #[\Override] + public function getShareByLegacyProviderAndId(string $legacyProvider, string $legacyId): Share { + $id = $this->getId($legacyProvider, $legacyId); + 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 + */ + 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); + } + + private function getId(string $legacyProvider, string $legacyId): ?string { + $qb = $this->connection->getQueryBuilder(); + $result = $qb + ->select('id') + ->from('share_legacy_mapping') + ->where($qb->expr()->eq('legacy_provider', $qb->createNamedParameter($legacyProvider))) + ->andWhere($qb->expr()->eq('legacy_id', $qb->createNamedParameter($legacyId))) + ->executeQuery(); + + /** @var int|false $id */ + $id = $result->fetchOne(); + if ($id === false) { + return null; + } + + return (string)$id; + } + + /** + * @return array + */ + 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), + }; + } + + private function recipientToLegacySharedWith(ShareRecipient $recipient): string { + if ($recipient->class === TokenShareRecipientType::class) { + return $this->cloudIdManager->getCloudId($recipient->value, $recipient->instance)->getId(); + } else { + return $recipient->value; + } + } + + /** + * @return array{value: string, remote: string|null} + */ + private function splitLegacySharedWith(int $shareType, string $sharedWith): array { + if ($shareType === IShare::TYPE_REMOTE || $shareType === IShare::TYPE_REMOTE_GROUP) { + $cloudId = $this->cloudIdManager->resolveCloudId($sharedWith); + return [ + 'value' => $cloudId->getUser(), + 'remote' => $cloudId->getRemote(), + ]; + } else { + return [ + 'value' => $sharedWith, + 'remote' => null, + ]; + } + } +} diff --git a/apps/files_sharing/tests/Sharing/LegacyBackendTest.php b/apps/files_sharing/tests/Sharing/LegacyBackendTest.php new file mode 100644 index 0000000000000..b5a29b953a00f --- /dev/null +++ b/apps/files_sharing/tests/Sharing/LegacyBackendTest.php @@ -0,0 +1,661 @@ +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), + Server::get(ICloudIdManager::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 db161bfd07bb0..bc547d1f267d7 100644 --- a/build/rector-strict.php +++ b/build/rector-strict.php @@ -47,6 +47,8 @@ $nextcloudDir . '/tests/Core/Sharing', $nextcloudDir . '/apps/files/lib/Sharing', $nextcloudDir . '/apps/files/tests/Sharing', + $nextcloudDir . '/apps/files_sharing/lib/Sharing', + $nextcloudDir . '/apps/files_sharing/tests/Sharing', ]) ->withAutoloadPaths([ // ensure rector properly autoload the public interfaces diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 79825014fdffc..f661fe8152658 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -896,6 +896,7 @@ 'OCP\\SetupCheck\\ISetupCheckManager' => $baseDir . '/lib/public/SetupCheck/ISetupCheckManager.php', 'OCP\\SetupCheck\\SetupResult' => $baseDir . '/lib/public/SetupCheck/SetupResult.php', 'OCP\\Share' => $baseDir . '/lib/public/Share.php', + 'OCP\\Share\\Events\\AfterShareModifiedEvent' => $baseDir . '/lib/public/Share/Events/AfterShareModifiedEvent.php', 'OCP\\Share\\Events\\BeforeShareCreatedEvent' => $baseDir . '/lib/public/Share/Events/BeforeShareCreatedEvent.php', 'OCP\\Share\\Events\\BeforeShareDeletedEvent' => $baseDir . '/lib/public/Share/Events/BeforeShareDeletedEvent.php', 'OCP\\Share\\Events\\ShareAcceptedEvent' => $baseDir . '/lib/public/Share/Events/ShareAcceptedEvent.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 260349ab1fdac..422bf9ccaf828 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -937,6 +937,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\SetupCheck\\ISetupCheckManager' => __DIR__ . '/../../..' . '/lib/public/SetupCheck/ISetupCheckManager.php', 'OCP\\SetupCheck\\SetupResult' => __DIR__ . '/../../..' . '/lib/public/SetupCheck/SetupResult.php', 'OCP\\Share' => __DIR__ . '/../../..' . '/lib/public/Share.php', + 'OCP\\Share\\Events\\AfterShareModifiedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/AfterShareModifiedEvent.php', 'OCP\\Share\\Events\\BeforeShareCreatedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/BeforeShareCreatedEvent.php', 'OCP\\Share\\Events\\BeforeShareDeletedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/BeforeShareDeletedEvent.php', 'OCP\\Share\\Events\\ShareAcceptedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareAcceptedEvent.php', diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index f1b8dbc4ce97e..0278c5605230e 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -60,6 +60,7 @@ use OCP\Security\PasswordContext; use OCP\Server; use OCP\Share; +use OCP\Share\Events\AfterShareModifiedEvent; use OCP\Share\Events\BeforeShareCreatedEvent; use OCP\Share\Events\BeforeShareDeletedEvent; use OCP\Share\Events\ShareAcceptedEvent; @@ -642,6 +643,7 @@ public function createShare(IShare $share): IShare { $provider = $this->factory->getProviderForType($share->getShareType()); if ($provider instanceof IShareProviderWithNotification) { $provider->sendMailNotification($share); + $this->dispatcher->dispatchTyped(new AfterShareModifiedEvent($share)); } else { $this->logger->debug('Share notification not sent because the provider does not support it.', ['app' => 'share']); } @@ -753,6 +755,7 @@ public function updateShare(IShare $share, bool $onlyValid = true): IShare { } else { $share = $provider->update($share); } + $this->dispatcher->dispatchTyped(new AfterShareModifiedEvent($share)); if ($expirationDateUpdated === true) { \OC_Hook::emit(Share::class, 'post_set_expiration_date', [ @@ -1067,7 +1070,11 @@ public function restoreShare(IShare $share, string $recipientId): IShare { [$providerId,] = $this->splitFullId($share->getFullId()); $provider = $this->factory->getProvider($providerId); - return $provider->restore($share, $recipientId); + $provider->restore($share, $recipientId); + + $this->dispatcher->dispatchTyped(new AfterShareModifiedEvent($share)); + + return $share; } #[Override] @@ -1449,6 +1456,8 @@ public function checkPassword(IShare $share, ?string $password): bool { $share->setPassword($newHash); $provider = $this->factory->getProviderForType($share->getShareType()); $provider->update($share); + + $this->dispatcher->dispatchTyped(new AfterShareModifiedEvent($share)); } return true; diff --git a/lib/private/Share20/Share.php b/lib/private/Share20/Share.php index d88710e4e191f..e018c3324d936 100644 --- a/lib/private/Share20/Share.php +++ b/lib/private/Share20/Share.php @@ -120,6 +120,15 @@ public function setProviderId(string $id): self { return $this; } + #[Override] + public function getProviderId(): string { + if ($this->providerId === null) { + throw new \UnexpectedValueException; + } + + return $this->providerId; + } + #[Override] public function setNode(Node $node): self { $this->fileId = null; diff --git a/lib/private/Sharing/ISharingLegacyBackend.php b/lib/private/Sharing/ISharingLegacyBackend.php index 5aa9f45271e75..9c7774031ff19 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,19 @@ 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; } diff --git a/lib/private/Sharing/SharingBackend.php b/lib/private/Sharing/SharingBackend.php index 5d6cb5d4017d2..d6c4b0f473240 100644 --- a/lib/private/Sharing/SharingBackend.php +++ b/lib/private/Sharing/SharingBackend.php @@ -58,7 +58,7 @@ public function __construct( } #[\Override] - public function createShare(string $id, ShareUser $owner, int $lastUpdated): void { + public function createShare(string $id, ShareUser $owner, \DateTimeImmutable $lastUpdated): void { $qb = $this->connection->getQueryBuilder(); $qb ->insert('sharing_share') @@ -66,7 +66,7 @@ public function createShare(string $id, ShareUser $owner, int $lastUpdated): voi 'id' => $qb->createNamedParameter($id), 'owner_user_id' => $qb->createNamedParameter($owner->userId), 'owner_instance' => $qb->createNamedParameter($owner->instance), - 'last_updated' => $qb->createNamedParameter($lastUpdated), + 'last_updated' => $qb->createNamedParameter(SharingManager::timeToMs($lastUpdated)), 'state' => $qb->createNamedParameter(ShareState::Draft->value), ]) ->executeStatement(); @@ -533,13 +533,13 @@ public function getShareOwner(string $id): ShareUser { * @param non-empty-list $ids */ #[\Override] - public function setLastUpdated(array $ids, int $lastUpdated): void { + public function setLastUpdated(array $ids, \DateTimeImmutable $lastUpdated): void { foreach (array_chunk($ids, 1000) as $chunk) { $qb = $this->connection->getQueryBuilder(); $rowCount = $qb ->update('sharing_share') - ->set('last_updated', $qb->createNamedParameter($lastUpdated, IQueryBuilder::PARAM_INT)) + ->set('last_updated', $qb->createNamedParameter(SharingManager::timeToMs($lastUpdated), IQueryBuilder::PARAM_INT)) ->where($qb->expr()->in('id', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))) ->executeStatement(); if ($rowCount !== count($chunk)) { @@ -954,7 +954,7 @@ private function list(ShareAccessContext $accessContext, ?string $filterShareID, $shares = array_map(static fn (array $share): Share => new Share( $share['id'], $share['owner'], - $share['last_updated'], + new \DateTimeImmutable('@' . $share['last_updated'] / 1000), // timestamp is stored in milliseconds $share['state'], $share['sources'], $share['recipients'], diff --git a/lib/private/Sharing/SharingManager.php b/lib/private/Sharing/SharingManager.php index 76593a8f4bc0b..d42772d26b209 100644 --- a/lib/private/Sharing/SharingManager.php +++ b/lib/private/Sharing/SharingManager.php @@ -11,6 +11,7 @@ use Exception; use NCU\Sharing\Exception\ShareInvalidException; +use NCU\Sharing\Exception\ShareNotFoundException; use NCU\Sharing\Exception\ShareOperationForbiddenException; use NCU\Sharing\ISharingBackend; use NCU\Sharing\ISharingManager; @@ -40,8 +41,16 @@ use OCP\IUserManager; use OCP\L10N\IFactory; use OCP\Security\ISecureRandom; +use OCP\Share\Events\AfterShareModifiedEvent; +use OCP\Share\Events\ShareAcceptedEvent; +use OCP\Share\Events\ShareCreatedEvent; +use OCP\Share\Events\ShareDeletedEvent; +use OCP\Share\Events\ShareDeletedFromSelfEvent; +use OCP\Share\Events\ShareMovedEvent; +use OCP\Share\IManager; use OCP\Snowflake\ISnowflakeGenerator; use OCP\User\Events\BeforeUserDeletedEvent; +use Psr\Clock\ClockInterface; use Random\Randomizer; use RuntimeException; @@ -53,22 +62,27 @@ /** * @psalm-import-type SharingShare from Share - * @template-implements IEventListener + * @template-implements IEventListener */ -final readonly class SharingManager implements ISharingManager, IEventListener { - private Randomizer $randomizer; +final class SharingManager implements ISharingManager, IEventListener { + private readonly Randomizer $randomizer; - private IL10N $l10n; + private readonly IL10N $l10n; - private ISharingBackend $backend; + private readonly ISharingBackend $backend; + + /** @var array */ + private array $currentlyUpdatingLegacyShares; public function __construct( IEventDispatcher $eventDispatcher, - private IUserManager $userManager, - private IFactory $l10nFactory, - private ISnowflakeGenerator $snowflakeGenerator, - private IDBConnection $dbConnection, - private ISharingRegistry $registry, + private readonly IUserManager $userManager, + private readonly IFactory $l10nFactory, + private readonly ISnowflakeGenerator $snowflakeGenerator, + private readonly IDBConnection $dbConnection, + private readonly ISharingRegistry $registry, + private readonly IManager $legacyManager, + private readonly ClockInterface $clock, IAppConfig $appConfig, ) { $this->randomizer = new Randomizer(); @@ -83,6 +97,14 @@ public function __construct( ); $eventDispatcher->addServiceListener(BeforeUserDeletedEvent::class, self::class); + $eventDispatcher->addServiceListener(AfterShareModifiedEvent::class, self::class); + $eventDispatcher->addServiceListener(ShareAcceptedEvent::class, self::class); + $eventDispatcher->addServiceListener(ShareCreatedEvent::class, self::class); + $eventDispatcher->addServiceListener(ShareDeletedEvent::class, self::class); + $eventDispatcher->addServiceListener(ShareDeletedFromSelfEvent::class, self::class); + $eventDispatcher->addServiceListener(ShareMovedEvent::class, self::class); + + $this->currentlyUpdatingLegacyShares = []; } #[\Override] @@ -140,13 +162,8 @@ public function generateSecret(): string { } #[\Override] - public function generateTimestamp(): int { - $time = (int)(microtime(true) * 1000.0); - if ($time < 0) { - throw new RuntimeException('Have you invented time travel?'); - } - - return $time; + public function getTime(): \DateTimeImmutable { + return $this->clock->now(); } #[\Override] @@ -158,7 +175,7 @@ public function createShare(ShareAccessContext $accessContext): string { $this->assertInTransaction(); $id = $this->snowflakeGenerator->nextId(); - $lastUpdated = $this->generateTimestamp(); + $lastUpdated = $this->getTime(); $this->backend->createShare($id, new ShareUser($currentUser->getUID(), null), $lastUpdated); $this->processShareUpdates([$id]); @@ -190,7 +207,7 @@ public function onOwnerDeleted(ShareAccessContext $accessContext, ShareUser $own public function updateShareState(ShareAccessContext $accessContext, string $id, ShareState $state): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); $this->validateShareOwnerOperation($accessContext, $owner); @@ -209,7 +226,7 @@ public function updateShareState(ShareAccessContext $accessContext, string $id, public function addShareSource(ShareAccessContext $accessContext, string $id, ShareSource $source): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); $this->validateShareOwnerOperation($accessContext, $owner); @@ -250,7 +267,7 @@ public function addShareSource(ShareAccessContext $accessContext, string $id, Sh public function removeShareSource(ShareAccessContext $accessContext, string $id, ShareSource $source): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); $this->validateShareOwnerOperation($accessContext, $owner); @@ -268,7 +285,7 @@ public function onSourceDeleted(ShareAccessContext $accessContext, ShareSource $ $this->assertInTransaction(); - $timestamp = $this->generateTimestamp(); + $timestamp = $this->getTime(); $updatedIds = $this->backend->onSourceDeleted($source); if ($updatedIds === []) { @@ -288,7 +305,7 @@ public function addShareRecipient(ShareAccessContext $accessContext, string $id, $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); @@ -351,7 +368,7 @@ public function removeShareRecipient(ShareAccessContext $accessContext, string $ $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); @@ -376,7 +393,7 @@ public function onRecipientDeleted(ShareAccessContext $accessContext, ShareRecip $this->assertInTransaction(); - $timestamp = $this->generateTimestamp(); + $timestamp = $this->getTime(); $updatedIds = $this->backend->onRecipientDeleted($recipient); if ($updatedIds === []) { @@ -396,7 +413,7 @@ public function onInitiatorDeleted(ShareAccessContext $accessContext, ShareUser $this->assertInTransaction(); - $timestamp = $this->generateTimestamp(); + $timestamp = $this->getTime(); $updatedIds = $this->backend->onInitiatorDeleted($initiator); if ($updatedIds === []) { @@ -412,7 +429,7 @@ public function onInitiatorDeleted(ShareAccessContext $accessContext, ShareUser public function updateShareRecipientSecret(ShareAccessContext $accessContext, string $id, ShareRecipient $recipient, string $secret): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); @@ -444,7 +461,7 @@ public function updateShareRecipientSecret(ShareAccessContext $accessContext, st public function createSharePropertyDefaultValue(Share $share, string $propertyTypeClass): Share { $this->assertInTransaction(); - $timestamp = $this->generateTimestamp(); + $timestamp = $this->getTime(); $this->backend->setLastUpdated([$share->id], $timestamp); if (($propertyType = $this->registry->getPropertyTypes()[$propertyTypeClass] ?? null) === null) { @@ -478,7 +495,7 @@ public function createSharePropertyDefaultValue(Share $share, string $propertyTy public function updateShareProperty(ShareAccessContext $accessContext, string $id, ShareProperty $property): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); $this->validateShareOwnerOperation($accessContext, $owner); @@ -503,8 +520,8 @@ public function updateShareProperty(ShareAccessContext $accessContext, string $i public function createSharePermissionDefaultValue(Share $share, string $permissionTypeClass): Share { $this->assertInTransaction(); - $timestamp = $this->generateTimestamp(); - $this->backend->setLastUpdated([$share->id], $timestamp); + $time = $this->getTime(); + $this->backend->setLastUpdated([$share->id], $time); if (($permissionType = $this->registry->getPermissionTypes()[$permissionTypeClass] ?? null) === null) { throw new RuntimeException('The permission is not registered: ' . $permissionTypeClass); @@ -520,7 +537,7 @@ public function createSharePermissionDefaultValue(Share $share, string $permissi $share = new Share( $share->id, $share->owner, - $timestamp, + $time, $share->state, $share->sources, $share->recipients, @@ -537,7 +554,7 @@ public function createSharePermissionDefaultValue(Share $share, string $permissi public function updateSharePermission(ShareAccessContext $accessContext, string $id, SharePermission $permission): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); $this->validateShareOwnerOperation($accessContext, $owner); @@ -574,7 +591,7 @@ public function updateSharePermission(ShareAccessContext $accessContext, string public function selectSharePermissionPreset(ShareAccessContext $accessContext, string $id, string $permissionPresetClass): void { $this->assertInTransaction(); - $this->backend->setLastUpdated([$id], $this->generateTimestamp()); + $this->backend->setLastUpdated([$id], $this->getTime()); $owner = $this->backend->getShareOwner($id); $this->validateShareOwnerOperation($accessContext, $owner); @@ -617,22 +634,124 @@ public function getShare(ShareAccessContext $accessContext, string $id): Share { public function getShares(ShareAccessContext $accessContext, ?string $filterSourceTypeClass, ?string $filterSourceTypeValue, ?string $lastShareID, ?int $limit): array { $this->assertInTransaction(); + if (($currentUser = $accessContext->currentUser) instanceof IUser) { + $this->importSharesFromLegacyBackend($currentUser); + } + return $this->backend->getShares($accessContext, $filterSourceTypeClass, $filterSourceTypeValue, $lastShareID, $limit); } + #[\Override] + public function importSharesFromLegacyBackend(IUser $user): array { + $this->assertInTransaction(); + + $shareIds = []; + + $legacyBackend = $this->registry->getLegacyBackend(); + if ($legacyBackend instanceof ISharingLegacyBackend) { + foreach ($legacyBackend->getUnmappedShares($user) as $unmappedShare) { + $this->updateShare($unmappedShare); + $shareIds[] = $unmappedShare->id; + } + } + + return $shareIds; + } + + #[\Override] + public function exportShareToLegacyBackend(string $id): void { + $this->assertInTransaction(); + + $share = $this->getShare(new ShareAccessContext(overrideChecks: true), $id); + + $this->updateShareInLegacyBackend($share); + } + #[\Override] public function handle(Event $event): void { - $shareUser = new ShareUser($event->getUser()->getUID(), null); + if ($event instanceof BeforeUserDeletedEvent) { + $shareUser = new ShareUser($event->getUser()->getUID(), null); + + try { + $this->dbConnection->beginTransaction(); + $this->onOwnerDeleted(new ShareAccessContext(overrideChecks: true), $shareUser); + $this->onInitiatorDeleted(new ShareAccessContext(overrideChecks: true), $shareUser); + $this->dbConnection->commit(); + } catch (Exception $exception) { + $this->dbConnection->rollBack(); + throw $exception; + } + + return; + } + + $legacyShare = $event->getShare(); + $legacyBackend = $this->registry->getLegacyBackend(); + if (!$legacyBackend instanceof ISharingLegacyBackend) { + return; + } + $ids = []; try { $this->dbConnection->beginTransaction(); - $this->onOwnerDeleted(new ShareAccessContext(overrideChecks: true), $shareUser); - $this->onInitiatorDeleted(new ShareAccessContext(overrideChecks: true), $shareUser); + try { + $share = $legacyBackend->getShareByLegacyProviderAndId($legacyShare->getProviderId(), $legacyShare->getId()); + if (!isset($this->currentlyUpdatingLegacyShares[$share->id])) { + $this->updateShare($share); + $ids[] = $share->id; + } + } catch (ShareNotFoundException) { + // TODO: Support federation + $owner = $this->userManager->get($legacyShare->getShareOwner()); + if ($owner instanceof IUser) { + $ids = array_merge($ids, $this->importSharesFromLegacyBackend($owner)); + } + } + $this->dbConnection->commit(); } catch (Exception $exception) { $this->dbConnection->rollBack(); throw $exception; } + + // TODO: Only run in behat tests + return; + foreach ($ids as $id) { + try { + $this->dbConnection->beginTransaction(); + + $legacyIds = $legacyBackend->getLegacyFullIds($id); + $legacySharesBefore = array_map(fn (string $legacyId): IShare => $this->legacyManager->getShareById($legacyId), $legacyIds); + + $this->exportShareToLegacyBackend($id); + + $legacySharesAfter = array_map(fn (string $legacyId): IShare => $this->legacyManager->getShareById($legacyId), $legacyIds); + + for ($i = 0, $iMax = count($legacyIds); $i < $iMax; ++$i) { + $legacyShareBefore = $legacySharesBefore[$i]; + $legacyShareAfter = $legacySharesAfter[$i]; + + $comparisons = [ + 'id' => static fn (IShare $share): mixed => $share->getId(), + 'full_id' => static fn (IShare $share): mixed => $share->getFullId(), + // TODO + ]; + + foreach ($comparisons as $field => $getter) { + $valueBefore = $getter($legacyShareBefore); + $valueAfter = $getter($legacyShareAfter); + if ($valueBefore !== $valueAfter) { + throw new RuntimeException('Values for ' . $field . ' do not match:\\nBefore: ' . var_export($valueBefore, true) . '\\nAfter: ' . var_export($valueAfter, true)); + } + } + } + + $this->dbConnection->rollBack(); + } catch (Exception $exception) { + $this->dbConnection->rollBack(); + throw $exception; + } + } } private function assertInTransaction(): void { @@ -802,28 +921,69 @@ 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); + } + } + + public static function timeToMs(\DateTimeImmutable $time): int { + if (method_exists($time, 'getMicrosecond')) { + $micros = $time->getMicrosecond(); + } else { + $micros = (int)$time->format('u'); + } + return $time->getTimestamp() * 1000 + (int)floor($micros / 1000); } } diff --git a/lib/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/ISharingBackend.php b/lib/unstable/Sharing/ISharingBackend.php index a4cb1401dd9cc..5fbc5a7149abd 100644 --- a/lib/unstable/Sharing/ISharingBackend.php +++ b/lib/unstable/Sharing/ISharingBackend.php @@ -29,7 +29,7 @@ interface ISharingBackend { * * @experimental 35.0.0 */ - public function createShare(string $id, ShareUser $owner, int $lastUpdated): void; + public function createShare(string $id, ShareUser $owner, \DateTimeImmutable $lastUpdated): void; /** * Perform all updates when the owner was deleted. @@ -199,9 +199,8 @@ public function getShareOwner(string $id): ShareUser; * Set the last updated timestamp for multiple shares. * * @param non-empty-list $ids - * @param non-negative-int $lastUpdated * @throws ShareNotFoundException * @experimental 35.0.0 */ - public function setLastUpdated(array $ids, int $lastUpdated): void; + public function setLastUpdated(array $ids, \DateTimeImmutable $lastUpdated): void; } diff --git a/lib/unstable/Sharing/ISharingManager.php b/lib/unstable/Sharing/ISharingManager.php index 21128680aaefc..46965673904f1 100644 --- a/lib/unstable/Sharing/ISharingManager.php +++ b/lib/unstable/Sharing/ISharingManager.php @@ -22,6 +22,7 @@ use NCU\Sharing\Source\IShareSourceType; use NCU\Sharing\Source\ShareSource; use OCP\AppFramework\Attribute\Consumable; +use OCP\IUser; /** * @experimental 35.0.0 @@ -50,12 +51,11 @@ public function searchRecipients(ShareAccessContext $accessContext, ?array $filt public function generateSecret(): string; /** - * Generate a new timestamp in milliseconds since the UNIX epoch. - * - * @return non-negative-int + * Get the current time + * * @experimental 35.0.0 */ - public function generateTimestamp(): int; + public function getTime(): \DateTimeImmutable; /** * Create a new share. @@ -221,4 +221,15 @@ public function getShare(ShareAccessContext $accessContext, string $id): Share; * @experimental 35.0.0 */ public function getShares(ShareAccessContext $accessContext, ?string $filterSourceTypeClass, ?string $filterSourceTypeValue, ?string $lastShareID, ?int $limit): array; + + /** + * @return list + * @since 35.0.0 + */ + public function importSharesFromLegacyBackend(IUser $user): array; + + /** + * @since 35.0.0 + */ + public function exportShareToLegacyBackend(string $id): void; } diff --git a/lib/unstable/Sharing/Share.php b/lib/unstable/Sharing/Share.php index ed74a4f2bcb8f..7f8f0cf01a7a9 100644 --- a/lib/unstable/Sharing/Share.php +++ b/lib/unstable/Sharing/Share.php @@ -154,8 +154,7 @@ public function __construct( /** @var non-empty-string $id */ public readonly string $id, public readonly ShareUser $owner, - /** @var non-negative-int $lastUpdated Unix time in milliseconds */ - public readonly int $lastUpdated, + public readonly \DateTimeImmutable $lastUpdated, public readonly ShareState $state, /** @var list $sources */ public readonly array $sources, @@ -237,10 +236,12 @@ public function format(ISharingRegistry $registry, IFactory $l10nFactory, IURLGe // First sort by priority and then sort by class name to get a stable order regardless of the DB order usort($permissions, static fn (array $a, array $b): int => 2 * ($b['priority'] <=> $a['priority']) + ($a['class'] <=> $b['class'])); + /** @var non-negative-int $timestamp */ + $timestamp = $this->lastUpdated->getTimestamp(); return [ 'id' => $this->id, 'owner' => $this->owner->format($userManager), - 'last_updated' => $this->lastUpdated, + 'last_updated' => $timestamp, 'state' => $this->state->value, 'sources' => ShareSource::formatMultiple($registry, $l10nFactory, $this->sources), 'recipients' => ShareRecipient::formatMultiple($registry, $l10nFactory, $urlGenerator, $userManager, $this->recipients), diff --git a/psalm-strict.xml b/psalm-strict.xml index c1b5dc95b0a22..fffa842517d16 100644 --- a/psalm-strict.xml +++ b/psalm-strict.xml @@ -53,6 +53,8 @@ + + diff --git a/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php b/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php index abfd54bc2a847..9866137487205 100644 --- a/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php +++ b/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php @@ -15,6 +15,7 @@ use NCU\Sharing\ShareAccessContext; use OC\Core\Sharing\Recipient\GroupShareRecipientType; use OC\Group\Database; +use OC\Sharing\SharingManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; use OCP\IGroup; @@ -140,14 +141,14 @@ public function testDelete(): void { $this->manager->addShareRecipient($accessContext, $id, new ShareRecipient($this->recipientType::class, $this->group1->getGID(), null)); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->group1->delete(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $share = $this->manager->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share->lastUpdated); - $this->assertLessThanOrEqual($after, $share->lastUpdated); + $this->assertGreaterThanOrEqual($before, SharingManager::timeToMs($share->lastUpdated)); + $this->assertLessThanOrEqual($after, SharingManager::timeToMs($share->lastUpdated)); $this->assertEquals([], $share->recipients); $this->manager->deleteShare($accessContext, $id); diff --git a/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php b/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php index 8b24d567c1655..31bbfd3458a55 100644 --- a/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php +++ b/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php @@ -171,14 +171,14 @@ public function testDelete(): void { $circlesManager = Server::get(CirclesManager::class); $circlesManager->startSession($circlesManager->getLocalFederatedUser($this->user1->getUID())); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $circlesManager->destroyCircle($this->team1->getId()); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $share = $this->manager->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share->lastUpdated); - $this->assertLessThanOrEqual($after, $share->lastUpdated); + $this->assertGreaterThanOrEqual($before, $share->lastUpdated->getTimestamp()); + $this->assertLessThanOrEqual($after, $share->lastUpdated->getTimestamp()); $this->assertEquals([], $share->recipients); $this->manager->deleteShare($accessContext, $id); diff --git a/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php b/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php index 453c0ecd12ac4..49935cd13d980 100644 --- a/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php +++ b/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php @@ -14,6 +14,7 @@ use NCU\Sharing\Recipient\ShareRecipient; use NCU\Sharing\ShareAccessContext; use OC\Core\Sharing\Recipient\UserShareRecipientType; +use OC\Sharing\SharingManager; use OC\User\Database; use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; @@ -144,14 +145,14 @@ public function testDelete(): void { $this->manager->addShareRecipient($accessContext, $id, new ShareRecipient($this->recipientType::class, $this->user2->getUID(), null)); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->user2->delete(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $share = $this->manager->getShare($accessContext, $id); - $this->assertGreaterThanOrEqual($before, $share->lastUpdated); - $this->assertLessThanOrEqual($after, $share->lastUpdated); + $this->assertGreaterThanOrEqual($before, SharingManager::timeToMs($share->lastUpdated)); + $this->assertLessThanOrEqual($after, SharingManager::timeToMs($share->lastUpdated)); $this->assertEquals([], $share->recipients); $this->manager->deleteShare($accessContext, $id); diff --git a/tests/lib/Sharing/AbstractSharingManagerTests.php b/tests/lib/Sharing/AbstractSharingManagerTests.php index 8f4522dc86ff2..5da9fe2777908 100644 --- a/tests/lib/Sharing/AbstractSharingManagerTests.php +++ b/tests/lib/Sharing/AbstractSharingManagerTests.php @@ -567,9 +567,9 @@ public function testSearchRecipientsOmitExisting(): void { public function testCreateShare(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->createShare($accessContext); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); unset($share['id']); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); @@ -690,9 +690,9 @@ public function testUpdateShareState(array $sources, array $recipients, array $p $this->assertEquals($errorMessage, $exception->getHint()); } } else { - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareState($accessContext, $id, ShareState::Active); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(ShareState::Active->value, $share['state']); @@ -706,9 +706,9 @@ public function testAddShareSource(): void { $id = $this->manager->createShare($accessContext); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals([ @@ -765,9 +765,9 @@ public function testRemoveShareSource(): void { $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->removeShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(ShareState::Active->value, $share['state']); @@ -782,9 +782,9 @@ public function testRemoveShareSource(): void { ], ], $share['sources']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->removeShareSource($accessContext, $id, new ShareSource(TestShareSourceType2::class, 'source2')); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(ShareState::Draft->value, $share['state']); @@ -798,9 +798,9 @@ public function testAddShareRecipient(): void { $id = $this->manager->createShare($accessContext); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->addShareRecipient($accessContext, $id, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals([ @@ -890,9 +890,9 @@ public function testAddChildShareRecipientWithResharePermission(): void { $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->addShareRecipient(new ShareAccessContext($this->user1), $id, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals([ @@ -955,9 +955,9 @@ public function testRemoveShareRecipient(): void { $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->removeShareRecipient($accessContext, $id, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(ShareState::Active->value, $share['state']); @@ -985,9 +985,9 @@ public function testRemoveShareRecipient(): void { ], ], $share['recipients']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->removeShareRecipient($accessContext, $id, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(ShareState::Draft->value, $share['state']); @@ -1074,9 +1074,9 @@ public function testRemoveChildShareRecipientWithResharePermission(): void { $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->removeShareRecipient(new ShareAccessContext($this->user1), $id, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals([ @@ -1232,9 +1232,9 @@ public function testUpdateShareRecipientSecret(bool $isSecretUpdatable): void { $this->assertEquals('You are not allowed to edit this share.', $exception->getHint()); } } else { - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareRecipientSecret($accessContext, $id, $recipient, 'mysecret'); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); unset($share['last_updated']); @@ -1292,9 +1292,9 @@ public function testUpdateShareProperty(array $values): void { $this->dbConnection->commit(); foreach ($values as $value) { - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyType1::class, $value)); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals([ @@ -1329,9 +1329,9 @@ public function testUpdateSharePropertyRequired(): void { $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeRequired::class, 'valid1')); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(ShareState::Draft->value, $share['state']); @@ -1364,9 +1364,9 @@ public function testUpdateSharePropertyRequired(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeRequired::class, 'valid2')); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(ShareState::Active->value, $share['state']); @@ -1395,9 +1395,9 @@ public function testUpdateSharePropertyRequired(): void { ], ], $share['properties']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeRequired::class, null)); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(ShareState::Draft->value, $share['state']); @@ -1497,9 +1497,9 @@ public function testUpdateSharePropertyModifyProperties(): void { $this->manager->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'old-value')); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-save-old-value')); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals([ @@ -1527,9 +1527,9 @@ public function testUpdateSharePropertyModifyProperties(): void { ], ], $share['properties']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-save')); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals([ @@ -1557,9 +1557,9 @@ public function testUpdateSharePropertyModifyProperties(): void { ], ], $share['properties']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-load')); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals([ @@ -1599,10 +1599,10 @@ public function testUpdateSharePermission(): void { $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->updateSharePermission($accessContext, $id, new SharePermission(ReshareSharePermissionType::class, true)); $share = $this->updateSharePermission($accessContext, $id, new SharePermission(TestSharePermissionType1::class, true)); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(ShareState::Draft->value, $share['state']); @@ -1631,9 +1631,9 @@ public function testUpdateSharePermission(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->dbConnection->commit(); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateSharePermission($accessContext, $id, new SharePermission(ReshareSharePermissionType::class, false)); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(ShareState::Active->value, $share['state']); @@ -1658,9 +1658,9 @@ public function testUpdateSharePermission(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateSharePermission($accessContext, $id, new SharePermission(TestSharePermissionType1::class, false)); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(ShareState::Draft->value, $share['state']); @@ -1727,12 +1727,12 @@ public function testSelectSharePermissionPreset(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->getShare($accessContext, $id); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare($accessContext, $id); $this->assertGreaterThanOrEqual($before, $share['last_updated']); @@ -1768,9 +1768,9 @@ public function testSelectSharePermissionPreset(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->selectSharePermissionPreset($accessContext, $id, TestSharePermissionPreset2::class); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(TestSharePermissionPreset2::class, $share['permission_preset']); @@ -1804,9 +1804,9 @@ public function testSelectSharePermissionPreset(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateSharePermission($accessContext, $id, new SharePermission(TestSharePermissionType3::class, true)); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertNull($share['permission_preset']); @@ -1840,9 +1840,9 @@ public function testSelectSharePermissionPreset(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->selectSharePermissionPreset($accessContext, $id, TestSharePermissionPreset1::class); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(TestSharePermissionPreset1::class, $share['permission_preset']); @@ -1876,9 +1876,9 @@ public function testSelectSharePermissionPreset(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->updateSharePermission($accessContext, $id, new SharePermission(TestSharePermissionType1::class, false)); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertNull($share['permission_preset']); @@ -1926,12 +1926,12 @@ public function testSelectSharePermissionPresetCompatible(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->getShare($accessContext, $id); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare($accessContext, $id); $this->assertGreaterThanOrEqual($before, $share['last_updated']); @@ -1949,9 +1949,9 @@ public function testSelectSharePermissionPresetCompatible(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->selectSharePermissionPreset($accessContext, $id, TestSharePermissionPreset2::class); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(TestSharePermissionPreset2::class, $share['permission_preset']); @@ -1967,9 +1967,9 @@ public function testSelectSharePermissionPresetCompatible(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertNull($share['permission_preset']); @@ -1994,9 +1994,9 @@ public function testSelectSharePermissionPresetCompatible(): void { ], ], $share['permissions']); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $share = $this->selectSharePermissionPreset($accessContext, $id, TestSharePermissionPreset2::class); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->assertGreaterThanOrEqual($before, $share['last_updated']); $this->assertLessThanOrEqual($after, $share['last_updated']); $this->assertEquals(TestSharePermissionPreset2::class, $share['permission_preset']); @@ -2043,7 +2043,7 @@ public function testDeleteShare(): void { public function testGetShare(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -2051,7 +2051,7 @@ public function testGetShare(): void { $this->manager->getShare($accessContext, $id); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare($accessContext, $id); $this->assertGreaterThanOrEqual($before, $share['last_updated']); @@ -2160,7 +2160,7 @@ public function testGetShareAsRecipientNotActive(): void { public function testGetShareAsRecipientActive(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -2171,7 +2171,7 @@ public function testGetShareAsRecipientActive(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare(new ShareAccessContext(currentUser: $this->user1), $id); $this->assertGreaterThanOrEqual($before, $share['last_updated']); @@ -2285,7 +2285,7 @@ public function testGetShareAsRecipientWithArguments(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -2295,7 +2295,7 @@ public function testGetShareAsRecipientWithArguments(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare(new ShareAccessContext(currentUser: $this->user1, arguments: [TestShareRecipientTypeArguments::class => 'secret']), $id); $this->assertGreaterThanOrEqual($before, $share['last_updated']); @@ -2409,7 +2409,7 @@ public function testGetShareWithSecretActive(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -2418,7 +2418,7 @@ public function testGetShareWithSecretActive(): void { $this->manager->updateSharePermission($accessContext, $id, new SharePermission(TestSharePermissionType1::class, true)); $this->manager->updateShareState($accessContext, $id, ShareState::Active); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->manager->getShare($accessContext, $id); $this->dbConnection->commit(); @@ -2535,7 +2535,7 @@ public function testGetShareAsRecipientFilteredProperties(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -2546,7 +2546,7 @@ public function testGetShareAsRecipientFilteredProperties(): void { $this->manager->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeFilter::class, 'visible')); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare(new ShareAccessContext(currentUser: $this->user1), $id); $this->assertGreaterThanOrEqual($before, $share['last_updated']); @@ -2644,11 +2644,11 @@ public function testGetShareAsRecipientFilteredProperties(): void { 'permission_preset' => TestSharePermissionPreset1::class, ], $share); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $this->manager->updateShareProperty($accessContext, $id, new ShareProperty(TestSharePropertyTypeFilter::class, 'filtered')); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare(new ShareAccessContext(currentUser: $this->owner), $id); $this->assertGreaterThanOrEqual($before, $share['last_updated']); @@ -2761,7 +2761,7 @@ public function testGetShareAsRecipientFilteredArguments(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -2771,7 +2771,7 @@ public function testGetShareAsRecipientFilteredArguments(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare(new ShareAccessContext(currentUser: $this->user1), $id); $this->assertGreaterThanOrEqual($before, $share['last_updated']); @@ -3004,14 +3004,14 @@ public function testGetShareWithPublicSecret(bool $isSecretPublic): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareRecipient($accessContext, $id, new ShareRecipient(TestShareRecipientType1::class, 'recipient1', null)); $this->manager->addShareRecipient($accessContext, $id, new ShareRecipient(TestShareRecipientTypePublicSecret::class, 'recipient2', null)); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare($accessContext, $id); $this->assertGreaterThanOrEqual($before, $share['last_updated']); @@ -3081,7 +3081,7 @@ public function testGetShareWithSecret(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -3094,7 +3094,7 @@ public function testGetShareWithSecret(): void { $this->manager->addShareRecipient(new ShareAccessContext($this->user2), $id, new ShareRecipient(TestShareRecipientTypePublicSecret::class, 'recipient4', null)); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $share = $this->getShare(new ShareAccessContext($this->user2), $id); $this->assertGreaterThanOrEqual($before, $share['last_updated']); @@ -3255,7 +3255,7 @@ public function testGetShareUniqueDisplayNames(): void { public function testGetShareDisabledOwner(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -3266,7 +3266,7 @@ public function testGetShareDisabledOwner(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->owner->setEnabled(false); @@ -3294,7 +3294,7 @@ public function testGetShareDisabledOwner(): void { public function testGetShareDisabledInitiator(): void { $accessContext = new ShareAccessContext($this->owner); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -3304,7 +3304,7 @@ public function testGetShareDisabledInitiator(): void { $this->manager->addShareRecipient(new ShareAccessContext(currentUser: $this->user1), $id, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); $this->dbConnection->commit(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->user1->setEnabled(false); @@ -3367,7 +3367,7 @@ public function testGetShareDisabledInitiator(): void { public function testGetShares(): void { $accessContext = new ShareAccessContext($this->owner); - $before1 = $this->manager->generateTimestamp(); + $before1 = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id1 = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id1, new ShareSource(TestShareSourceType1::class, 'source1')); @@ -3375,9 +3375,9 @@ public function testGetShares(): void { $this->manager->getShare($accessContext, $id1); $this->dbConnection->commit(); - $after1 = $this->manager->generateTimestamp(); + $after1 = $this->manager->getTime(); - $before2 = $this->manager->generateTimestamp(); + $before2 = $this->manager->getTime(); $this->dbConnection->beginTransaction(); $id2 = $this->manager->createShare($accessContext); $this->manager->addShareSource($accessContext, $id2, new ShareSource(TestShareSourceType2::class, 'source2')); @@ -3385,7 +3385,7 @@ public function testGetShares(): void { $this->manager->getShare($accessContext, $id2); $this->dbConnection->commit(); - $after2 = $this->manager->generateTimestamp(); + $after2 = $this->manager->getTime(); $shares = $this->getShares($accessContext, null, null, null, null); $this->assertCount(2, $shares); @@ -3978,9 +3978,9 @@ public function testInitiatorDeleted(): void { $this->manager->updateShareState($accessContext, $id, ShareState::Active); $this->manager->addShareRecipient(new ShareAccessContext(currentUser: $this->user1), $id, new ShareRecipient(TestShareRecipientType2::class, 'recipient2', null)); - $before = $this->manager->generateTimestamp(); + $before = $this->manager->getTime(); $this->user1->delete(); - $after = $this->manager->generateTimestamp(); + $after = $this->manager->getTime(); $this->dbConnection->commit(); $share = $this->getShare(new ShareAccessContext(overrideChecks: true), $id); diff --git a/unified-sharing-check.sh b/unified-sharing-check.sh new file mode 100755 index 0000000000000..c1d5f50e70447 --- /dev/null +++ b/unified-sharing-check.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euxo pipefail + +composer psalm +composer psalm:ocp +composer psalm:ncu +composer psalm:strict +composer rector:strict +composer openapi +./build/autoloaderchecker.sh diff --git a/unified-sharing-test.sh b/unified-sharing-test.sh new file mode 100755 index 0000000000000..2d965aa7088e4 --- /dev/null +++ b/unified-sharing-test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +for target in tests/lib/Sharing tests/Core/Sharing apps/sharing apps/files/tests/Sharing apps/files_sharing/tests/Listener/RestrictInteractionListenerTest.php apps/files_sharing/tests/Sharing; do + if [ -d "$target" ] || [ -f "$target" ]; then + ./autotest.sh sqlite "$target" + fi +done