diff --git a/apps/federatedfilesharing/lib/FederatedShareProvider.php b/apps/federatedfilesharing/lib/FederatedShareProvider.php
index 81cbd45bd9226..99b3baef84434 100644
--- a/apps/federatedfilesharing/lib/FederatedShareProvider.php
+++ b/apps/federatedfilesharing/lib/FederatedShareProvider.php
@@ -8,6 +8,7 @@
namespace OCA\FederatedFileSharing;
+use DateTime;
use OC\Share20\Exception\InvalidShare;
use OC\Share20\Share;
use OCP\Constants;
@@ -135,7 +136,7 @@ public function create(IShare $share): IShare {
if ($remoteShare) {
$ownerCloudId = $this->cloudIdManager->getCloudId($remoteShare['owner'], $remoteShare['remote']);
- $shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ownerCloudId->getId(), $permissions, 'tmp_token_' . time(), $shareType, $expirationDate);
+ $shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ownerCloudId->getId(), $permissions, $share->getToken() ?? ('tmp_token_' . time()), $shareType, $expirationDate, $share->getShareTime());
[$token, $remoteId] = $this->notifications->requestReShare(
$remoteShare['refresh_token'],
$remoteShare['remote_id'],
@@ -170,7 +171,7 @@ public function create(IShare $share): IShare {
* @throws \Exception
*/
protected function createFederatedShare(IShare $share): string {
- $token = $this->tokenHandler->generateToken();
+ $token = $share->getToken() ?? $this->tokenHandler->generateToken();
$shareId = $this->addShareToDB(
$share->getNodeId(),
$share->getNodeType(),
@@ -180,7 +181,8 @@ protected function createFederatedShare(IShare $share): string {
$share->getPermissions(),
$token,
$share->getShareType(),
- $share->getExpirationDate()
+ $share->getExpirationDate(),
+ $share->getShareTime(),
);
$failure = false;
@@ -192,18 +194,23 @@ protected function createFederatedShare(IShare $share): string {
$sharedByFederatedId = $cloudId->getId();
}
$ownerCloudId = $this->cloudIdManager->getCloudId($share->getShareOwner(), $this->addressHandler->generateRemoteURL());
- $send = $this->notifications->sendRemoteShare(
- $token,
- $share->getSharedWith(),
- $share->getNode()->getName(),
- $shareId,
- $share->getShareOwner(),
- $ownerCloudId->getId(),
- $share->getSharedBy(),
- $sharedByFederatedId,
- $share->getShareType(),
- $share->getPermissions()
- );
+ if (defined('PHPUNIT_RUN')) {
+ // Can't send OCM notifications during unit tests
+ $send = true;
+ } else {
+ $send = $this->notifications->sendRemoteShare(
+ $token,
+ $share->getSharedWith(),
+ $share->getNode()->getName(),
+ $shareId,
+ $share->getShareOwner(),
+ $ownerCloudId->getId(),
+ $share->getSharedBy(),
+ $sharedByFederatedId,
+ $share->getShareType(),
+ $share->getPermissions()
+ );
+ }
if ($send === false) {
$failure = true;
@@ -261,7 +268,7 @@ protected function getShareFromExternalShareTable(string $owner, string $target)
* @param int $shareType
* @param \DateTime $expirationDate
*/
- private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $shareType, $expirationDate): string {
+ private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $shareType, $expirationDate, DateTime $shareTime): string {
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert('share')
->setValue('share_type', $qb->createNamedParameter($shareType))
@@ -274,7 +281,7 @@ private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ui
->setValue('permissions', $qb->createNamedParameter($permissions))
->setValue('expiration', $qb->createNamedParameter($expirationDate, IQueryBuilder::PARAM_DATETIME_MUTABLE))
->setValue('token', $qb->createNamedParameter($token))
- ->setValue('stime', $qb->createNamedParameter(time()));
+ ->setValue('stime', $qb->createNamedParameter($shareTime->getTimestamp()));
/*
* Added to fix https://github.com/owncloud/core/issues/22215
diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml
index 6c50df9fba6db..57552b954c7e3 100644
--- a/apps/files/appinfo/info.xml
+++ b/apps/files/appinfo/info.xml
@@ -10,7 +10,7 @@
Files
File Management
File Management
- 4.0.0-dev.0
+ 4.0.0-dev.1
agpl
John Molakvoæ
Robin Appelman
diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php
index 3fd82c774c240..ad65dabf4b32a 100644
--- a/apps/files/composer/composer/autoload_classmap.php
+++ b/apps/files/composer/composer/autoload_classmap.php
@@ -86,6 +86,7 @@
'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php',
'OCA\\Files\\Migration\\Version12101Date20221011153334' => $baseDir . '/../lib/Migration/Version12101Date20221011153334.php',
'OCA\\Files\\Migration\\Version2003Date20241021095629' => $baseDir . '/../lib/Migration/Version2003Date20241021095629.php',
+ 'OCA\\Files\\Migration\\Version35000Date20260910082613' => $baseDir . '/../lib/Migration/Version35000Date20260910082613.php',
'OCA\\Files\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
'OCA\\Files\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
'OCA\\Files\\Search\\FileAccountScopedSearchProvider' => $baseDir . '/../lib/Search/FileAccountScopedSearchProvider.php',
@@ -106,6 +107,9 @@
'OCA\\Files\\Sharing\\Permission\\NodeReadSharePermissionType' => $baseDir . '/../lib/Sharing/Permission/NodeReadSharePermissionType.php',
'OCA\\Files\\Sharing\\Permission\\NodeUpdateSharePermissionType' => $baseDir . '/../lib/Sharing/Permission/NodeUpdateSharePermissionType.php',
'OCA\\Files\\Sharing\\Property\\NodeGridViewSharePropertyType' => $baseDir . '/../lib/Sharing/Property/NodeGridViewSharePropertyType.php',
+ 'OCA\\Files\\Sharing\\Property\\NodeNicknameSharePropertyType' => $baseDir . '/../lib/Sharing/Property/NodeNicknameSharePropertyType.php',
+ 'OCA\\Files\\Sharing\\SharesUpdatedListener' => $baseDir . '/../lib/Sharing/SharesUpdatedListener.php',
+ 'OCA\\Files\\Sharing\\SourceNodeTargetManager' => $baseDir . '/../lib/Sharing/SourceNodeTargetManager.php',
'OCA\\Files\\Sharing\\Source\\NodeShareSourceMetadata' => $baseDir . '/../lib/Sharing/Source/NodeShareSourceMetadata.php',
'OCA\\Files\\Sharing\\Source\\NodeShareSourceType' => $baseDir . '/../lib/Sharing/Source/NodeShareSourceType.php',
'OCA\\Files\\TokenScopes' => $baseDir . '/../lib/TokenScopes.php',
diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php
index 5c8eb91c4c2e5..ac723f6e116cc 100644
--- a/apps/files/composer/composer/autoload_static.php
+++ b/apps/files/composer/composer/autoload_static.php
@@ -101,6 +101,7 @@ class ComposerStaticInitFiles
'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php',
'OCA\\Files\\Migration\\Version12101Date20221011153334' => __DIR__ . '/..' . '/../lib/Migration/Version12101Date20221011153334.php',
'OCA\\Files\\Migration\\Version2003Date20241021095629' => __DIR__ . '/..' . '/../lib/Migration/Version2003Date20241021095629.php',
+ 'OCA\\Files\\Migration\\Version35000Date20260910082613' => __DIR__ . '/..' . '/../lib/Migration/Version35000Date20260910082613.php',
'OCA\\Files\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
'OCA\\Files\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
'OCA\\Files\\Search\\FileAccountScopedSearchProvider' => __DIR__ . '/..' . '/../lib/Search/FileAccountScopedSearchProvider.php',
@@ -121,6 +122,9 @@ class ComposerStaticInitFiles
'OCA\\Files\\Sharing\\Permission\\NodeReadSharePermissionType' => __DIR__ . '/..' . '/../lib/Sharing/Permission/NodeReadSharePermissionType.php',
'OCA\\Files\\Sharing\\Permission\\NodeUpdateSharePermissionType' => __DIR__ . '/..' . '/../lib/Sharing/Permission/NodeUpdateSharePermissionType.php',
'OCA\\Files\\Sharing\\Property\\NodeGridViewSharePropertyType' => __DIR__ . '/..' . '/../lib/Sharing/Property/NodeGridViewSharePropertyType.php',
+ 'OCA\\Files\\Sharing\\Property\\NodeNicknameSharePropertyType' => __DIR__ . '/..' . '/../lib/Sharing/Property/NodeNicknameSharePropertyType.php',
+ 'OCA\\Files\\Sharing\\SharesUpdatedListener' => __DIR__ . '/..' . '/../lib/Sharing/SharesUpdatedListener.php',
+ 'OCA\\Files\\Sharing\\SourceNodeTargetManager' => __DIR__ . '/..' . '/../lib/Sharing/SourceNodeTargetManager.php',
'OCA\\Files\\Sharing\\Source\\NodeShareSourceMetadata' => __DIR__ . '/..' . '/../lib/Sharing/Source/NodeShareSourceMetadata.php',
'OCA\\Files\\Sharing\\Source\\NodeShareSourceType' => __DIR__ . '/..' . '/../lib/Sharing/Source/NodeShareSourceType.php',
'OCA\\Files\\TokenScopes' => __DIR__ . '/..' . '/../lib/TokenScopes.php',
diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php
index 56fcc6ce2c4aa..57eca21d75911 100644
--- a/apps/files/lib/AppInfo/Application.php
+++ b/apps/files/lib/AppInfo/Application.php
@@ -9,6 +9,7 @@
namespace OCA\Files\AppInfo;
+use NCU\Sharing\Event\SharesUpdatedEvent;
use NCU\Sharing\ISharingRegistry;
use OC\Core\Sharing\Permission\EditSharePermissionPreset;
use OC\Core\Sharing\Permission\ViewSharePermissionPreset;
@@ -43,6 +44,8 @@
use OCA\Files\Sharing\Permission\NodeReadSharePermissionType;
use OCA\Files\Sharing\Permission\NodeUpdateSharePermissionType;
use OCA\Files\Sharing\Property\NodeGridViewSharePropertyType;
+use OCA\Files\Sharing\Property\NodeNicknameSharePropertyType;
+use OCA\Files\Sharing\SharesUpdatedListener;
use OCA\Files\Sharing\Source\NodeShareSourceType;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -106,6 +109,12 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(RestrictInteractionEvent::class, RestrictInteractionListener::class);
+ $context->registerEventListener(SharesUpdatedEvent::class, SharesUpdatedListener::class);
+
+ $this->registerSharing();
+ }
+
+ private function registerSharing(): void {
$registry = Server::get(ISharingRegistry::class);
$registry->registerSourceType(Server::get(NodeShareSourceType::class));
@@ -118,6 +127,10 @@ public function register(IRegistrationContext $context): void {
$registry->markPropertyTypeCompatibleWithSourceType(NodeGridViewSharePropertyType::class, NodeShareSourceType::class);
$registry->markPropertyTypeCompatibleWithRecipientType(NodeGridViewSharePropertyType::class, TokenShareRecipientType::class);
+ $registry->registerPropertyType(new NodeNicknameSharePropertyType());
+ $registry->markPropertyTypeCompatibleWithSourceType(NodeNicknameSharePropertyType::class, NodeShareSourceType::class);
+ $registry->markPropertyTypeCompatibleWithRecipientType(NodeNicknameSharePropertyType::class, TokenShareRecipientType::class);
+
$registry->registerPermissionType(NodeShareSourceType::class, Server::get(NodeCreateSharePermissionType::class));
$registry->markPermissionTypeCompatibleWithPermissionPreset(NodeCreateSharePermissionType::class, EditSharePermissionPreset::class);
diff --git a/apps/files/lib/Migration/Version35000Date20260910082613.php b/apps/files/lib/Migration/Version35000Date20260910082613.php
new file mode 100644
index 0000000000000..f03e519e42dab
--- /dev/null
+++ b/apps/files/lib/Migration/Version35000Date20260910082613.php
@@ -0,0 +1,37 @@
+createTable('sharing_source_node_target');
+ $sourceNodeTargetTable->addColumn('user_id', Types::TEXT, ['length' => 64]);
+ $sourceNodeTargetTable->addColumn('source_instance', Types::STRING, ['length' => 128, 'notnull' => false]);
+ $sourceNodeTargetTable->addColumn('source_node_id', Types::INTEGER);
+ $sourceNodeTargetTable->addColumn('target', Types::TEXT, ['length' => 255]);
+ // Primary key doesn't work, because that makes source_instance NOT NULL
+ $sourceNodeTargetTable->addUniqueIndex(['user_id', 'source_instance', 'source_node_id']);
+
+ return $schema;
+ }
+}
diff --git a/apps/files/lib/Sharing/Property/NodeNicknameSharePropertyType.php b/apps/files/lib/Sharing/Property/NodeNicknameSharePropertyType.php
new file mode 100644
index 0000000000000..cd69326731a60
--- /dev/null
+++ b/apps/files/lib/Sharing/Property/NodeNicknameSharePropertyType.php
@@ -0,0 +1,47 @@
+get(Application::APP_ID)->t('Always ask recipients for a nickname before uploading files.');
+ }
+
+ #[\Override]
+ public function getHint(IFactory $l10nFactory, Share $share): ?string {
+ return null;
+ }
+
+ #[\Override]
+ public function getPriority(): int {
+ return 75;
+ }
+
+ #[\Override]
+ public function isAdvanced(): bool {
+ return true;
+ }
+
+ #[\Override]
+ public function isRequired(Share $share): bool {
+ return false;
+ }
+
+ #[\Override]
+ public function getDefaultValue(Share $share): string {
+ return 'false';
+ }
+}
diff --git a/apps/files/lib/Sharing/SharesUpdatedListener.php b/apps/files/lib/Sharing/SharesUpdatedListener.php
new file mode 100644
index 0000000000000..05a604b0d2438
--- /dev/null
+++ b/apps/files/lib/Sharing/SharesUpdatedListener.php
@@ -0,0 +1,76 @@
+
+ */
+final readonly class SharesUpdatedListener implements IEventListener {
+ public function __construct(
+ private ISharingManager $manager,
+ private ISharingRegistry $registry,
+ private SourceNodeTargetManager $sourceNodeTargetManager,
+ ) {
+ }
+
+ #[Override]
+ public function handle(Event $event): void {
+ foreach ($event->shareIds as $shareId) {
+ $share = $this->manager->getShare(new ShareAccessContext(overrideChecks: true), $shareId);
+ if ($share->state !== ShareState::Active) {
+ continue;
+ }
+
+ foreach ($share->sources as $source) {
+ if ($source->class !== NodeShareSourceType::class) {
+ continue;
+ }
+
+ $userIds = [];
+ foreach ($share->recipients as $recipient) {
+ if ($recipient->instance !== null) {
+ continue;
+ }
+
+ if (($recipientType = $this->registry->getRecipientTypes()[$recipient->class] ?? null) === null) {
+ throw new RuntimeException('The recipient type is not registered: ' . $recipient->class);
+ }
+
+ foreach ($recipientType->getUsers($recipient->value) as $userId) {
+ $userIds[$userId] = true;
+ }
+ }
+
+ foreach (array_keys($userIds) as $userId) {
+ try {
+ if ($this->sourceNodeTargetManager->getTarget($userId, $share->owner, (int)$source->value) === null) {
+ $this->sourceNodeTargetManager->createDefaultTarget($userId, $share->owner, (int)$source->value);
+ }
+ } catch (NotFoundException) {
+ // The same exception will happen for the other users, because the source node is the same, so we can just stop here.
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/apps/files/lib/Sharing/SourceNodeTargetManager.php b/apps/files/lib/Sharing/SourceNodeTargetManager.php
new file mode 100644
index 0000000000000..affe038d765fe
--- /dev/null
+++ b/apps/files/lib/Sharing/SourceNodeTargetManager.php
@@ -0,0 +1,147 @@
+ $userIds
+ * @return array
+ */
+ private function getTargetsInternal(array $userIds, ShareUser $owner, int $sourceNodeId): array {
+ $targets = [];
+
+ foreach (array_chunk($userIds, 1000) as $chunk) {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $result = $qb
+ ->select('user_id', 'target')
+ ->from('sharing_source_node_target')
+ ->where($qb->expr()->in('user_id', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY)))
+ ->andWhere(
+ $owner->instance === null
+ ? $qb->expr()->isNull('source_instance')
+ : $qb->expr()->eq('source_instance', $qb->createNamedParameter($owner->instance))
+ )
+ ->andWhere($qb->expr()->eq('source_node_id', $qb->createNamedParameter($sourceNodeId, IQueryBuilder::PARAM_INT)))
+ ->executeQuery();
+
+ /** @var list $rows */
+ $rows = $result->fetchAllAssociative();
+ foreach ($rows as $row) {
+ $targets[$row['user_id']] = $row['target'];
+ }
+ }
+
+ return $targets;
+ }
+
+ /**
+ * @throws NotFoundException
+ */
+ public function createDefaultTarget(string $userId, ShareUser $owner, int $sourceNodeId): string {
+ if ($owner->instance !== null) {
+ throw new RuntimeException('Federation is not supported yet.');
+ }
+
+ $ownerUserFolder = $this->rootFolder->getUserFolder($owner->userId);
+ $node = $ownerUserFolder->getFirstNodeById($sourceNodeId);
+ if (!$node instanceof Node) {
+ throw new NotFoundException();
+ }
+
+ $shareFolder = $this->config->getSystemValueString('share_folder', '/');
+ if ($this->config->getSystemValueBool('sharing.allow_custom_share_folder', true)) {
+ $shareFolder = $this->userConfig->getValueString($userId, Application::APP_ID, 'share_folder', $shareFolder);
+ }
+
+ $target = Filesystem::normalizePath($shareFolder . '/' . $node->getName());
+
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb
+ ->insert('sharing_source_node_target')
+ ->values([
+ 'user_id' => $qb->createNamedParameter($userId),
+ 'source_instance' => $qb->createNamedParameter($owner->instance),
+ 'source_node_id' => $qb->createNamedParameter($sourceNodeId),
+ 'target' => $qb->createNamedParameter($target),
+ ])
+ ->executeStatement();
+
+ return $target;
+ }
+
+ public function getTarget(string $userId, ShareUser $owner, int $sourceNodeId): ?string {
+ return $this->getTargetsInternal([$userId], $owner, $sourceNodeId)[$userId] ?? null;
+ }
+
+ /**
+ * @param list $userIds
+ * @return array
+ */
+ public function getTargets(array $userIds, ShareUser $owner, int $sourceNodeId): array {
+ $targets = $this->getTargetsInternal($userIds, $owner, $sourceNodeId);
+ foreach ($userIds as $userId) {
+ if (isset($targets[$userId])) {
+ continue;
+ }
+
+ $targets[$userId] = $this->createDefaultTarget($userId, $owner, $sourceNodeId);
+ }
+
+ return $targets;
+ }
+
+ public function setTarget(string $userId, ShareUser $owner, int $sourceNodeId, string $target): void {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $rowCount = $qb
+ ->update('sharing_source_node_target')
+ ->set('target', $qb->createNamedParameter($target))
+ ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
+ ->andWhere(
+ $owner->instance === null
+ ? $qb->expr()->isNull('source_instance')
+ : $qb->expr()->eq('source_instance', $qb->createNamedParameter($owner->instance))
+ )
+ ->andWhere($qb->expr()->eq('source_node_id', $qb->createNamedParameter($sourceNodeId, IQueryBuilder::PARAM_INT)))
+ ->executeStatement();
+
+ if ($rowCount === 0) {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb
+ ->insert('sharing_source_node_target')
+ ->values([
+ 'user_id' => $qb->createNamedParameter($userId),
+ 'source_instance' => $qb->createNamedParameter($owner->instance),
+ 'source_node_id' => $qb->createNamedParameter($sourceNodeId),
+ 'target' => $qb->createNamedParameter($target),
+ ])
+ ->executeStatement();
+ }
+ }
+}
diff --git a/apps/files/tests/Sharing/SharesUpdatedListenerTest.php b/apps/files/tests/Sharing/SharesUpdatedListenerTest.php
new file mode 100644
index 0000000000000..7ca992598119d
--- /dev/null
+++ b/apps/files/tests/Sharing/SharesUpdatedListenerTest.php
@@ -0,0 +1,108 @@
+owner = $this->createUser('owner', 'password');
+ $this->user = $this->createUser('user', 'password');
+
+ $this->node = Server::get(IRootFolder::class)->getUserFolder($this->owner->getUID())->newFile('foo.txt');
+
+ $this->dbConnection = Server::get(IDBConnection::class);
+ $this->sharingManager = Server::get(ISharingManager::class);
+ $this->sourceNodeTargetManager = Server::get(SourceNodeTargetManager::class);
+ }
+
+ #[Override]
+ public function tearDown(): void {
+ $qb = Server::get(IDBConnection::class)->getQueryBuilder();
+ $qb
+ ->delete('sharing_source_node_target')
+ ->executeStatement();
+
+ $accessContext = new ShareAccessContext(overrideChecks: true);
+ $this->dbConnection->beginTransaction();
+ foreach ($this->sharingManager->getShares($accessContext, null, null, null, null, null, null) as $share) {
+ $this->sharingManager->deleteShare($accessContext, $share);
+ }
+
+ $this->dbConnection->commit();
+
+ parent::tearDown();
+ }
+
+ private function getTargetWithoutDefault(): ?string {
+ /** @psalm-suppress MixedReturnStatement */
+ return$this->invokePrivate($this->sourceNodeTargetManager, 'getTarget', [$this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node->getId()]);
+ }
+
+ public function test(): void {
+ $accessContext = new ShareAccessContext($this->owner);
+
+ $this->dbConnection->beginTransaction();
+
+ $share = $this->sharingManager->createShare($accessContext);
+ $this->assertNull($this->getTargetWithoutDefault());
+
+ $share = $this->sharingManager->addShareSource($accessContext, $share, new ShareSource(NodeShareSourceType::class, (string)$this->node->getId()));
+ $this->assertNull($this->getTargetWithoutDefault());
+
+ $share = $this->sharingManager->addShareRecipient($accessContext, $share, new ShareRecipient(UserShareRecipientType::class, $this->user->getUID(), null));
+ $this->assertNull($this->getTargetWithoutDefault());
+
+ $share = $this->sharingManager->updateSharePermission($accessContext, $share, new SharePermission(NodeReadSharePermissionType::class, true));
+ $this->assertNull($this->getTargetWithoutDefault());
+
+ $this->sharingManager->updateShareState($accessContext, $share, ShareState::Active);
+ $this->assertEquals('/foo.txt', $this->getTargetWithoutDefault());
+
+ $this->dbConnection->commit();
+ }
+}
diff --git a/apps/files/tests/Sharing/SourceNodeTargetManagerTest.php b/apps/files/tests/Sharing/SourceNodeTargetManagerTest.php
new file mode 100644
index 0000000000000..1579987077c8e
--- /dev/null
+++ b/apps/files/tests/Sharing/SourceNodeTargetManagerTest.php
@@ -0,0 +1,104 @@
+owner = $this->createUser('owner', 'password');
+ $this->user = $this->createUser('user', 'password');
+
+ $this->node = Server::get(IRootFolder::class)->getUserFolder($this->owner->getUID())->newFile('foo.txt');
+
+ $this->manager = Server::get(SourceNodeTargetManager::class);
+ }
+
+ #[Override]
+ public function tearDown(): void {
+ $qb = Server::get(IDBConnection::class)->getQueryBuilder();
+ $qb
+ ->delete('sharing_source_node_target')
+ ->executeStatement();
+
+ parent::tearDown();
+ }
+
+ public function testCreateDefaultTarget(): void {
+ $this->assertEquals('/foo.txt', $this->manager->createDefaultTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node->getId()));
+ }
+
+ public function testCreateDefaultSystem(): void {
+ $config = Server::get(IConfig::class);
+
+ $config->setSystemValue('share_folder', '/system');
+ $this->assertEquals('/system/foo.txt', $this->manager->createDefaultTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node->getId()));
+
+ $config->deleteSystemValue('share_folder');
+ }
+
+ public function testCreateDefaultUser(): void {
+ $userConfig = Server::get(IUserConfig::class);
+
+ $userConfig->setValueString($this->user->getUID(), Application::APP_ID, 'share_folder', '/user');
+ $this->assertEquals('/user/foo.txt', $this->manager->createDefaultTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node->getId()));
+
+ $userConfig->deleteUserConfig($this->user->getUID(), Application::APP_ID, 'share_folder');
+ }
+
+ public function testCreateDefaultUserNotAllowed(): void {
+ $config = Server::get(IConfig::class);
+ $userConfig = Server::get(IUserConfig::class);
+
+ $config->setSystemValue('sharing.allow_custom_share_folder', false);
+ $userConfig->setValueString($this->user->getUID(), Application::APP_ID, 'share_folder', '/user');
+ $this->assertEquals('/foo.txt', $this->manager->createDefaultTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node->getId()));
+
+ $config->deleteSystemValue('sharing.allow_custom_share_folder');
+ $userConfig->deleteUserConfig($this->user->getUID(), Application::APP_ID, 'share_folder');
+ }
+
+ public function testSetAndGetTarget(): void {
+ $this->assertNull($this->manager->getTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node->getId()));
+
+ $this->manager->setTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node->getId(), '/bar.txt');
+ $this->assertEquals('/bar.txt', $this->manager->getTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node->getId()));
+
+ $this->manager->setTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node->getId(), '/baz.txt');
+ $this->assertEquals('/baz.txt', $this->manager->getTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node->getId()));
+ }
+}
diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php
index 854c92aad0060..3a667550839f8 100644
--- a/apps/files_sharing/lib/Controller/ShareAPIController.php
+++ b/apps/files_sharing/lib/Controller/ShareAPIController.php
@@ -699,6 +699,7 @@ public function createShare(
if ($expireDate !== '') {
try {
$expireDateTime = $this->parseDate($expireDate);
+ $expireDateTime->setTime(23, 59, 59);
$share->setExpirationDate($expireDateTime);
} catch (\Exception $e) {
throw new OCSNotFoundException($e->getMessage(), $e);
@@ -1377,6 +1378,7 @@ public function updateShare(
} elseif ($expireDate !== null) {
try {
$expireDateTime = $this->parseDate($expireDate);
+ $expireDateTime->setTime(23, 59, 59);
$share->setExpirationDate($expireDateTime);
} catch (\Exception $e) {
throw new OCSBadRequestException($e->getMessage(), $e);
diff --git a/apps/sharebymail/lib/ShareByMailProvider.php b/apps/sharebymail/lib/ShareByMailProvider.php
index 6f51cf09b7c85..53dbf3428e21e 100644
--- a/apps/sharebymail/lib/ShareByMailProvider.php
+++ b/apps/sharebymail/lib/ShareByMailProvider.php
@@ -1047,7 +1047,8 @@ protected function createShareObject(array $data): IShare {
->setTarget($data['file_target'])
->setMailSend((bool)$data['mail_send'])
->setNote($data['note'])
- ->setToken($data['token']);
+ ->setToken($data['token'])
+ ->setStatus((int)$data['accepted']);
$shareTime = new \DateTime();
$shareTime->setTimestamp((int)$data['stime']);
@@ -1253,7 +1254,11 @@ public function getAccessList($nodes, $currentAccess): array {
}
#[\Override]
- public function getAllShares(): iterable {
+ public function getAllShares(bool $withUserGroup = false): iterable {
+ if ($withUserGroup) {
+ throw new RuntimeException('Unintended usage.');
+ }
+
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
diff --git a/apps/sharing/appinfo/info.xml b/apps/sharing/appinfo/info.xml
index b46cd273f56f8..c78760cb54a8a 100644
--- a/apps/sharing/appinfo/info.xml
+++ b/apps/sharing/appinfo/info.xml
@@ -9,7 +9,7 @@
Sharing
This app provides APIs and occ commands to manage shares.
This app provides APIs and occ commands to manage shares.
- 2.0.0-dev.3
+ 2.0.0-dev.4
AGPL-3.0-or-later
Kate Döen
Sharing
diff --git a/apps/sharing/composer/composer/autoload_classmap.php b/apps/sharing/composer/composer/autoload_classmap.php
index d2b48b8753319..b687f4694eab0 100644
--- a/apps/sharing/composer/composer/autoload_classmap.php
+++ b/apps/sharing/composer/composer/autoload_classmap.php
@@ -32,6 +32,7 @@
'OCA\\Sharing\\Migration\\Version1000Date20260826073021' => $baseDir . '/../lib/Migration/Version1000Date20260826073021.php',
'OCA\\Sharing\\Migration\\Version1000Date20260826115938' => $baseDir . '/../lib/Migration/Version1000Date20260826115938.php',
'OCA\\Sharing\\Migration\\Version1000Date20260826122228' => $baseDir . '/../lib/Migration/Version1000Date20260826122228.php',
+ 'OCA\\Sharing\\Migration\\Version1000Date20260903133238' => $baseDir . '/../lib/Migration/Version1000Date20260903133238.php',
'OCA\\Sharing\\Migration\\Version2000Date20261013142538' => $baseDir . '/../lib/Migration/Version2000Date20261013142538.php',
'OCA\\Sharing\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
);
diff --git a/apps/sharing/composer/composer/autoload_static.php b/apps/sharing/composer/composer/autoload_static.php
index 60852a2cd612b..61f5b89c6a5cc 100644
--- a/apps/sharing/composer/composer/autoload_static.php
+++ b/apps/sharing/composer/composer/autoload_static.php
@@ -47,6 +47,7 @@ class ComposerStaticInitSharing
'OCA\\Sharing\\Migration\\Version1000Date20260826073021' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20260826073021.php',
'OCA\\Sharing\\Migration\\Version1000Date20260826115938' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20260826115938.php',
'OCA\\Sharing\\Migration\\Version1000Date20260826122228' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20260826122228.php',
+ 'OCA\\Sharing\\Migration\\Version1000Date20260903133238' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20260903133238.php',
'OCA\\Sharing\\Migration\\Version2000Date20261013142538' => __DIR__ . '/..' . '/../lib/Migration/Version2000Date20261013142538.php',
'OCA\\Sharing\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
);
diff --git a/apps/sharing/lib/Controller/ApiV1Controller.php b/apps/sharing/lib/Controller/ApiV1Controller.php
index 1e5b4d42ad3b8..eb4b28266798f 100644
--- a/apps/sharing/lib/Controller/ApiV1Controller.php
+++ b/apps/sharing/lib/Controller/ApiV1Controller.php
@@ -50,8 +50,6 @@
use RuntimeException;
use ValueError;
-// TODO: Add "recipient suggestions" endpoint
-
/**
* @psalm-import-type SharingShare from ResponseDefinitions
* @psalm-import-type SharingRecipient from ResponseDefinitions
diff --git a/apps/sharing/lib/Migration/Version1000Date20260903133238.php b/apps/sharing/lib/Migration/Version1000Date20260903133238.php
new file mode 100644
index 0000000000000..a905538d59040
--- /dev/null
+++ b/apps/sharing/lib/Migration/Version1000Date20260903133238.php
@@ -0,0 +1,36 @@
+createTable('sharing_share_legacy_mapping');
+ $legacyMappingTable->addColumn('id', Types::BIGINT);
+ $legacyMappingTable->addColumn('legacy_provider', Types::STRING, ['length' => 128]);
+ $legacyMappingTable->addColumn('legacy_id', Types::INTEGER);
+ $legacyMappingTable->addColumn('last_updated', Types::BIGINT);
+ $legacyMappingTable->addColumn('recipient_secrets', Types::TEXT);
+ $legacyMappingTable->addColumn('recipient_value', Types::STRING, ['length' => 255]);
+ $legacyMappingTable->setPrimaryKey(['legacy_provider', 'legacy_id']);
+ $legacyMappingTable->addIndex(['id']);
+
+ return $schema;
+ }
+}
diff --git a/build/integration/run.sh b/build/integration/run.sh
index 4ff808b5056b9..7c376c1c34330 100755
--- a/build/integration/run.sh
+++ b/build/integration/run.sh
@@ -36,6 +36,9 @@ if [ "$INSTALLED" == "true" ]; then
$OCC config:system:set sharing.federation.allowSelfSignedCertificates --value true --type bool
# Allow creating users with dummy passwords
$OCC app:disable password_policy
+ # Enable Unified Sharing legacy sync validation
+ $OCC config:system:set --type=boolean --value=true sharing.unified_api_enable
+ $OCC config:system:set --type=boolean --value=true unified_sharing.legacy_sync.validation.enable
else
if [ "$SCENARIO_TO_RUN" != "setup_features/setup.feature" ]; then
echo "Nextcloud instance needs to be installed" >&2
diff --git a/build/rector-strict.php b/build/rector-strict.php
index a4457f40dc22d..bb3e0924ce6bb 100644
--- a/build/rector-strict.php
+++ b/build/rector-strict.php
@@ -63,6 +63,9 @@
$nextcloudDir . '/lib/public/SystemReport',
$nextcloudDir . '/lib/private/SystemReport',
$nextcloudDir . '/tests/lib/SystemReport',
+ $nextcloudDir . '/lib/public/Share/Events/ShareUpdatedEvent.php',
+ $nextcloudDir . '/lib/public/Share/Events/ShareRestoredEvent.php',
+ $nextcloudDir . '/apps/files/lib/Migration/Version35000Date20260910082613.php',
])
->withAutoloadPaths([
// ensure rector properly autoload the public interfaces
diff --git a/core/AppInfo/Application.php b/core/AppInfo/Application.php
index 31b79d4e85213..210f03e68b1ed 100644
--- a/core/AppInfo/Application.php
+++ b/core/AppInfo/Application.php
@@ -124,6 +124,10 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(UserDeletedEvent::class, UserDeletedDirectEditingTokenCleanupListener::class);
$context->registerEventListener(UserChangedEvent::class, UserDisabledDirectEditingTokenCleanupListener::class);
+ $this->registerSharing();
+ }
+
+ private function registerSharing(): void {
$registry = Server::get(ISharingRegistry::class);
$registry->registerRecipientType(Server::get(EmailShareRecipientType::class));
diff --git a/core/Sharing/Recipient/EmailShareRecipientType.php b/core/Sharing/Recipient/EmailShareRecipientType.php
index a42d43a8c8944..f0157c10f7110 100644
--- a/core/Sharing/Recipient/EmailShareRecipientType.php
+++ b/core/Sharing/Recipient/EmailShareRecipientType.php
@@ -44,6 +44,11 @@ public function getRecipients(?IUser $currentUser, mixed $arguments): array {
return [];
}
+ #[\Override]
+ public function getUsers(string $recipient): array {
+ return [];
+ }
+
#[\Override]
public function getRecipientDisplayName(string $recipient, ?string $instance): ?string {
if ($instance !== null) {
diff --git a/core/Sharing/Recipient/GroupShareRecipientType.php b/core/Sharing/Recipient/GroupShareRecipientType.php
index 53c1de0629961..6074d2738e078 100644
--- a/core/Sharing/Recipient/GroupShareRecipientType.php
+++ b/core/Sharing/Recipient/GroupShareRecipientType.php
@@ -64,6 +64,11 @@ public function getRecipients(?IUser $currentUser, mixed $arguments): array {
return $this->groupManager->getUserGroupIds($currentUser);
}
+ #[\Override]
+ public function getUsers(string $recipient): array {
+ return array_keys($this->groupManager->get($recipient)?->getUsers() ?? []);
+ }
+
#[\Override]
public function getRecipientDisplayName(string $recipient, ?string $instance): ?string {
if ($instance !== null) {
diff --git a/core/Sharing/Recipient/TeamShareRecipientType.php b/core/Sharing/Recipient/TeamShareRecipientType.php
index 61a02fded937b..734362badbb08 100644
--- a/core/Sharing/Recipient/TeamShareRecipientType.php
+++ b/core/Sharing/Recipient/TeamShareRecipientType.php
@@ -69,6 +69,11 @@ public function getRecipients(?IUser $currentUser, mixed $arguments): array {
return array_map(static fn (Team $team): string => $team->getId(), $this->getTeamManager()->getTeamsForUser($currentUser->getUID()));
}
+ #[\Override]
+ public function getUsers(string $recipient): array {
+ return array_keys($this->getTeamManager()->getMembersOfTeam($recipient, null));
+ }
+
#[\Override]
public function getRecipientDisplayName(string $recipient, ?string $instance): ?string {
if ($instance !== null) {
diff --git a/core/Sharing/Recipient/TokenShareRecipientType.php b/core/Sharing/Recipient/TokenShareRecipientType.php
index 115a45e89cdce..be92071bdbe36 100644
--- a/core/Sharing/Recipient/TokenShareRecipientType.php
+++ b/core/Sharing/Recipient/TokenShareRecipientType.php
@@ -42,6 +42,11 @@ public function getRecipients(?IUser $currentUser, mixed $arguments): array {
return [];
}
+ #[\Override]
+ public function getUsers(string $recipient): array {
+ return [];
+ }
+
#[\Override]
public function getRecipientDisplayName(string $recipient, ?string $instance): ?string {
return null;
diff --git a/core/Sharing/Recipient/UserShareRecipientType.php b/core/Sharing/Recipient/UserShareRecipientType.php
index 0b4010136a4fb..633e114c15ea3 100644
--- a/core/Sharing/Recipient/UserShareRecipientType.php
+++ b/core/Sharing/Recipient/UserShareRecipientType.php
@@ -63,6 +63,11 @@ public function getRecipients(?IUser $currentUser, mixed $arguments): array {
return [$currentUser->getUID()];
}
+ #[\Override]
+ public function getUsers(string $recipient): array {
+ return [$recipient];
+ }
+
#[\Override]
public function getRecipientDisplayName(string $recipient, ?string $instance): ?string {
if ($instance !== null) {
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index b3b5b63fb3a1e..8bc58fc7f1de7 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -45,6 +45,8 @@
'NCU\\Security\\Signature\\ISignedRequest' => $baseDir . '/lib/unstable/Security/Signature/ISignedRequest.php',
'NCU\\Security\\Signature\\Model\\Signatory' => $baseDir . '/lib/unstable/Security/Signature/Model/Signatory.php',
'NCU\\Sharing\\Event\\SharesDefaultSetEvent' => $baseDir . '/lib/unstable/Sharing/Event/SharesDefaultSetEvent.php',
+ 'NCU\\Sharing\\Event\\SharesDeletedEvent' => $baseDir . '/lib/unstable/Sharing/Event/SharesDeletedEvent.php',
+ 'NCU\\Sharing\\Event\\SharesUpdatedEvent' => $baseDir . '/lib/unstable/Sharing/Event/SharesUpdatedEvent.php',
'NCU\\Sharing\\Exception\\AShareException' => $baseDir . '/lib/unstable/Sharing/Exception/AShareException.php',
'NCU\\Sharing\\Exception\\ShareInvalidException' => $baseDir . '/lib/unstable/Sharing/Exception/ShareInvalidException.php',
'NCU\\Sharing\\Exception\\ShareNotFoundException' => $baseDir . '/lib/unstable/Sharing/Exception/ShareNotFoundException.php',
@@ -949,7 +951,9 @@
'OCP\\Share\\Events\\ShareDeletedEvent' => $baseDir . '/lib/public/Share/Events/ShareDeletedEvent.php',
'OCP\\Share\\Events\\ShareDeletedFromSelfEvent' => $baseDir . '/lib/public/Share/Events/ShareDeletedFromSelfEvent.php',
'OCP\\Share\\Events\\ShareMovedEvent' => $baseDir . '/lib/public/Share/Events/ShareMovedEvent.php',
+ 'OCP\\Share\\Events\\ShareRestoredEvent' => $baseDir . '/lib/public/Share/Events/ShareRestoredEvent.php',
'OCP\\Share\\Events\\ShareTransferredEvent' => $baseDir . '/lib/public/Share/Events/ShareTransferredEvent.php',
+ 'OCP\\Share\\Events\\ShareUpdatedEvent' => $baseDir . '/lib/public/Share/Events/ShareUpdatedEvent.php',
'OCP\\Share\\Events\\VerifyMountPointEvent' => $baseDir . '/lib/public/Share/Events/VerifyMountPointEvent.php',
'OCP\\Share\\Exceptions\\AlreadySharedException' => $baseDir . '/lib/public/Share/Exceptions/AlreadySharedException.php',
'OCP\\Share\\Exceptions\\GenericShareException' => $baseDir . '/lib/public/Share/Exceptions/GenericShareException.php',
@@ -2390,7 +2394,10 @@
'OC\\Share20\\UserRemovedListener' => $baseDir . '/lib/private/Share20/UserRemovedListener.php',
'OC\\Share\\Constants' => $baseDir . '/lib/private/Share/Constants.php',
'OC\\Sharing\\ClassMapper' => $baseDir . '/lib/private/Sharing/ClassMapper.php',
+ 'OC\\Sharing\\LegacyMapper' => $baseDir . '/lib/private/Sharing/LegacyMapper.php',
+ 'OC\\Sharing\\LegacyMapping' => $baseDir . '/lib/private/Sharing/LegacyMapping.php',
'OC\\Sharing\\SharingBackend' => $baseDir . '/lib/private/Sharing/SharingBackend.php',
+ 'OC\\Sharing\\SharingLegacySync' => $baseDir . '/lib/private/Sharing/SharingLegacySync.php',
'OC\\Sharing\\SharingManager' => $baseDir . '/lib/private/Sharing/SharingManager.php',
'OC\\Sharing\\SharingRegistry' => $baseDir . '/lib/private/Sharing/SharingRegistry.php',
'OC\\Snowflake\\APCuSequence' => $baseDir . '/lib/private/Snowflake/APCuSequence.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 35c872fd7527d..5126dca67553a 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -86,6 +86,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'NCU\\Security\\Signature\\ISignedRequest' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/ISignedRequest.php',
'NCU\\Security\\Signature\\Model\\Signatory' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Model/Signatory.php',
'NCU\\Sharing\\Event\\SharesDefaultSetEvent' => __DIR__ . '/../../..' . '/lib/unstable/Sharing/Event/SharesDefaultSetEvent.php',
+ 'NCU\\Sharing\\Event\\SharesDeletedEvent' => __DIR__ . '/../../..' . '/lib/unstable/Sharing/Event/SharesDeletedEvent.php',
+ 'NCU\\Sharing\\Event\\SharesUpdatedEvent' => __DIR__ . '/../../..' . '/lib/unstable/Sharing/Event/SharesUpdatedEvent.php',
'NCU\\Sharing\\Exception\\AShareException' => __DIR__ . '/../../..' . '/lib/unstable/Sharing/Exception/AShareException.php',
'NCU\\Sharing\\Exception\\ShareInvalidException' => __DIR__ . '/../../..' . '/lib/unstable/Sharing/Exception/ShareInvalidException.php',
'NCU\\Sharing\\Exception\\ShareNotFoundException' => __DIR__ . '/../../..' . '/lib/unstable/Sharing/Exception/ShareNotFoundException.php',
@@ -990,7 +992,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Share\\Events\\ShareDeletedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareDeletedEvent.php',
'OCP\\Share\\Events\\ShareDeletedFromSelfEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareDeletedFromSelfEvent.php',
'OCP\\Share\\Events\\ShareMovedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareMovedEvent.php',
+ 'OCP\\Share\\Events\\ShareRestoredEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareRestoredEvent.php',
'OCP\\Share\\Events\\ShareTransferredEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareTransferredEvent.php',
+ 'OCP\\Share\\Events\\ShareUpdatedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareUpdatedEvent.php',
'OCP\\Share\\Events\\VerifyMountPointEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/VerifyMountPointEvent.php',
'OCP\\Share\\Exceptions\\AlreadySharedException' => __DIR__ . '/../../..' . '/lib/public/Share/Exceptions/AlreadySharedException.php',
'OCP\\Share\\Exceptions\\GenericShareException' => __DIR__ . '/../../..' . '/lib/public/Share/Exceptions/GenericShareException.php',
@@ -2431,7 +2435,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Share20\\UserRemovedListener' => __DIR__ . '/../../..' . '/lib/private/Share20/UserRemovedListener.php',
'OC\\Share\\Constants' => __DIR__ . '/../../..' . '/lib/private/Share/Constants.php',
'OC\\Sharing\\ClassMapper' => __DIR__ . '/../../..' . '/lib/private/Sharing/ClassMapper.php',
+ 'OC\\Sharing\\LegacyMapper' => __DIR__ . '/../../..' . '/lib/private/Sharing/LegacyMapper.php',
+ 'OC\\Sharing\\LegacyMapping' => __DIR__ . '/../../..' . '/lib/private/Sharing/LegacyMapping.php',
'OC\\Sharing\\SharingBackend' => __DIR__ . '/../../..' . '/lib/private/Sharing/SharingBackend.php',
+ 'OC\\Sharing\\SharingLegacySync' => __DIR__ . '/../../..' . '/lib/private/Sharing/SharingLegacySync.php',
'OC\\Sharing\\SharingManager' => __DIR__ . '/../../..' . '/lib/private/Sharing/SharingManager.php',
'OC\\Sharing\\SharingRegistry' => __DIR__ . '/../../..' . '/lib/private/Sharing/SharingRegistry.php',
'OC\\Snowflake\\APCuSequence' => __DIR__ . '/../../..' . '/lib/private/Snowflake/APCuSequence.php',
diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php
index cd9e65cd757f5..d7b2d5cf042f6 100644
--- a/lib/private/Share20/DefaultShareProvider.php
+++ b/lib/private/Share20/DefaultShareProvider.php
@@ -1137,7 +1137,11 @@ private function createShare($data): IShare {
$shareTime->setTimestamp((int)$data['stime']);
$share->setShareTime($shareTime);
- if ($share->getShareType() === IShare::TYPE_USER) {
+ if (($parent = $data['parent']) !== null) {
+ $share->setParent((int)$parent);
+ }
+
+ if ($share->getShareType() === IShare::TYPE_USER || $share->getShareType() === IShare::TYPE_USERGROUP) {
$share->setSharedWith($data['share_with']);
$share->setSharedWithDisplayNameCallback(fn (IShare $share) => $this->userManager->getDisplayName($share->getSharedWith()));
} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
@@ -1760,12 +1764,17 @@ private function sendNote(array $recipients, IShare $share) {
}
#[\Override]
- public function getAllShares(): iterable {
+ public function getAllShares(bool $withUserGroup = false): iterable {
+ $shareTypes = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK];
+ if ($withUserGroup) {
+ $shareTypes[] = IShare::TYPE_USERGROUP;
+ }
+
$qb = $this->dbConn->getQueryBuilder();
$qb->select('*')
->from('share')
- ->where($qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK], IQueryBuilder::PARAM_INT_ARRAY)));
+ ->where($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY)));
$cursor = $qb->executeQuery();
while ($data = $cursor->fetchAssociative()) {
diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php
index 1971abbdb6508..f8f9a921cc4e0 100644
--- a/lib/private/Share20/Manager.php
+++ b/lib/private/Share20/Manager.php
@@ -67,6 +67,8 @@
use OCP\Share\Events\ShareDeletedEvent;
use OCP\Share\Events\ShareDeletedFromSelfEvent;
use OCP\Share\Events\ShareMovedEvent;
+use OCP\Share\Events\ShareRestoredEvent;
+use OCP\Share\Events\ShareUpdatedEvent;
use OCP\Share\Exceptions\AlreadySharedException;
use OCP\Share\Exceptions\GenericShareException;
use OCP\Share\Exceptions\ShareNotFound;
@@ -300,7 +302,6 @@ protected function validateExpirationDate(
if (!$share->getNoExpirationDate() || $isEnforced) {
if ($expirationDate !== null) {
$expirationDate->setTimezone($this->dateTimeZone->getTimeZone());
- $expirationDate->setTime(23, 59, 59);
$date = new \DateTime('now', $this->dateTimeZone->getTimeZone());
$date->setTime(0, 0, 0);
@@ -611,7 +612,7 @@ public function createShare(IShare $share): IShare {
$share->setNode($oldShare->getNode());
// Reset the target if it is null for the new share
- if ($share->getTarget() === '') {
+ if ($share->getTarget() === null) {
$share->setTarget($target);
}
} catch (AlreadySharedException $e) {
@@ -636,7 +637,9 @@ public function createShare(IShare $share): IShare {
}
// Post share event
- $this->dispatchEvent(new ShareCreatedEvent($share), 'share created');
+ $event = new ShareCreatedEvent($share);
+ $this->dispatchEvent($event, 'share created');
+ $share = $event->getShare();
// Send email if needed
if ($this->config->getSystemValueBool('sharing.enable_share_mail', true)) {
@@ -762,6 +765,10 @@ public function updateShare(IShare $share, bool $onlyValid = true): IShare {
$share = $provider->update($share);
}
+ $event = new ShareUpdatedEvent($share);
+ $this->dispatcher->dispatchTyped($event);
+ $share = $event->getShare();
+
if ($expirationDateUpdated === true) {
\OC_Hook::emit(Share::class, 'post_set_expiration_date', [
'itemType' => $share->getNode() instanceof File ? 'file' : 'folder',
@@ -815,6 +822,7 @@ public function acceptShare(IShare $share, string $recipientId): IShare {
$event = new ShareAcceptedEvent($share);
$this->dispatchEvent($event, 'share accepted');
+ $share = $event->getShare();
return $share;
}
@@ -1080,7 +1088,13 @@ public function restoreShare(IShare $share, string $recipientId): IShare {
[$providerId,] = $this->splitFullId($share->getFullId());
$provider = $this->factory->getProvider($providerId);
- return $provider->restore($share, $recipientId);
+ $share = $provider->restore($share, $recipientId);
+
+ $event = new ShareRestoredEvent($share);
+ $this->dispatcher->dispatchTyped($event);
+ $share = $event->getShare();
+
+ return $share;
}
#[Override]
@@ -1111,11 +1125,13 @@ public function moveShare(IShare $share, string $recipientId): IShare {
[$providerId,] = $this->splitFullId($share->getFullId());
$provider = $this->factory->getProvider($providerId);
- $result = $provider->move($share, $recipientId);
+ $share = $provider->move($share, $recipientId);
- $this->dispatchEvent(new ShareMovedEvent($share, $recipient), 'share moved');
+ $event = new ShareMovedEvent($share, $recipient);
+ $this->dispatchEvent($event, 'share moved');
+ $share = $event->getShare();
- return $result;
+ return $share;
}
#[Override]
@@ -1501,6 +1517,10 @@ public function checkPassword(IShare $share, ?string $password): bool {
$share->setPasswordHash($newHash);
$provider = $this->factory->getProviderForType($share->getShareType());
$provider->update($share);
+
+ $event = new ShareUpdatedEvent($share);
+ $this->dispatcher->dispatchTyped($event);
+ $share = $event->getShare();
}
return true;
diff --git a/lib/private/Share20/Share.php b/lib/private/Share20/Share.php
index 35c70a1464bfc..e7e2c1e92893e 100644
--- a/lib/private/Share20/Share.php
+++ b/lib/private/Share20/Share.php
@@ -123,6 +123,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/LegacyMapper.php b/lib/private/Sharing/LegacyMapper.php
new file mode 100644
index 0000000000000..205a95703acef
--- /dev/null
+++ b/lib/private/Sharing/LegacyMapper.php
@@ -0,0 +1,139 @@
+ $recipientSecrets
+ */
+ public function createLegacyMapping(string $id, string $legacyProvider, int $legacyId, DateTimeImmutable $lastUpdated, array $recipientSecrets, string $recipientValue): LegacyMapping {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb
+ ->insert('sharing_share_legacy_mapping')
+ ->values([
+ 'id' => $qb->createNamedParameter($id),
+ 'legacy_provider' => $qb->createNamedParameter($legacyProvider),
+ 'legacy_id' => $qb->createNamedParameter($legacyId, IQueryBuilder::PARAM_INT),
+ 'last_updated' => $qb->createNamedParameter(SharingManager::timeToMs($lastUpdated)),
+ 'recipient_secrets' => $qb->createNamedParameter(json_encode($recipientSecrets)),
+ 'recipient_value' => $qb->createNamedParameter($recipientValue),
+ ])
+ ->executeStatement();
+
+ return new LegacyMapping(
+ $id,
+ $legacyProvider,
+ $legacyId,
+ $lastUpdated,
+ $recipientSecrets,
+ $recipientValue,
+ );
+ }
+
+ public function getLegacyMappingByLegacyProviderAndId(string $legacyProvider, int $legacyId): ?LegacyMapping {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $result = $qb
+ ->select('id', 'last_updated', 'recipient_secrets', 'recipient_value')
+ ->from('sharing_share_legacy_mapping')
+ ->where($qb->expr()->eq('legacy_provider', $qb->createNamedParameter($legacyProvider)))
+ ->andWhere($qb->expr()->eq('legacy_id', $qb->createNamedParameter($legacyId, IQueryBuilder::PARAM_INT)))
+ ->executeQuery();
+
+ /** @var array{id: int, last_updated: int, recipient_secrets: non-empty-string, recipient_value: string}|false $row */
+ $row = $result->fetch();
+ if ($row === false) {
+ return null;
+ }
+
+ return new LegacyMapping(
+ (string)$row['id'],
+ $legacyProvider,
+ $legacyId,
+ SharingBackend::parseTimestamp((string)$row['last_updated']),
+ $this->decodeRecipientSecrets($row['recipient_secrets']),
+ $row['recipient_value'],
+ );
+ }
+
+ /**
+ * @return list
+ */
+ public function getLegacyMappings(string $id): array {
+ $legacyMappings = [];
+
+ $qb = $this->dbConnection->getQueryBuilder();
+ $result = $qb
+ ->select('id', 'legacy_provider', 'legacy_id', 'last_updated', 'recipient_secrets', 'recipient_value')
+ ->from('sharing_share_legacy_mapping')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
+ ->executeQuery();
+
+ /** @var list $rows */
+ $rows = $result->fetchAllAssociative();
+ foreach ($rows as $row) {
+ $legacyMappings[] = new LegacyMapping(
+ (string)$row['id'],
+ $row['legacy_provider'],
+ $row['legacy_id'],
+ SharingBackend::parseTimestamp((string)$row['last_updated']),
+ $this->decodeRecipientSecrets($row['recipient_secrets']),
+ $row['recipient_value'],
+ );
+ }
+
+ return $legacyMappings;
+ }
+
+ public function deleteLegacyMappings(string $id): void {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb
+ ->delete('sharing_share_legacy_mapping')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
+ ->executeStatement();
+ }
+
+ public function updateLegacyMapping(LegacyMapping $legacyMapping): void {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb
+ ->update('sharing_share_legacy_mapping')
+ ->set('last_updated', $qb->createNamedParameter(SharingManager::timeToMs($legacyMapping->lastUpdated)))
+ ->set('recipient_secrets', $qb->createNamedParameter(json_encode($legacyMapping->recipientSecrets)))
+ ->set('recipient_value', $qb->createNamedParameter($legacyMapping->recipientValue))
+ ->where($qb->expr()->eq('legacy_provider', $qb->createNamedParameter($legacyMapping->legacyProvider)))
+ ->andWhere($qb->expr()->eq('legacy_id', $qb->createNamedParameter($legacyMapping->legacyId)))
+ ->executeStatement();
+ }
+
+ /**
+ * @psalm-suppress MixedReturnTypeCoercion Psalm doesn't understand the string checks
+ * @param non-empty-string $raw
+ * @return non-empty-list
+ */
+ private function decodeRecipientSecrets(string $raw): array {
+ $data = json_decode($raw);
+ if (!is_array($data) || !array_is_list($data) || $data === [] || array_any($data, static fn (mixed $value): bool => !is_string($value) || $value === '')) {
+ throw new RuntimeException('The recipient secrets must be a non-empty list of non-empty strings.');
+ }
+
+ return $data;
+ }
+}
diff --git a/lib/private/Sharing/LegacyMapping.php b/lib/private/Sharing/LegacyMapping.php
new file mode 100644
index 0000000000000..d456bd303d258
--- /dev/null
+++ b/lib/private/Sharing/LegacyMapping.php
@@ -0,0 +1,43 @@
+ $recipientSecrets
+ */
+ public array $recipientSecrets,
+ public string $recipientValue,
+ ) {
+ }
+
+ /**
+ * @throws ShareNotFound
+ */
+ public function getLegacyShare(IManager $legacySharingManager): IShare {
+ // Not cached on purpose, so we always get the current state of the share.
+ return $legacySharingManager->getShareById(
+ $this->legacyProvider . ':' . $this->legacyId,
+ onlyValid: false,
+ );
+ }
+}
diff --git a/lib/private/Sharing/SharingBackend.php b/lib/private/Sharing/SharingBackend.php
index 1e3bcc1ee321b..847feddd93839 100644
--- a/lib/private/Sharing/SharingBackend.php
+++ b/lib/private/Sharing/SharingBackend.php
@@ -1141,12 +1141,6 @@ private function list(
/** @var ?string $value */
$value = $row['property_value'];
-
- $propertyType = $registryPropertyTypes[$propertyTypeClass];
- if ($propertyType instanceof ISharePropertyTypeModifyValue) {
- $value = $propertyType->modifyValueOnLoad($value);
- }
-
$shares[$shareId]['properties'][$propertyTypeClass] = new ShareProperty($propertyTypeClass, $value);
}
}
@@ -1314,9 +1308,6 @@ public function createSharePropertyDefaultValue(Share $share, string $propertyTy
$property = new ShareProperty($propertyTypeClass, $propertyType->getDefaultValue($share));
$value = $this->updateShareProperty($share->id, $property);
- if ($propertyType instanceof ISharePropertyTypeModifyValue) {
- $value = $propertyType->modifyValueOnLoad($value);
- }
$property = new ShareProperty(
$property->class,
@@ -1465,7 +1456,7 @@ public function getRecommendedRecipients(
}, $rows)));
}
- private static function parseTimestamp(string $timestampMs): \DateTimeImmutable {
+ public static function parseTimestamp(string $timestampMs): \DateTimeImmutable {
if (method_exists(\DateTimeImmutable::class, 'createFromTimestamp')) {
// with php 8.3 the method doesn't exist and psalm doesn't know the return type
/** @psalm-suppress MixedReturnStatement */
@@ -1491,4 +1482,27 @@ private function isNullOrEqual(IQueryBuilder $query, string $field, ?string $val
? $query->expr()->isNull($field)
: $query->expr()->eq($field, $query->createNamedParameter($value));
}
+
+ #[\Override]
+ public function getUserStatuses(string $id, array $userIds): array {
+ $this->assertInTransaction();
+
+ $userStatuses = [];
+ foreach (array_chunk($userIds, 1000) as $chunk) {
+ $qb = $this->connection->getQueryBuilder();
+ $result = $qb
+ ->select('user_id', 'status_id')
+ ->from('sharing_share_user_status')
+ ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($id)))
+ ->andWhere($qb->expr()->in('user_id', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY)))
+ ->executeQuery();
+ /** @var list */
+ $rows = $result->fetchAll();
+ foreach ($rows as $row) {
+ $userStatuses[$row['user_id']] = ShareUserStatus::decode($row['status_id']);
+ }
+ }
+
+ return $userStatuses;
+ }
}
diff --git a/lib/private/Sharing/SharingLegacySync.php b/lib/private/Sharing/SharingLegacySync.php
new file mode 100644
index 0000000000000..de5fdeb223873
--- /dev/null
+++ b/lib/private/Sharing/SharingLegacySync.php
@@ -0,0 +1,1186 @@
+
+ */
+final class SharingLegacySync implements IEventListener {
+ private ?ISharingManager $sharingManager = null;
+
+ private ?SourceNodeTargetManager $sourceNodeTargetManager = null;
+
+ /** @var array */
+ private array $sharesInSync = [];
+
+ // Only used for testing!
+ private bool $ignoreAllEvents = false;
+
+ private bool $ignoreNewLegacyShare = false;
+
+ public function __construct(
+ private readonly IEventDispatcher $eventDispatcher,
+ private readonly IManager $legacySharingManager,
+ private readonly ISharingBackend $backend,
+ private readonly IDBConnection $dbConnection,
+ private readonly ISnowflakeGenerator $snowflakeGenerator,
+ private readonly ICloudIdManager $cloudIdManager,
+ private readonly LegacyMapper $legacyMapper,
+ private readonly ISharingRegistry $sharingRegistry,
+ private readonly IConfig $config,
+ private readonly IMimeTypeLoader $mimeTypeLoader,
+ ) {
+ $this->eventDispatcher->addServiceListener(ShareAcceptedEvent::class, self::class);
+ $this->eventDispatcher->addServiceListener(ShareCreatedEvent::class, self::class);
+ $this->eventDispatcher->addServiceListener(ShareDeletedEvent::class, self::class);
+ $this->eventDispatcher->addServiceListener(ShareDeletedFromSelfEvent::class, self::class);
+ $this->eventDispatcher->addServiceListener(ShareMovedEvent::class, self::class);
+ $this->eventDispatcher->addServiceListener(ShareRestoredEvent::class, self::class);
+ $this->eventDispatcher->addServiceListener(ShareUpdatedEvent::class, self::class);
+ $this->eventDispatcher->addServiceListener(SharesUpdatedEvent::class, self::class);
+ $this->eventDispatcher->addServiceListener(SharesDeletedEvent::class, self::class);
+ }
+
+ private function getSharingManager(): ISharingManager {
+ // Can't inject in constructor, as it leads to an infinite loop
+ return $this->sharingManager ??= Server::get(ISharingManager::class);
+ }
+
+ private function getSourceNodeTargetManager(): SourceNodeTargetManager {
+ // Class is not resolved when DI tries to construct this class.
+ return $this->sourceNodeTargetManager ??= Server::get(SourceNodeTargetManager::class);
+ }
+
+ private function isValidationEnabled(): bool {
+ return $this->config->getSystemValueBool('unified_sharing.legacy_sync.validation.enable');
+ }
+
+ private function isPublicLegacyShare(IShare $legacyShare): bool {
+ return $legacyShare->getShareType() === IShare::TYPE_LINK || $legacyShare->getShareType() === IShare::TYPE_EMAIL;
+ }
+
+ private function isRemoteLegacyShare(IShare $legacyShare): bool {
+ return $legacyShare->getShareType() === IShare::TYPE_REMOTE || $legacyShare->getShareType() === IShare::TYPE_REMOTE_GROUP;
+ }
+
+ // TODO: Test
+ public function mapLegacyShares(?IUser $user, ?int $limit = null): void {
+ // TODO: Make it work with all providers (deck, talk, mail, external etc.)
+ $provider = 'ocinternal';
+ // TODO: Filter by user
+ $qb = $this->dbConnection->getQueryBuilder();
+ $result = $qb
+ ->select('s.id', 's.stime')
+ ->from('share', 's')
+ ->leftJoin('s', 'sharing_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 $rows */
+ $rows = $result->fetchAll();
+ foreach ($rows as $row) {
+ $id = $this->snowflakeGenerator->nextId(DateTimeImmutable::createFromMutable((new DateTime())->setTimestamp($row['stime'])));
+ $legacyMapping = $this->legacyMapper->createLegacyMapping(
+ $id,
+ $provider,
+ $row['id'],
+ new DateTimeImmutable(),
+ [$this->getSharingManager()->generateSecret()],
+ $this->getSharingManager()->generateSecret(),
+ );
+
+ $this->syncLegacySharesToShare($id, [$legacyMapping]);
+ }
+ }
+
+ #[\Override]
+ public function handle(Event $event): void {
+ if ($this->ignoreAllEvents) {
+ return;
+ }
+
+ // TODO: Disable syncing if kill switch is enabled and truncate the unified sharing table to avoid the two systems getting out of sync.
+ // TODO: Sync shares might trigger event listeners that modify the target shares (e.g. auto accept), but we ignore them here. We need to detect this case and then sync back the changes to the source shares. Let's pray no infinite loop get's started by this.
+
+ if (
+ $event instanceof ShareAcceptedEvent
+ || $event instanceof ShareCreatedEvent
+ || $event instanceof ShareDeletedEvent
+ || $event instanceof ShareDeletedFromSelfEvent
+ || $event instanceof ShareMovedEvent
+ || $event instanceof ShareRestoredEvent
+ || $event instanceof ShareUpdatedEvent
+ ) {
+ $legacyShare = $event->getShare();
+ $legacyProvider = $legacyShare->getProviderId();
+ $legacyId = (int)$legacyShare->getId();
+ $legacyMapping = $this->legacyMapper->getLegacyMappingByLegacyProviderAndId($legacyProvider, $legacyId);
+
+ if ($event instanceof ShareDeletedEvent) {
+ // If the legacy share was not mapped to a share, we can ignore it.
+ if ($legacyMapping instanceof LegacyMapping) {
+ $this->wrapSync($legacyMapping->id, function () use ($legacyMapping): void {
+ try {
+ $this->backend->deleteShare($legacyMapping->id);
+ } catch (ShareNotFoundException) {
+ // If the owner is deleted, the share was already deleted by the user recipient type through the manager, but we still receive this event from the legacy sharing manager.
+ }
+
+ $this->legacyMapper->deleteLegacyMappings($legacyMapping->id);
+ });
+ }
+ } else {
+ $id = null;
+ if ($legacyMapping instanceof LegacyMapping) {
+ $id = $legacyMapping->id;
+ $this->wrapSync($id, function () use ($id): void {
+ /** @var non-empty-list $legacyMappings The list can't be empty, because $legacyMapping is not null */
+ $legacyMappings = $this->legacyMapper->getLegacyMappings($id);
+ $this->syncLegacySharesToShare($id, $legacyMappings);
+ });
+ } elseif (!$this->ignoreNewLegacyShare) {
+ $id = $this->snowflakeGenerator->nextId(DateTimeImmutable::createFromMutable($legacyShare->getShareTime()));
+ $this->wrapSync($id, function () use ($id, $legacyProvider, $legacyId, $legacyShare): void {
+ $secrets = [];
+ $value = null;
+ if ($this->isPublicLegacyShare($legacyShare) || $this->isRemoteLegacyShare($legacyShare)) {
+ $token = $legacyShare->getToken();
+ if ($token === '') {
+ throw new RuntimeException('Token must not be an empty string.');
+ }
+
+ $secrets = [$token];
+ }
+
+ if ($legacyShare->getShareType() === IShare::TYPE_LINK) {
+ $value = $this->getSharingManager()->generateSecret();
+ }
+
+ if ($legacyShare->getAttributes()?->getAttribute('fileRequest', 'enabled') === true) {
+ $emails = $legacyShare->getAttributes()?->getAttribute('shareWith', 'emails') ?? [];
+ if (!is_array($emails) || !array_is_list($emails) || array_any($emails, static fn (mixed $value): bool => !is_string($value) || $value === '')) {
+ throw new RuntimeException('The emails must be a list of non-empty strings.');
+ }
+
+ $token = $legacyShare->getToken();
+ if ($token === '') {
+ throw new RuntimeException('Token must not be an empty string.');
+ }
+
+ $secrets = [$token];
+ $emailCount = count($emails);
+ for ($i = 0; $i < $emailCount; ++$i) {
+ $secrets[] = $this->getSharingManager()->generateSecret();
+ }
+
+ $value = $this->getSharingManager()->generateSecret();
+ }
+
+ if ($secrets === []) {
+ $secrets = [$this->getSharingManager()->generateSecret()];
+ }
+
+ $value ??= $this->splitLegacySharedWith($legacyShare)['value'];
+
+ $legacyMapping = $this->legacyMapper->createLegacyMapping(
+ $id,
+ $legacyProvider,
+ $legacyId,
+ new DateTimeImmutable(),
+ $secrets,
+ $value,
+ );
+ $this->syncLegacySharesToShare($id, [$legacyMapping]);
+ });
+ }
+
+ if ($id !== null && $this->isValidationEnabled()) {
+ $this->wrapSync($id, function () use ($id): void {
+ $share = $this->getSharingManager()->getShare(new ShareAccessContext(overrideChecks: true), $id);
+ $this->syncShareToLegacyShares($share);
+ });
+
+ if (!$event instanceof ShareDeletedFromSelfEvent) {
+ $event->setShare($this->legacySharingManager->getShareById($legacyShare->getFullId(), onlyValid: false));
+ }
+ }
+ }
+
+ return;
+ }
+
+ if ($event instanceof SharesDeletedEvent) {
+ foreach ($event->shareIds as $id) {
+ $this->wrapSync($id, function () use ($id): void {
+ $legacyMappings = $this->legacyMapper->getLegacyMappings($id);
+ foreach ($legacyMappings as $legacyMapping) {
+ try {
+ $this->legacySharingManager->deleteShare($legacyMapping->getLegacyShare($this->legacySharingManager));
+ } catch (ShareNotFound) {
+ }
+ }
+
+ $this->legacyMapper->deleteLegacyMappings($id);
+ });
+ }
+
+ return;
+ }
+
+ /** @psalm-suppress RedundantConditionGivenDocblockType */
+ if ($event instanceof SharesUpdatedEvent) {
+ foreach ($event->shareIds as $id) {
+ $this->wrapSync($id, function () use ($id): void {
+ $share = $this->getSharingManager()->getShare(new ShareAccessContext(overrideChecks: true), $id);
+ // Only active shares are synced, everything else is dropped
+ if ($share->state === ShareState::Active) {
+ $this->syncShareToLegacyShares($share);
+ } else {
+ $legacyMappings = $this->legacyMapper->getLegacyMappings($id);
+ foreach ($legacyMappings as $legacyMapping) {
+ try {
+ $this->legacySharingManager->deleteShare($legacyMapping->getLegacyShare($this->legacySharingManager));
+ } catch (ShareNotFound) {
+ }
+ }
+
+ $this->legacyMapper->deleteLegacyMappings($id);
+ }
+ });
+
+ // This has no validation mode, because we don't have existing integration tests for it.
+ }
+
+ return;
+ }
+ }
+
+ /**
+ * @param (Closure(): void) $callback
+ */
+ private function wrapSync(string $id, Closure $callback): void {
+ if (isset($this->sharesInSync[$id])) {
+ return;
+ }
+
+ try {
+ $this->sharesInSync[$id] = true;
+ $this->dbConnection->beginTransaction();
+
+ $callback();
+
+ $this->dbConnection->commit();
+ unset($this->sharesInSync[$id]);
+ } catch (Exception $exception) {
+ $this->dbConnection->rollBack();
+ unset($this->sharesInSync[$id]);
+ throw $exception;
+ }
+ }
+
+ /**
+ * @param non-empty-string $id
+ * @param non-empty-list $legacyMappings
+ */
+ private function syncLegacySharesToShare(string $id, array $legacyMappings): void {
+ // TODO: Instead of using the first legacy share for common attributes, use the mostly recently updated one. Then also update the common stuff for the other legacy shares?
+
+ /** @var array */
+ $sources = [];
+ /** @var array, array> */
+ $recipients = [];
+ $legacyLastUpdated = null;
+
+ /** @var list $legacyShares */
+ $legacyShares = [];
+ foreach ($legacyMappings as $legacyMapping) {
+ if ($legacyLastUpdated === null || $legacyLastUpdated->diff($legacyMapping->lastUpdated)->invert === 0) {
+ $legacyLastUpdated = $legacyMapping->lastUpdated;
+ }
+
+ $legacyShare = $legacyMapping->getLegacyShare($this->legacySharingManager);
+ $legacyShares[] = $legacyShare;
+ }
+
+ /** @var array, SharePermission> */
+ $permissions = [];
+ foreach ($legacyShares as $legacyShare) {
+ // Only allow if both agree, to avoid mixups.
+ $allowDownload = !$legacyShare->getHideDownload() && $legacyShare->getAttributes()?->getAttribute('permissions', 'download') !== false;
+ // We want the sum of all enabled permissions.
+ foreach ($this->legacyPermissionsToPermissions($legacyShare->getPermissions(), $allowDownload) as $permission) {
+ if (!isset($permissions[$permission->class]) || !$permissions[$permission->class]->enabled) {
+ $permissions[$permission->class] = $permission;
+ }
+ }
+ }
+
+ foreach ($legacyMappings as $legacyMapping) {
+ // TODO: Don't refetch share
+ $legacyShare = $legacyMapping->getLegacyShare($this->legacySharingManager);
+
+ $nodeId = $legacyShare->getNodeId();
+ $sources[$nodeId] ??= new ShareSource(NodeShareSourceType::class, (string)$nodeId);
+
+ $shareInitiator = $this->splitLegacySharedByOrOwner($legacyShare->getSharedBy());
+ if ($shareInitiator['value'] === '') {
+ throw new RuntimeException('sharedBy value must not be an empty string.');
+ }
+
+ if ($shareInitiator['instance'] !== null) {
+ // This should never happen, because incoming legacy remote shares are handled separately.
+ throw new RuntimeException('Incoming remote shares are not supported.');
+ }
+
+ // Only allow if both agree, to avoid mixups.
+ $recipientAllowDownload = !$legacyShare->getHideDownload() && $legacyShare->getAttributes()?->getAttribute('permissions', 'download') !== false;
+ // We only want to set those permissions that are masked and not already denied by the share itself.
+ $recipientPermissions = array_filter($this->legacyPermissionsToPermissions($legacyShare->getPermissions(), $recipientAllowDownload), fn (SharePermission $permission): bool => !$permission->enabled && (!isset($permissions[$permission->class]) || $permissions[$permission->class]->enabled));
+
+ if ($legacyShare->getAttributes()?->getAttribute('fileRequest', 'enabled') === true) {
+ $emails = $legacyShare->getAttributes()?->getAttribute('shareWith', 'emails') ?? [];
+ if (!is_array($emails) || !array_is_list($emails) || array_any($emails, static fn (mixed $value): bool => !is_string($value) || $value === '')) {
+ throw new RuntimeException('The emails must be a list of non-empty strings.');
+ }
+
+ if (count($legacyMapping->recipientSecrets) !== count($emails) + 1) {
+ throw new RuntimeException('The secrets count does not match.');
+ }
+
+ if ($legacyMapping->recipientValue === '') {
+ throw new RuntimeException('Recipient value must not be an empty string.');
+ }
+
+ $recipients[TokenShareRecipientType::class] ??= [];
+ $recipients[TokenShareRecipientType::class][$this->getLegacySharedWithUnique($legacyShare)] ??= new ShareRecipient(
+ TokenShareRecipientType::class,
+ $legacyMapping->recipientValue,
+ null,
+ // TODO: Make sure mapping secret is updated if token changes (for all mappings of the same recipient).
+ $legacyMapping->recipientSecrets[0],
+ new ShareUser(
+ $shareInitiator['value'],
+ $shareInitiator['instance'],
+ ),
+ $recipientPermissions,
+ );
+
+ /** @var list $emails */
+ foreach ($emails as $index => $email) {
+ $recipients[EmailShareRecipientType::class] ??= [];
+ $recipients[EmailShareRecipientType::class][$email] ??= new ShareRecipient(
+ EmailShareRecipientType::class,
+ $email,
+ null,
+ // TODO: Make sure mapping secret is updated if token changes (for all mappings of the same recipient).
+ $legacyMapping->recipientSecrets[$index + 1],
+ new ShareUser(
+ $shareInitiator['value'],
+ $shareInitiator['instance'],
+ ),
+ $recipientPermissions,
+ );
+ }
+ } else {
+ $recipientTypeClass = $this->legacyShareTypeToRecipientTypeClass($legacyShare->getShareType(), $legacyShare->getAttributes());
+ $isTokenRecipient = $recipientTypeClass === TokenShareRecipientType::class;
+ $recipient = $isTokenRecipient ? ['value' => $legacyMapping->recipientValue, 'instance' => null] : $this->splitLegacySharedWith($legacyShare);
+ $recipientValue = $recipient['value'];
+ if ($recipientValue === '') {
+ throw new RuntimeException('Recipient value must not be an empty string.');
+ }
+
+ $recipients[$recipientTypeClass] ??= [];
+ $recipients[$recipientTypeClass][$this->getLegacySharedWithUnique($legacyShare)] ??= new ShareRecipient(
+ $recipientTypeClass,
+ $recipientValue,
+ $recipient['instance'],
+ // TODO: Make sure mapping secret is updated if token changes (for all mappings of the same recipient).
+ $legacyMapping->recipientSecrets[0],
+ new ShareUser(
+ $shareInitiator['value'],
+ $shareInitiator['instance'],
+ ),
+ $recipientPermissions,
+ );
+ }
+ }
+
+ if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getShareOwner())) {
+ throw new RuntimeException("All legacy shares sharing a share id don't have the same owner");
+ }
+
+ $shareOwner = $this->splitLegacySharedByOrOwner($legacyShares[0]->getShareOwner());
+ if ($shareOwner['value'] === '') {
+ throw new RuntimeException('shareOwner value must not be an empty string.');
+ }
+
+ if ($shareOwner['instance'] !== null) {
+ // This should never happen, because incoming legacy remote shares are handled separately.
+ throw new RuntimeException('Incoming remote shares are not supported.');
+ }
+
+ $owner = new ShareUser(
+ $shareOwner['value'],
+ $shareOwner['instance'],
+ );
+
+ if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getExpirationDate())) {
+ throw new RuntimeException("All legacy shares sharing a share id don't have the expiration date");
+ }
+
+ if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getPassword())) {
+ throw new RuntimeException("All legacy shares sharing a share id don't have the password");
+ }
+
+ if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getLabel())) {
+ throw new RuntimeException("All legacy shares sharing a share id don't have the label");
+ }
+
+ if (!$this->checkAllSame($legacyShares, fn (IShare $share) => $share->getNote())) {
+ throw new RuntimeException("All legacy shares sharing a share id don't have the note");
+ }
+
+ $password = $legacyShares[0]->getPassword();
+ if ($password !== null && !$legacyShare->isPasswordHashed()) {
+ throw new RuntimeException('The password must be hashed already.');
+ }
+
+ $properties = [
+ new ShareProperty(ExpirationDateSharePropertyType::class, $legacyShares[0]->getExpirationDate()?->format(DateTimeInterface::ATOM)),
+ new ShareProperty(PasswordSharePropertyType::class, $password),
+ new ShareProperty(LabelSharePropertyType::class, ($label = $legacyShares[0]->getLabel()) !== '' ? $label : null),
+ new ShareProperty(NoteSharePropertyType::class, ($note = $legacyShares[0]->getNote()) !== '' ? $note : null),
+ new ShareProperty(NodeGridViewSharePropertyType::class, $legacyShares[0]->getAttributes()?->getAttribute('config', 'grid_view') === true ? 'true' : 'false'),
+ new ShareProperty(NodeNicknameSharePropertyType::class, $legacyShares[0]->getAttributes()?->getAttribute('fileRequest', 'enabled') === true ? 'true' : 'false'),
+ ];
+
+ // To avoid diffing the shares, we just delete and create it.
+ try {
+ $this->backend->deleteShare($id);
+ } catch (ShareNotFoundException) {
+ }
+
+ $this->createShare($legacyShares, new Share(
+ $id,
+ $owner,
+ $legacyLastUpdated,
+ // Legacy shares are always active
+ ShareState::Active,
+ null,
+ array_values($sources),
+ array_values(array_merge(...array_values($recipients))),
+ array_combine(array_map(static fn (ShareProperty $property): string => $property->class, $properties), $properties),
+ array_combine(array_map(static fn (SharePermission $permission): string => $permission->class, $permissions), $permissions),
+ ));
+ }
+
+ /**
+ * @param list $legacyShares
+ */
+ private function createShare(array $legacyShares, Share $share): Share {
+ $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 ($recipient->permissions as $permission) {
+ $this->backend->updateShareRecipientPermission($share->id, $recipient, $permission);
+ }
+ }
+
+ foreach ($share->properties as $property) {
+ $this->backend->updateShareProperty($share->id, $property);
+ }
+
+ foreach ($share->permissions as $permission) {
+ $this->backend->updateSharePermission($share->id, $permission);
+ }
+
+ foreach ($legacyShares as $legacyShare) {
+ /** {@see IShare::TYPE_USERGROUP} shares are never handled directly here */
+ if ($legacyShare->getShareType() === IShare::TYPE_USER) {
+ $this->backend->updateShareUserStatus($share->id, $legacyShare->getSharedWith(), $this->legacyUserStatusToUserStatus($legacyShare->getStatus() ?? IShare::STATUS_PENDING));
+ $this->getSourceNodeTargetManager()->setTarget($legacyShare->getSharedWith(), $share->owner, $legacyShare->getNodeId(), $legacyShare->getTarget());
+ } elseif ($legacyShare->getShareType() === IShare::TYPE_GROUP) {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $result = $qb
+ ->select('share_with', 'accepted', 'file_target', 'permissions')
+ ->from('share')
+ ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP)))
+ ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter($legacyShare->getId(), IQueryBuilder::PARAM_INT)))
+ ->executeQuery();
+
+ /** @var list $rows */
+ $rows = $result->fetchAllAssociative();
+ foreach ($rows as $row) {
+ if ($row['permissions'] === 0) {
+ $this->backend->updateShareUserStatus($share->id, $row['share_with'], ShareUserStatus::Rejected);
+ } else {
+ $this->backend->updateShareUserStatus($share->id, $row['share_with'], $this->legacyUserStatusToUserStatus($row['accepted']));
+ }
+
+ $this->getSourceNodeTargetManager()->setTarget($row['share_with'], $share->owner, $legacyShare->getNodeId(), $row['file_target']);
+ }
+ }
+ }
+
+ [$share] = $this->backend->ensureDefaults([$share]);
+
+ return $share;
+ }
+
+ private function syncShareToLegacyShares(Share $share): void {
+ if ($share->owner->instance !== null) {
+ // We can't save them, because they are handled separately in the legacy sharing system.
+ return;
+ }
+
+ /** @var array>> $legacyMappings */
+ $legacyMappings = [];
+ /** @var array */
+ $oldLegacyShares = [];
+ foreach ($this->legacyMapper->getLegacyMappings($share->id) as $legacyMapping) {
+ $legacyShare = $legacyMapping->getLegacyShare($this->legacySharingManager);
+
+ $legacyShareType = $legacyShare->getShareType();
+ $legacySharedWithUnique = $this->getLegacySharedWithUnique($legacyShare);
+
+ $legacyMappings[$legacyShareType] ??= [];
+ $legacyMappings[$legacyShareType][$legacySharedWithUnique] ??= [];
+ $legacyMappings[$legacyShareType][$legacySharedWithUnique][$legacyShare->getNodeId()] = $legacyMapping;
+
+ $oldLegacyShares[$legacyShare->getFullId()] = $legacyShare;
+ }
+
+ if ($share->recipients === []) {
+ return;
+ }
+
+ /** @var array */
+ $newLegacyShares = [];
+ foreach ($share->sources as $source) {
+ if ($source->class !== NodeShareSourceType::class) {
+ continue;
+ }
+
+ if (($share->properties[NodeNicknameSharePropertyType::class] ?? null)?->value === 'true') {
+ $tokenRecipient = null;
+ $emailRecipients = [];
+ foreach ($share->recipients as $recipient) {
+ if ($recipient->class === TokenShareRecipientType::class) {
+ if ($tokenRecipient !== null) {
+ throw new RuntimeException('More than one token recipient not allowed.');
+ }
+
+ $tokenRecipient = $recipient;
+ continue;
+ }
+
+ if ($recipient->class === EmailShareRecipientType::class) {
+ $emailRecipients[] = $recipient;
+ continue;
+ }
+
+ throw new RuntimeException('Recipient type that is not a token or email: ' . $recipient->class);
+ }
+
+ if ($tokenRecipient === null) {
+ throw new RuntimeException('No token recipient.');
+ }
+
+ if ($tokenRecipient->initiator === null) {
+ throw new RuntimeException('Token recipient initiator cannot be null at this point.');
+ }
+
+ if ($tokenRecipient->secret === null) {
+ throw new RuntimeException('Token recipient secret cannot be null at this point.');
+ }
+
+ $attributes = new ShareAttributes();
+
+ // Create a new object to avoid copying existing attributes.
+ $legacyShare = $this->legacySharingManager->newShare();
+ $legacyShare->setShareType(IShare::TYPE_EMAIL);
+ $legacyShare->setSharedWith('');
+
+ $attributes->setAttribute('shareWith', 'emails', array_map(static fn (ShareRecipient $recipient): string => $recipient->value, $emailRecipients));
+
+ $legacyShare->setSharedBy($this->shareUserToLegacyIdentifier($tokenRecipient->initiator));
+
+ // Make sure the token recipient is always the first one, so we can correctly assign back the secrets later.
+ $orderedRecipients = array_merge([$tokenRecipient], $emailRecipients);
+ $orderedRecipientSecrets = array_map(function (ShareRecipient $recipient): string {
+ if ($recipient->secret === null) {
+ throw new RuntimeException('The secret must not be null.');
+ }
+
+ return $recipient->secret;
+ }, $orderedRecipients);
+
+ $legacyShare->setToken($this->isPublicLegacyShare($legacyShare) ? $tokenRecipient->secret : '');
+
+ $legacyShare->setStatus(IShare::STATUS_PENDING);
+
+ $legacyShare->setAttributes($attributes);
+
+ $this->setCommonLegacyShareFields($share, $source, $tokenRecipient, $legacyShare);
+
+ $legacyShare = $this->createOrUpdateLegacyShare($share, $legacyMappings, $legacyShare, $tokenRecipient->value, $orderedRecipientSecrets);
+
+ $newLegacyShares[$legacyShare->getFullId()] = $legacyShare;
+ } else {
+ foreach ($share->recipients as $recipient) {
+ /** {@see IShare::TYPE_USERGROUP} shares are handled automatically by the DefaultShareProvider. */
+ /** @psalm-suppress ArgumentTypeCoercion Psalm gets confused with the properties keys */
+ $legacyShareType = $this->recipientTypeClassToLegacyShareType($recipient->class, $recipient->instance, $share->properties);
+ if ($legacyShareType === null) {
+ continue;
+ }
+
+ // Create a new object to avoid copying existing attributes.
+ $legacyShare = $this->legacySharingManager->newShare();
+ $legacyShare->setShareType($legacyShareType);
+
+ /** @psalm-suppress ArgumentTypeCoercion Psalm gets confused with the properties keys */
+ if (($legacySharedBy = $this->shareRecipientToLegacyIdentifier($recipient, $share->properties)) !== null) {
+ $legacyShare->setSharedWith($legacySharedBy);
+ }
+
+ if ($recipient->initiator === null) {
+ throw new RuntimeException('Share recipient initiator cannot be null at this point.');
+ }
+
+ $legacyShare->setSharedBy($this->shareUserToLegacyIdentifier($recipient->initiator));
+
+ $recipientSecret = $recipient->secret;
+ if ($recipientSecret === null) {
+ throw new RuntimeException('secret must be set.');
+ }
+
+ $legacyShare->setToken($this->isPublicLegacyShare($legacyShare) || $this->isRemoteLegacyShare($legacyShare) ? $recipientSecret : '');
+
+ $legacyShare->setStatus(
+ $this->userStatusToLegacyUserStatus(
+ (
+ $recipient->class === UserShareRecipientType::class && $recipient->instance === null
+ ? ($this->backend->getUserStatuses($share->id, [$recipient->value])[$recipient->value] ?? null)
+ : null
+ ) ?? ShareUserStatus::Pending
+ )
+ );
+
+ if ($recipient->class === UserShareRecipientType::class && $recipient->instance === null) {
+ $target = $this->getSourceNodeTargetManager()->getTarget($recipient->value, $share->owner, (int)$source->value) ?? $this->getSourceNodeTargetManager()->createDefaultTarget($recipient->value, $share->owner, (int)$source->value);
+ $legacyShare->setTarget($target);
+ }
+
+ $this->setCommonLegacyShareFields($share, $source, $recipient, $legacyShare);
+
+ $legacyShare = $this->createOrUpdateLegacyShare($share, $legacyMappings, $legacyShare, $recipient->value, [$recipientSecret]);
+
+ foreach ($share->recipients as $recipient) {
+ // Child shares might automatically get accepted, so we need to update their user status.
+ // We don't need to update any legacy share objects, because the group child shares are handled completely outside of this sync (which is the reason why we're doing the following directly in the DB).
+ if ($recipient->class === GroupShareRecipientType::class) {
+ if (($recipientType = $this->sharingRegistry->getRecipientTypes()[$recipient->class] ?? null) === null) {
+ throw new RuntimeException('The recipient type is not registered: ' . $recipient->class);
+ }
+
+ $userIds = $recipientType->getUsers($recipient->value);
+
+ $userIdsByUserStatus = [];
+ foreach ($this->backend->getUserStatuses($share->id, $userIds) as $userId => $userStatus) {
+ $userIdsByUserStatus[$userStatus->value] ??= [];
+ $userIdsByUserStatus[$userStatus->value][] = $userId;
+ }
+
+ foreach ($userIdsByUserStatus as $userStatus => $userIds) {
+ foreach (array_chunk($userIds, 1000) as $chunk) {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb
+ ->update('share')
+ ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP, IQueryBuilder::PARAM_INT)))
+ ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter((int)$legacyShare->getId(), IQueryBuilder::PARAM_INT)))
+ ->andWhere($qb->expr()->in('share_with', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY)));
+ if ($userStatus === ShareUserStatus::Rejected->value) {
+ $qb
+ ->set('permissions', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT));
+ } else {
+ $qb
+ ->set('accepted', $qb->createNamedParameter($this->userStatusToLegacyUserStatus(ShareUserStatus::from($userStatus)), IQueryBuilder::PARAM_INT));
+ }
+
+ $qb->executeStatement();
+ }
+ }
+
+ $userIdsByTarget = [];
+ foreach ($this->getSourceNodeTargetManager()->getTargets($userIds, $share->owner, (int)$source->value) as $userId => $target) {
+ $userIdsByTarget[$target] ??= [];
+ $userIdsByTarget[$target][] = $userId;
+ }
+
+ foreach ($userIdsByTarget as $target => $userIds) {
+ foreach (array_chunk($userIds, 1000) as $chunk) {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb
+ ->update('share')
+ ->set('file_target', $qb->createNamedParameter($target))
+ ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP, IQueryBuilder::PARAM_INT)))
+ ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter((int)$legacyShare->getId(), IQueryBuilder::PARAM_INT)))
+ ->andWhere($qb->expr()->in('share_with', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY)))
+ ->executeStatement();
+ }
+ }
+ }
+ }
+
+ $newLegacyShares[$legacyShare->getFullId()] = $legacyShare;
+ }
+ }
+ }
+
+ foreach (array_diff(array_keys($oldLegacyShares), array_keys($newLegacyShares)) as $invalidLegacyShareId) {
+ $this->legacySharingManager->deleteShare($oldLegacyShares[$invalidLegacyShareId]);
+ }
+
+ foreach ($newLegacyShares as $legacyShare) {
+ $this->legacySharingManager->updateShare($legacyShare);
+ }
+ }
+
+ private function setCommonLegacyShareFields(Share $share, ShareSource $source, ShareRecipient $recipient, IShare $legacyShare): void {
+ $attributes = $legacyShare->getAttributes() ?? new ShareAttributes();
+
+ $legacyShare->setNodeId((int)$source->value);
+
+ $legacyShare->setShareTime(DateTime::createFromImmutable($share->getCreatedAt()));
+
+ [$permissions, $allowDownload] = $this->permissionsToLegacyPermissions($source, $share->getEffectiveEnabledPermissionsForRecipients([$recipient]));
+ $legacyShare->setPermissions($permissions);
+
+ // Always set both, to avoid mixups.
+ $attributes->setAttribute('permissions', 'download', $allowDownload);
+ $legacyShare->setHideDownload(!$allowDownload);
+
+ if (($share->properties[NodeNicknameSharePropertyType::class] ?? null)?->value === 'true') {
+ $attributes->setAttribute('fileRequest', 'enabled', true);
+ }
+
+ $legacyShare->setShareOwner($this->shareUserToLegacyIdentifier($share->owner));
+
+ $legacyShare->setNote(($share->properties[NoteSharePropertyType::class] ?? null)?->value ?? '');
+
+ $legacyShare->setLabel($this->isPublicLegacyShare($legacyShare) ? ($share->properties[LabelSharePropertyType::class] ?? null)?->value ?? '' : '');
+
+ if (($share->properties[NodeGridViewSharePropertyType::class] ?? null)?->value === 'true') {
+ $attributes->setAttribute('config', 'grid_view', true);
+ }
+
+ $expirationDate = ($share->properties[ExpirationDateSharePropertyType::class] ?? null)?->value;
+ $expirationDate = $expirationDate !== null ? DateTime::createFromFormat(DateTimeInterface::ATOM, $expirationDate) : null;
+ if ($expirationDate === false) {
+ throw new RuntimeException('Failed to parse expiration date.');
+ }
+
+ $legacyShare->setExpirationDate($expirationDate);
+ // We don't call setNoExpirationDate, because the value isn't actually saved
+
+ if (($passwordHash = ($share->properties[PasswordSharePropertyType::class] ?? null)?->value) !== null) {
+ $legacyShare->setPasswordHash($passwordHash);
+ }
+
+ $legacyShare->setAttributes($attributes);
+ }
+
+ /**
+ * @param array>> $legacyMappings
+ * @param non-empty-string $recipientValue
+ * @param non-empty-list $recipientSecrets
+ */
+ private function createOrUpdateLegacyShare(Share $share, array $legacyMappings, IShare $legacyShare, string $recipientValue, array $recipientSecrets): IShare {
+ if (($legacyMapping = $legacyMappings[$legacyShare->getShareType()][$this->getLegacySharedWithUnique($legacyShare)][$legacyShare->getNodeId()] ?? null) !== null) {
+ $oldLegacyShare = $legacyMapping->getLegacyShare($this->legacySharingManager);
+
+ $legacyShare->setId($oldLegacyShare->getId());
+ $legacyShare->setProviderId($oldLegacyShare->getProviderId());
+
+ $this->legacyMapper->updateLegacyMapping(new LegacyMapping(
+ $legacyMapping->id,
+ $legacyMapping->legacyProvider,
+ $legacyMapping->legacyId,
+ $share->lastUpdated,
+ $legacyMapping->recipientSecrets,
+ $legacyMapping->recipientValue,
+ ));
+ } else {
+ try {
+ // We need to create the legacy mapping ourselves to control the share id, so we disable the automapping of new legacy shares.
+ $this->ignoreNewLegacyShare = true;
+ // This is the only way we can get a provider and id assigned.
+ $legacyShare = $this->legacySharingManager->createShare($legacyShare);
+ $this->ignoreNewLegacyShare = false;
+ } catch (Exception $exception) {
+ $this->ignoreNewLegacyShare = false;
+ throw $exception;
+ }
+
+ $this->legacyMapper->createLegacyMapping(
+ $share->id,
+ $legacyShare->getProviderId(),
+ (int)$legacyShare->getId(),
+ $share->lastUpdated,
+ $recipientSecrets,
+ $recipientValue,
+ );
+ }
+
+ return $legacyShare;
+ }
+
+ /**
+ * @return IShare::STATUS_*
+ */
+ private function userStatusToLegacyUserStatus(ShareUserStatus $userStatus): int {
+ return match ($userStatus) {
+ ShareUserStatus::Accepted => IShare::STATUS_ACCEPTED,
+ ShareUserStatus::Pending => IShare::STATUS_PENDING,
+ ShareUserStatus::Rejected => IShare::STATUS_REJECTED,
+ };
+ }
+
+ /**
+ * @param IShare::STATUS_* $legacyUserStatus
+ */
+ private function legacyUserStatusToUserStatus(int $legacyUserStatus): ShareUserStatus {
+ return match ($legacyUserStatus) {
+ IShare::STATUS_ACCEPTED => ShareUserStatus::Accepted,
+ IShare::STATUS_PENDING => ShareUserStatus::Pending,
+ IShare::STATUS_REJECTED => ShareUserStatus::Rejected,
+ };
+ }
+
+ /**
+ * @param list> $permissions
+ * @return array{int-mask-of, bool}
+ */
+ private function permissionsToLegacyPermissions(ShareSource $source, array $permissions): array {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $result = $qb
+ ->select('mimetype')
+ ->from('filecache')
+ ->where($qb->expr()->eq('fileid', $qb->createNamedParameter((int)$source->value, IQueryBuilder::PARAM_INT)))
+ ->executeQuery();
+
+ /** @var int|false */
+ $mimeTypeId = $result->fetchOne();
+ if ($mimeTypeId === false) {
+ throw new RuntimeException('Share source does not exist: ' . $source->value);
+ }
+
+ $nodeIsFile = $this->mimeTypeLoader->getMimetypeById($mimeTypeId) !== 'httpd/unix-directory';
+
+ /** @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 array, SharePermission>
+ */
+ private function legacyPermissionsToPermissions(int $legacyPermissions, bool $allowDownload): array {
+ /** @var array, SharePermission> $permissions */
+ $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) {
+ $permissions[$permissionTypeClass] = new SharePermission($permissionTypeClass, ($legacyPermissions & $mask) === $mask);
+ }
+
+ $permissions[NodeDownloadSharePermissionType::class] = new SharePermission(NodeDownloadSharePermissionType::class, $allowDownload);
+
+ return $permissions;
+ }
+
+ /**
+ * @param array, ShareProperty> $shareProperties
+ */
+ private function shareRecipientToLegacyIdentifier(ShareRecipient $recipient, array $shareProperties): ?string {
+ if (($shareProperties[NodeNicknameSharePropertyType::class] ?? null)?->value === 'true') {
+ return '';
+ }
+
+ if ($recipient->instance !== null) {
+ $cloudId = $this->cloudIdManager->getCloudId($recipient->value, $recipient->instance);
+ return $cloudId->getUser() . '@' . $cloudId->getRemote();
+ }
+
+ if ($recipient->class === TokenShareRecipientType::class) {
+ return null;
+ }
+
+ return $recipient->value;
+ }
+
+ private function shareUserToLegacyIdentifier(ShareUser $user): string {
+ if ($user->instance !== null) {
+ $cloudId = $this->cloudIdManager->getCloudId($user->userId, $user->instance);
+ return $cloudId->getUser() . '@' . $cloudId->getRemote();
+ }
+
+ return $user->userId;
+ }
+
+ /**
+ * @param class-string $recipientTypeClass
+ * @param array, ShareProperty> $shareProperties
+ * @return ?IShare::TYPE_*
+ */
+ private function recipientTypeClassToLegacyShareType(string $recipientTypeClass, ?string $recipientInstance, array $shareProperties): ?int {
+ if (($shareProperties[NodeNicknameSharePropertyType::class] ?? null)?->value === 'true') {
+ return IShare::TYPE_EMAIL;
+ }
+
+ return match ($recipientTypeClass) {
+ UserShareRecipientType::class => $recipientInstance === null ? IShare::TYPE_USER : IShare::TYPE_REMOTE,
+ GroupShareRecipientType::class => $recipientInstance === null ? IShare::TYPE_GROUP : IShare::TYPE_REMOTE_GROUP,
+ TokenShareRecipientType::class => IShare::TYPE_LINK,
+ EmailShareRecipientType::class => IShare::TYPE_EMAIL,
+ TeamShareRecipientType::class => IShare::TYPE_CIRCLE,
+ // TODO talk, deck
+ default => null,
+ };
+ }
+
+ /**
+ * @param IShare::TYPE_* $legacyShareType
+ * @return class-string
+ */
+ private function legacyShareTypeToRecipientTypeClass(int $legacyShareType, ?IAttributes $attributes): string {
+ if ($attributes?->getAttribute('fileRequest', 'enabled') === true) {
+ return TokenShareRecipientType::class;
+ }
+
+ 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,
+ // TODO talk, deck
+ default => throw new RuntimeException('Unsupported legacy share type: ' . $legacyShareType),
+ };
+ }
+
+ /**
+ * Check that all items return the same result when used as argument to a function
+ *
+ * @template T
+ * @template U
+ * @param iterable $items
+ * @param callable(T):U $fn
+ */
+ private function checkAllSame(iterable $items, callable $fn): bool {
+ $first = true;
+ $commonValue = null;
+ foreach ($items as $item) {
+ $value = $fn($item);
+ if ($first) {
+ $commonValue = $value;
+ $first = false;
+ } elseif ($value !== $commonValue) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @return array{value: string, instance: non-empty-string|null}
+ */
+ private function splitLegacySharedWith(IShare $legacyShare): array {
+ if ($legacyShare->getShareType() === IShare::TYPE_REMOTE || $legacyShare->getShareType() === IShare::TYPE_REMOTE_GROUP) {
+ $cloudId = $this->cloudIdManager->resolveCloudId($legacyShare->getSharedWith());
+
+ $user = $cloudId->getUser();
+ if ($user === '') {
+ throw new RuntimeException('User must not be an empty string.');
+ }
+
+ $remote = $cloudId->getRemote();
+ if ($remote === '') {
+ throw new RuntimeException('Remote must not be an empty string.');
+ }
+
+ return [
+ 'value' => $user,
+ 'instance' => $remote,
+ ];
+ }
+
+ return [
+ // This can be an empty string, in case it is a file request
+ 'value' => $legacyShare->getSharedWith(),
+ 'instance' => null,
+ ];
+ }
+
+ /**
+ * @return array{value: string, instance: non-empty-string|null}
+ */
+ private function splitLegacySharedByOrOwner(string $sharedByOrOwner): array {
+ // The type of the legacy share is only for the sharedWith, but we also can't have an email here, so we just have to check if it is a Cloud ID.
+ if ($this->cloudIdManager->isValidCloudId($sharedByOrOwner)) {
+ $cloudId = $this->cloudIdManager->resolveCloudId($sharedByOrOwner);
+
+ $user = $cloudId->getUser();
+ if ($user === '') {
+ throw new RuntimeException('User must not be an empty string.');
+ }
+
+ $remote = $cloudId->getRemote();
+ if ($remote === '') {
+ throw new RuntimeException('Remote must not be an empty string.');
+ }
+
+ return [
+ 'value' => $user,
+ 'instance' => $remote,
+ ];
+ }
+
+ return [
+ 'value' => $sharedByOrOwner,
+ 'instance' => null,
+ ];
+ }
+
+ private function getLegacySharedWithUnique(IShare $legacyShare): string {
+ return $this->isPublicLegacyShare($legacyShare) ? $legacyShare->getToken() : $legacyShare->getSharedWith();
+ }
+}
diff --git a/lib/private/Sharing/SharingManager.php b/lib/private/Sharing/SharingManager.php
index a6d36ca28e782..dabda62970a66 100644
--- a/lib/private/Sharing/SharingManager.php
+++ b/lib/private/Sharing/SharingManager.php
@@ -11,6 +11,8 @@
use Exception;
use NCU\Sharing\Event\SharesDefaultSetEvent;
+use NCU\Sharing\Event\SharesDeletedEvent;
+use NCU\Sharing\Event\SharesUpdatedEvent;
use NCU\Sharing\Exception\ShareInvalidException;
use NCU\Sharing\Exception\ShareOperationForbiddenException;
use NCU\Sharing\ISharingBackend;
@@ -18,7 +20,6 @@
use NCU\Sharing\ISharingRegistry;
use NCU\Sharing\Permission\ISharePermissionType;
use NCU\Sharing\Permission\SharePermission;
-use NCU\Sharing\Property\ISharePropertyTypeModifyValue;
use NCU\Sharing\Property\ShareProperty;
use NCU\Sharing\Recipient\IShareRecipientType;
use NCU\Sharing\Recipient\IShareRecipientTypePublicSecret;
@@ -51,9 +52,7 @@
use Random\Randomizer;
use RuntimeException;
-// TODO: Test sharing to federated users, groups and circles
// TODO: Implement share transfers
-// TODO: Cache share owner
/**
* @psalm-import-type SharingShare from Share
@@ -75,6 +74,7 @@ public function __construct(
private ClockInterface $clock,
private IManager $legacySharingManager,
private IConfig $config,
+ private SharingLegacySync $legacySync,
) {
$this->randomizer = new Randomizer();
$this->l10n = $l10nFactory->get('sharing');
@@ -192,7 +192,10 @@ public function onOwnerDeleted(ShareAccessContext $accessContext, ShareUser $own
// No need to update the last updated timestamp, because the share will be deleted anyway.
- $this->backend->onOwnerDeleted($owner);
+ $ids = $this->backend->onOwnerDeleted($owner);
+ if ($ids !== []) {
+ $this->eventDispatcher->dispatchTyped(new SharesDeletedEvent($ids));
+ }
}
#[\Override]
@@ -643,9 +646,6 @@ public function updateShareProperty(ShareAccessContext $accessContext, Share $sh
$this->backend->setLastUpdated([$share->id], $time);
$value = $this->backend->updateShareProperty($share->id, $property);
- if ($propertyType instanceof ISharePropertyTypeModifyValue) {
- $value = $propertyType->modifyValueOnLoad($value);
- }
$property = new ShareProperty(
$property->class,
@@ -731,11 +731,8 @@ public function updateShareRecipientPermission(ShareAccessContext $accessContext
$this->validateReshareOperation($accessContext, $share, $recipient);
}
- if ($permission->enabled) {
- $enabledPermissions = $share->getEffectiveEnabledPermissions($accessContext);
- if (!isset($enabledPermissions[$permission->class])) {
- throw new ShareOperationForbiddenException();
- }
+ if ($permission->enabled && !in_array($permission->class, $share->getEffectiveEnabledPermissions($accessContext), true)) {
+ throw new ShareOperationForbiddenException();
}
$this->backend->updateShareRecipientPermission($share->id, $recipient, $permission);
@@ -839,6 +836,8 @@ public function deleteShare(ShareAccessContext $accessContext, Share $share): vo
'shareId' => $share->id,
'owner' => $share->owner->userId,
], 'unified_sharing.share.deleted');
+
+ $this->eventDispatcher->dispatchTyped(new SharesDeletedEvent([$share->id]));
}
/**
@@ -863,6 +862,10 @@ public function getShares(
?string $lastShareID,
?int $limit,
): array {
+ if (($currentUser = $accessContext->currentUser) instanceof IUser) {
+ $this->legacySync->mapLegacyShares($currentUser);
+ }
+
return $this->backend->getShares($accessContext, $filterSourceTypeClass, $filterSourceTypeValue, $filterState, $filterUserStatus, $lastShareID, $limit);
}
@@ -873,8 +876,11 @@ public function handle(Event $event): void {
$keys = array_keys($shares);
$shares = $this->processShareUpdates(array_values($shares));
$event->setShares(array_combine($keys, $shares));
+
+ return;
}
+ /** @psalm-suppress RedundantConditionGivenDocblockType */
if ($event instanceof BeforeUserDeletedEvent) {
$shareUser = new ShareUser($event->getUser()->getUID(), null);
@@ -887,6 +893,8 @@ public function handle(Event $event): void {
$this->dbConnection->rollBack();
throw $exception;
}
+
+ return;
}
}
@@ -927,7 +935,7 @@ private function validateShareEditPermissions(ShareAccessContext $accessContext,
* @throws ShareOperationForbiddenException
*/
private function validatePermission(ShareAccessContext $accessContext, Share $share, string $permissionTypeClass): void {
- if (!isset($share->getEffectiveEnabledPermissions($accessContext)[$permissionTypeClass])) {
+ if (!in_array($permissionTypeClass, $share->getEffectiveEnabledPermissions($accessContext), true)) {
throw new ShareOperationForbiddenException();
}
}
@@ -958,7 +966,8 @@ private function validateReshareOperation(ShareAccessContext $accessContext, Sha
*/
private function validateInteraction(ShareAccessContext $accessContext, Share $share): void {
$action = new ShareAction(
- null, array_values(array_map(static fn (SharePermission $permission): string => $permission->class, $share->getEffectiveEnabledPermissions($accessContext)))
+ null,
+ $share->getEffectiveEnabledPermissions($accessContext),
);
if ($share->owner->instance !== null) {
@@ -1072,6 +1081,8 @@ private function processShareUpdates(array $shares): array {
}
}
+ $this->eventDispatcher->dispatchTyped(new SharesUpdatedEvent(array_map(static fn (Share $share): string => $share->id, $shares)));
+
return $shares;
}
diff --git a/lib/private/Teams/TeamManager.php b/lib/private/Teams/TeamManager.php
index d17aefe36223b..c2992350e6760 100644
--- a/lib/private/Teams/TeamManager.php
+++ b/lib/private/Teams/TeamManager.php
@@ -134,14 +134,18 @@ public function getTeamsForResource(string $providerId, string $resourceId, stri
return array_map($this->circleToTeam(...), $this->getTeams($provider->getTeamsForResource($resourceId), $userId));
}
- private function getTeamInternal(string $teamId, string $userId, ?CircleProbe $probe = null): ?Circle {
+ private function getTeamInternal(string $teamId, ?string $userId, ?CircleProbe $probe = null): ?Circle {
if (!$this->hasTeamSupport()) {
return null;
}
+ if ($userId !== null) {
+ $this->circlesManager->startSession($this->circlesManager->getLocalFederatedUser($userId));
+ } else {
+ $this->circlesManager->startSuperSession();
+ }
+
try {
- $federatedUser = $this->circlesManager->getFederatedUser($userId, Member::TYPE_USER);
- $this->circlesManager->startSession($federatedUser);
return $this->circlesManager->getCircle($teamId, $probe);
} catch (CircleNotFoundException) {
return null;
@@ -154,7 +158,7 @@ private function getTeamInternal(string $teamId, string $userId, ?CircleProbe $p
* @return array userId => displayName
*/
#[\Override]
- public function getMembersOfTeam(string $teamId, string $userId): array {
+ public function getMembersOfTeam(string $teamId, ?string $userId): array {
$team = $this->getTeamInternal($teamId, $userId);
if ($team === null) {
return [];
diff --git a/lib/public/Share/Events/ShareAcceptedEvent.php b/lib/public/Share/Events/ShareAcceptedEvent.php
index aa5f5beb2cc9a..255dc6a19503d 100644
--- a/lib/public/Share/Events/ShareAcceptedEvent.php
+++ b/lib/public/Share/Events/ShareAcceptedEvent.php
@@ -31,4 +31,11 @@ public function __construct(
public function getShare(): IShare {
return $this->share;
}
+
+ /**
+ * @since 35.0.0
+ */
+ public function setShare(IShare $share): void {
+ $this->share = $share;
+ }
}
diff --git a/lib/public/Share/Events/ShareCreatedEvent.php b/lib/public/Share/Events/ShareCreatedEvent.php
index 3681f68e0a0e7..512c339aa87bb 100644
--- a/lib/public/Share/Events/ShareCreatedEvent.php
+++ b/lib/public/Share/Events/ShareCreatedEvent.php
@@ -16,16 +16,13 @@
* @since 18.0.0
*/
class ShareCreatedEvent extends Event {
- /** @var IShare */
- private $share;
-
/**
* @since 18.0.0
*/
- public function __construct(IShare $share) {
+ public function __construct(
+ private IShare $share,
+ ) {
parent::__construct();
-
- $this->share = $share;
}
/**
@@ -34,4 +31,11 @@ public function __construct(IShare $share) {
public function getShare(): IShare {
return $this->share;
}
+
+ /**
+ * @since 35.0.0
+ */
+ public function setShare(IShare $share): void {
+ $this->share = $share;
+ }
}
diff --git a/lib/public/Share/Events/ShareDeletedEvent.php b/lib/public/Share/Events/ShareDeletedEvent.php
index 295e483cb84f5..a83e6c061aef2 100644
--- a/lib/public/Share/Events/ShareDeletedEvent.php
+++ b/lib/public/Share/Events/ShareDeletedEvent.php
@@ -16,24 +16,16 @@
* @since 21.0.0
*/
class ShareDeletedEvent extends Event {
- /** @var IShare */
- private $share;
-
/**
- *
- * @param IShare $share
- * @param IShare[] $children
- *
* @since 21.0.0
*/
- public function __construct(IShare $share) {
+ public function __construct(
+ private IShare $share,
+ ) {
parent::__construct();
-
- $this->share = $share;
}
/**
- * @return IShare
* @since 21.0.0
*/
public function getShare(): IShare {
diff --git a/lib/public/Share/Events/ShareMovedEvent.php b/lib/public/Share/Events/ShareMovedEvent.php
index a29bc5a9f41d1..5195acc5b4c71 100644
--- a/lib/public/Share/Events/ShareMovedEvent.php
+++ b/lib/public/Share/Events/ShareMovedEvent.php
@@ -21,7 +21,7 @@ class ShareMovedEvent extends Event {
* @since 33.0.0
*/
public function __construct(
- private readonly IShare $share,
+ private IShare $share,
private readonly IUser $user,
) {
parent::__construct();
@@ -40,4 +40,11 @@ public function getShare(): IShare {
public function getUser(): IUser {
return $this->user;
}
+
+ /**
+ * @since 35.0.0
+ */
+ public function setShare(IShare $share): void {
+ $this->share = $share;
+ }
}
diff --git a/lib/public/Share/Events/ShareRestoredEvent.php b/lib/public/Share/Events/ShareRestoredEvent.php
new file mode 100644
index 0000000000000..2452a79202688
--- /dev/null
+++ b/lib/public/Share/Events/ShareRestoredEvent.php
@@ -0,0 +1,44 @@
+share;
+ }
+
+ /**
+ * @since 35.0.0
+ */
+ public function setShare(IShare $share): void {
+ $this->share = $share;
+ }
+}
diff --git a/lib/public/Share/Events/ShareUpdatedEvent.php b/lib/public/Share/Events/ShareUpdatedEvent.php
new file mode 100644
index 0000000000000..163b69da5be82
--- /dev/null
+++ b/lib/public/Share/Events/ShareUpdatedEvent.php
@@ -0,0 +1,44 @@
+share;
+ }
+
+ /**
+ * @since 35.0.0
+ */
+ public function setShare(IShare $share): void {
+ $this->share = $share;
+ }
+}
diff --git a/lib/public/Share/IShare.php b/lib/public/Share/IShare.php
index 683b67d5dcdb6..d1e1954f7f057 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/public/Teams/ITeamManager.php b/lib/public/Teams/ITeamManager.php
index f6da931f85511..09760a5b1ea34 100644
--- a/lib/public/Teams/ITeamManager.php
+++ b/lib/public/Teams/ITeamManager.php
@@ -70,12 +70,12 @@ public function getTeamsForUser(string $userId): array;
* Returns a mapping of user ID to display name for all members of a given team.
*
* @param string $teamId ID of the team whose members are being queried
- * @param string $userId ID of the user from whose point of view the members are being queried
+ * @param ?string $userId ID of the user from whose point of view the members are being queried
*
* @return array userId => displayName
* @since 34.0.0
*/
- public function getMembersOfTeam(string $teamId, string $userId): array;
+ public function getMembersOfTeam(string $teamId, ?string $userId): array;
/**
* Returns whether the Teams backend is available
diff --git a/lib/unstable/Sharing/Event/SharesDeletedEvent.php b/lib/unstable/Sharing/Event/SharesDeletedEvent.php
new file mode 100644
index 0000000000000..ea70e39db27ca
--- /dev/null
+++ b/lib/unstable/Sharing/Event/SharesDeletedEvent.php
@@ -0,0 +1,31 @@
+ $shareIds
+ * @experimental 35.0.0
+ */
+ public function __construct(
+ public readonly array $shareIds,
+ ) {
+ parent::__construct();
+ }
+}
diff --git a/lib/unstable/Sharing/Event/SharesUpdatedEvent.php b/lib/unstable/Sharing/Event/SharesUpdatedEvent.php
new file mode 100644
index 0000000000000..4bb712d6bb764
--- /dev/null
+++ b/lib/unstable/Sharing/Event/SharesUpdatedEvent.php
@@ -0,0 +1,31 @@
+ $shareIds
+ * @experimental 35.0.0
+ */
+ public function __construct(
+ public readonly array $shareIds,
+ ) {
+ parent::__construct();
+ }
+}
diff --git a/lib/unstable/Sharing/ISharingBackend.php b/lib/unstable/Sharing/ISharingBackend.php
index 410c591a123e8..e47a8fac12513 100644
--- a/lib/unstable/Sharing/ISharingBackend.php
+++ b/lib/unstable/Sharing/ISharingBackend.php
@@ -232,4 +232,11 @@ public function getRecommendedRecipients(
int $count = 5,
int $offset = 0,
): array;
+
+ /**
+ * @param list $userIds
+ * @return array
+ * @experimental 35.0.0
+ */
+ public function getUserStatuses(string $id, array $userIds): array;
}
diff --git a/lib/unstable/Sharing/Property/APasswordSharePropertyType.php b/lib/unstable/Sharing/Property/APasswordSharePropertyType.php
index 4ff5ba8a62eb3..58ecb1605a90b 100644
--- a/lib/unstable/Sharing/Property/APasswordSharePropertyType.php
+++ b/lib/unstable/Sharing/Property/APasswordSharePropertyType.php
@@ -73,6 +73,13 @@ public function modifyValueOnSave(?string $oldValue, ?string $newValue): ?string
return $oldValue;
}
+ // A password that looks like a password hash would pass here, but that is hard to create by accident.
+ // Intentionally storing a "plain" password in the DB can't be abused by attackers.
+ // TODO: Remove this, once the legacy sync is removed.
+ if ($this->getHasher()->validate($newValue)) {
+ return $newValue;
+ }
+
return $this->getHasher()->hash($newValue);
}
@@ -80,7 +87,7 @@ public function modifyValueOnSave(?string $oldValue, ?string $newValue): ?string
* @experimental 35.0.0
*/
#[\Override]
- public function modifyValueOnLoad(?string $value): ?string {
+ public function modifyValueOnFormat(?string $value): ?string {
if ($value === null) {
return null;
}
diff --git a/lib/unstable/Sharing/Property/ISharePropertyTypeModifyValue.php b/lib/unstable/Sharing/Property/ISharePropertyTypeModifyValue.php
index 268d018036ee4..a10c6788b6516 100644
--- a/lib/unstable/Sharing/Property/ISharePropertyTypeModifyValue.php
+++ b/lib/unstable/Sharing/Property/ISharePropertyTypeModifyValue.php
@@ -26,9 +26,9 @@ interface ISharePropertyTypeModifyValue extends ISharePropertyType {
public function modifyValueOnSave(?string $oldValue, ?string $newValue): ?string;
/**
- * Modify the value whenever a share is fetched from the database.
+ * Modify the value whenever a share is formatted for the output.
*
* @experimental 35.0.0
*/
- public function modifyValueOnLoad(?string $value): ?string;
+ public function modifyValueOnFormat(?string $value): ?string;
}
diff --git a/lib/unstable/Sharing/Property/ShareProperty.php b/lib/unstable/Sharing/Property/ShareProperty.php
index 2894424b97f02..b064f60a9ca4b 100644
--- a/lib/unstable/Sharing/Property/ShareProperty.php
+++ b/lib/unstable/Sharing/Property/ShareProperty.php
@@ -44,6 +44,11 @@ public function format(ISharingRegistry $registry, IFactory $l10nFactory, Share
throw new RuntimeException('The property type is not registered: ' . $this->class);
}
+ $value = $this->value;
+ if ($propertyType instanceof ISharePropertyTypeModifyValue) {
+ $value = $propertyType->modifyValueOnFormat($value);
+ }
+
return $propertyType->format($share, [
'class' => $this->class,
'display_name' => $propertyType->getDisplayName($l10nFactory),
@@ -51,7 +56,7 @@ public function format(ISharingRegistry $registry, IFactory $l10nFactory, Share
'priority' => $propertyType->getPriority(),
'advanced' => $propertyType->isAdvanced(),
'required' => $propertyType->isRequired($share),
- 'value' => $this->value,
+ 'value' => $value,
]);
}
diff --git a/lib/unstable/Sharing/Recipient/IShareRecipientType.php b/lib/unstable/Sharing/Recipient/IShareRecipientType.php
index fc3d2b6a4ed19..40ab615dcca81 100644
--- a/lib/unstable/Sharing/Recipient/IShareRecipientType.php
+++ b/lib/unstable/Sharing/Recipient/IShareRecipientType.php
@@ -47,6 +47,14 @@ public function validateRecipient(string $recipient): bool;
*/
public function getRecipients(?IUser $currentUser, mixed $arguments): array;
+ /**
+ * Get user ids for a recipient value.
+ *
+ * @return list
+ * @experimental 35.0.0
+ */
+ public function getUsers(string $recipient): array;
+
/**
* @param non-empty-string $recipient
* @return ?non-empty-string
diff --git a/lib/unstable/Sharing/Share.php b/lib/unstable/Sharing/Share.php
index 55a11f6e15710..9643ff5637347 100644
--- a/lib/unstable/Sharing/Share.php
+++ b/lib/unstable/Sharing/Share.php
@@ -9,6 +9,7 @@
namespace NCU\Sharing;
+use DateTimeImmutable;
use NCU\Sharing\Permission\ISharePermissionPreset;
use NCU\Sharing\Permission\ISharePermissionType;
use NCU\Sharing\Permission\SharePermission;
@@ -24,6 +25,7 @@
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Server;
+use OCP\Snowflake\ISnowflakeDecoder;
/**
* Keep the following types in sync with apps/sharing/lib/ResponseDefinitions.php:
@@ -150,10 +152,12 @@
*/
#[Consumable(since: '35.0.0')]
final class Share {
+ private ?DateTimeImmutable $createdAt = null;
+
/** @var array> $recipientsCache */
private array $recipientsCache = [];
- /** @var array, SharePermission>> $enabledPermissionsCache */
+ /** @var array>> $enabledPermissionsCache */
private array $enabledPermissionsCache = [];
/**
@@ -177,6 +181,13 @@ public function __construct(
) {
}
+ /**
+ * @experimental 35.0.0
+ */
+ public function getCreatedAt(): DateTimeImmutable {
+ return $this->createdAt ??= Server::get(ISnowflakeDecoder::class)->decode($this->id)->getCreatedAt();
+ }
+
/**
* @return list
* @experimental 35.0.0
@@ -214,7 +225,7 @@ public function getEffectiveRecipients(ShareAccessContext $accessContext): array
}
/**
- * @return array, SharePermission>
+ * @return list>
* @experimental 35.0.0
*/
public function getEffectiveEnabledPermissions(ShareAccessContext $accessContext): array {
@@ -223,29 +234,42 @@ public function getEffectiveEnabledPermissions(ShareAccessContext $accessContext
return $enabledPermissions;
}
+ if ($accessContext->overrideChecks || $this->owner->isCurrentUser($accessContext)) {
+ $enabledPermissions = array_keys(array_filter($this->permissions, static fn (SharePermission $permission): bool => $permission->enabled));
+ } else {
+ $enabledPermissions = $this->getEffectiveEnabledPermissionsForRecipients($this->getEffectiveRecipients($accessContext));
+ }
+
+ return $this->enabledPermissionsCache[$hash] = $enabledPermissions;
+ }
+
+ /**
+ * @param list $recipients
+ * @return list>
+ * @experimental 35.0.0
+ */
+ public function getEffectiveEnabledPermissionsForRecipients(array $recipients): array {
$enabledPermissions = array_filter($this->permissions, static fn (SharePermission $permission): bool => $permission->enabled);
- if (!$accessContext->overrideChecks && !$this->owner->isCurrentUser($accessContext)) {
- /** @var array, SharePermission> $recipientsEnabledPermissions */
- $recipientsEnabledPermissions = [];
- foreach ($this->getEffectiveRecipients($accessContext) as $recipient) {
- foreach ($recipient->permissions as $permission) {
- // If any recipient has the permission enabled, we grant it.
- if (!isset($recipientsEnabledPermissions[$permission->class]) || ($permission->enabled && !$recipientsEnabledPermissions[$permission->class]->enabled)) {
- $recipientsEnabledPermissions[$permission->class] = $permission;
- }
+ /** @var array, SharePermission> $recipientsEnabledPermissions */
+ $recipientsEnabledPermissions = [];
+ foreach ($recipients as $recipient) {
+ foreach ($recipient->permissions as $permission) {
+ // If any recipient has the permission enabled, we grant it.
+ if (!isset($recipientsEnabledPermissions[$permission->class]) || ($permission->enabled && !$recipientsEnabledPermissions[$permission->class]->enabled)) {
+ $recipientsEnabledPermissions[$permission->class] = $permission;
}
}
+ }
- foreach ($recipientsEnabledPermissions as $permission) {
- // If the permission was disabled by any recipient and not enabled by another, we deny it.
- if (!$permission->enabled) {
- unset($enabledPermissions[$permission->class]);
- }
+ foreach ($recipientsEnabledPermissions as $permission) {
+ // If the permission was disabled by any recipient and not enabled by another, we deny it.
+ if (!$permission->enabled) {
+ unset($enabledPermissions[$permission->class]);
}
}
- return $this->enabledPermissionsCache[$hash] = $enabledPermissions;
+ return array_keys($enabledPermissions);
}
/**
@@ -273,7 +297,7 @@ public function format(ISharingRegistry $registry, IFactory $l10nFactory, IURLGe
$selectedPermissionPresetClass = null;
- $enabledPermissionTypeClasses = array_values(array_map(static fn (SharePermission $permission): string => $permission->class, $this->getEffectiveEnabledPermissions($accessContext)));
+ $enabledPermissionTypeClasses = $this->getEffectiveEnabledPermissions($accessContext);
sort($enabledPermissionTypeClasses);
$requiredPermissionTypeClasses = [];
diff --git a/psalm-strict.xml b/psalm-strict.xml
index 383d80f2d8b49..de512a693b873 100644
--- a/psalm-strict.xml
+++ b/psalm-strict.xml
@@ -69,6 +69,9 @@
+
+
+
@@ -82,6 +85,7 @@
+
diff --git a/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php b/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php
index 7e932a79e61de..b9dcb3e4d2243 100644
--- a/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php
+++ b/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php
@@ -104,8 +104,12 @@ public function testValidateRecipient(): void {
$this->assertFalse($this->recipientType->validateRecipient('invalid'));
}
- public function testGetRecipientValues(): void {
- $this->assertEquals(['group1'], $this->recipientType->getRecipients($this->user1, null));
+ public function testGetRecipients(): void {
+ $this->assertEquals([$this->group1->getGID()], $this->recipientType->getRecipients($this->user1, null));
+ }
+
+ public function testGetUsers(): void {
+ $this->assertEquals([$this->user1->getUID()], $this->recipientType->getUsers($this->group1->getGID()));
}
public function testGetRecipientDisplayName(): void {
diff --git a/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php b/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php
index 61ea50089b892..dab77698373e8 100644
--- a/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php
+++ b/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php
@@ -125,10 +125,16 @@ public function testValidateRecipient(): void {
$this->assertFalse($this->recipientType->validateRecipient('invalid'));
}
- public function testGetRecipientValues(): void {
+ public function testGetRecipients(): void {
$this->assertContains($this->team1->getId(), $this->recipientType->getRecipients($this->user1, null));
}
+ /** @psalm-suppress UnevaluatedCode Test is skipped */
+ public function testGetUsers(): void {
+ $this->markTestSkipped('This test is broken if run together with other test suites because circles 🤷♀️');
+ $this->assertEquals([$this->user1->getUID()], $this->recipientType->getUsers($this->team1->getId()));
+ }
+
/** @psalm-suppress UnevaluatedCode Test is skipped */
public function testGetRecipientDisplayName(): void {
$this->markTestSkipped('This test is broken if run together with other test suites because circles 🤷♀️');
diff --git a/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php b/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php
index 20978861bb5d6..f7e4e2aa7f3e9 100644
--- a/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php
+++ b/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php
@@ -95,8 +95,12 @@ public function testValidateRecipient(): void {
$this->assertFalse($this->recipientType->validateRecipient('invalid'));
}
- public function testGetRecipientValues(): void {
- $this->assertEquals(['user1'], $this->recipientType->getRecipients($this->user1, null));
+ public function testGetRecipients(): void {
+ $this->assertEquals([$this->user1->getUID()], $this->recipientType->getRecipients($this->user1, null));
+ }
+
+ public function testGetUsers(): void {
+ $this->assertEquals([$this->user1->getUID()], $this->recipientType->getUsers($this->user1->getUID()));
}
public function testGetRecipientDisplayName(): void {
diff --git a/tests/lib/Sharing/AbstractSharingManagerTests.php b/tests/lib/Sharing/AbstractSharingManagerTests.php
index df4ec3fe5cd35..799b4842510ab 100644
--- a/tests/lib/Sharing/AbstractSharingManagerTests.php
+++ b/tests/lib/Sharing/AbstractSharingManagerTests.php
@@ -1748,7 +1748,7 @@ public function testUpdateSharePropertyRequired(): void {
public function testUpdateSharePropertyModifyProperties(): void {
$this->registry->registerPropertyType(
- new TestSharePropertyTypeModifyValue(['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load'])
+ new TestSharePropertyTypeModifyValue(['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-format'])
);
$this->registry->markPropertyTypeCompatibleWithSourceType(TestSharePropertyTypeModifyValue::class, TestShareSourceType1::class);
$this->registry->markPropertyTypeCompatibleWithRecipientType(TestSharePropertyTypeModifyValue::class, TestShareRecipientType1::class);
@@ -1786,7 +1786,7 @@ public function testUpdateSharePropertyModifyProperties(): void {
'required' => false,
'value' => 'modified-on-save',
'type' => 'enum',
- 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load'],
+ 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-format'],
],
], $formatted['properties']);
@@ -1820,7 +1820,7 @@ public function testUpdateSharePropertyModifyProperties(): void {
'required' => false,
'value' => 'old-value',
'type' => 'enum',
- 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load'],
+ 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-format'],
],
], $formatted['properties']);
@@ -1850,12 +1850,12 @@ public function testUpdateSharePropertyModifyProperties(): void {
'required' => false,
'value' => 'modified-on-save',
'type' => 'enum',
- 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load'],
+ 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-format'],
],
], $formatted['properties']);
$before = $this->manager->getTime();
- $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-load'));
+ $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-format'));
$after = $this->manager->getTime();
$this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated']));
$this->assertEquals([
@@ -1877,9 +1877,9 @@ public function testUpdateSharePropertyModifyProperties(): void {
'priority' => 1,
'advanced' => false,
'required' => false,
- 'value' => 'modified-on-load',
+ 'value' => 'modified-on-format',
'type' => 'enum',
- 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load'],
+ 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-format'],
],
], $formatted['properties']);
}
diff --git a/tests/lib/Sharing/Property/APasswordSharePropertyTypeTest.php b/tests/lib/Sharing/Property/APasswordSharePropertyTypeTest.php
index 1d104c72ce32b..019dab10ce313 100644
--- a/tests/lib/Sharing/Property/APasswordSharePropertyTypeTest.php
+++ b/tests/lib/Sharing/Property/APasswordSharePropertyTypeTest.php
@@ -105,9 +105,9 @@ public function testValidateValue(): void {
$this->assertIsString($this->propertyType->validateValue($l10nFactory, $share, '123'));
}
- public function testModifyValueOnFetch(): void {
- $this->assertNull($this->propertyType->modifyValueOnLoad(null));
- $this->assertEquals(APasswordSharePropertyType::PLACEHOLDER, $this->propertyType->modifyValueOnLoad(''));
+ public function testModifyValueOnFormat(): void {
+ $this->assertNull($this->propertyType->modifyValueOnFormat(null));
+ $this->assertEquals(APasswordSharePropertyType::PLACEHOLDER, $this->propertyType->modifyValueOnFormat(''));
}
public function testModifyValueOnSave(): void {
diff --git a/tests/lib/Sharing/ShareTest.php b/tests/lib/Sharing/ShareTest.php
index 817a525a719f2..a92e48af84eb1 100644
--- a/tests/lib/Sharing/ShareTest.php
+++ b/tests/lib/Sharing/ShareTest.php
@@ -203,7 +203,7 @@ public function testGetEffectiveEnabledPermissions(array $recipients, array $per
$this->assertEquals(
$enabled
- ? [TestSharePermissionType1::class => new SharePermission(TestSharePermissionType1::class, true)]
+ ? [TestSharePermissionType1::class]
: [],
$share->getEffectiveEnabledPermissions(new ShareAccessContext($this->user1)),
);
diff --git a/tests/lib/Sharing/SharingLegacySyncTest.php b/tests/lib/Sharing/SharingLegacySyncTest.php
new file mode 100644
index 0000000000000..3c17c88a12ea2
--- /dev/null
+++ b/tests/lib/Sharing/SharingLegacySyncTest.php
@@ -0,0 +1,2081 @@
+sharingRegistry = Server::get(ISharingRegistry::class);
+ $this->sharingManager = Server::get(ISharingManager::class);
+ $this->sharingBackend = Server::get(ISharingBackend::class);
+ $this->legacySharingManager = Server::get(IManager::class);
+ $this->legacySync = Server::get(SharingLegacySync::class);
+ $this->legacyMapper = Server::get(LegacyMapper::class);
+ $this->dbConnection = Server::get(IDBConnection::class);
+ $this->snowflakeGenerator = Server::get(ISnowflakeGenerator::class);
+ $this->snowflakeDecoder = Server::get(ISnowflakeDecoder::class);
+ $this->sourceNodeTargetManager = Server::get(SourceNodeTargetManager::class);
+
+ $this->sharingRegistry->clear();
+ $this->invokePrivate(Server::get(Application::class), 'registerSharing');
+ $this->invokePrivate(Server::get(\OCA\Files\AppInfo\Application::class), 'registerSharing');
+
+ $this->owner = $this->createUser('owner', 'password');
+ $this->user1 = $this->createUser('user1', 'password');
+ $this->user2 = $this->createUser('user2', 'password');
+ $this->group = $this->createGroup('group');
+ $this->groupBackend->addToGroup($this->user1->getUID(), $this->group->getGID());
+
+ $ownerFolder = Server::get(IRootFolder::class)->getUserFolder($this->owner->getUID());
+ $this->nodeFolder = $ownerFolder->newFolder('foo');
+ $this->nodeFile = $ownerFolder->newFile('foo.txt');
+
+ $this->dbConnection->beginTransaction();
+ }
+
+ #[\Override]
+ public function tearDown(): void {
+ $this->clearShares();
+
+ $this->sharingRegistry->clear();
+
+ Server::get(ISetupManager::class)->tearDown();
+
+ // Can't rollback, because it would rollback the classmap data, but the ClassMapper still has the values cached.
+ $this->dbConnection->commit();
+ parent::tearDown();
+ }
+
+ private function clearShares(): void {
+ foreach ($this->legacySharingManager->getAllShares() as $legacyShare) {
+ $this->legacySharingManager->deleteShare($legacyShare);
+ }
+
+ $accessContext = new ShareAccessContext(overrideChecks: true);
+ foreach ($this->sharingManager->getShares($accessContext, null, null, null, null, null, null) as $share) {
+ $this->sharingManager->deleteShare($accessContext, $share);
+ }
+
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb
+ ->delete('sharing_source_node_target')
+ ->executeStatement();
+ }
+
+ /**
+ * {@see \OCP\Share\IManager::getAllShares()} does not return shares of {@see IShare::TYPE_USERGROUP}, which we need to verify the behavior.
+ * @return list
+ */
+ private function getAllLegacyShares(): array {
+ $providers = Server::get(IProviderFactory::class)->getAllProviders();
+
+ $legacyShares = [];
+ foreach ($providers as $provider) {
+ if ($provider->identifier() === 'ocinternal') {
+ /** @var DefaultShareProvider $provider */
+ $legacyShares[] = array_values(iterator_to_array($provider->getAllShares(true)));
+ } else {
+ $legacyShares[] = array_values(iterator_to_array($provider->getAllShares()));
+ }
+ }
+
+ return array_merge(...$legacyShares);
+ }
+
+ /**
+ * @param array $shareUserStatuses
+ */
+ private function createShare(Share $share, array $shareUserStatuses): void {
+ $this->sharingBackend->createShare($share->id, $share->owner, $share->getCreatedAt());
+
+ foreach ($share->sources as $source) {
+ $this->sharingBackend->addShareSource($share->id, $source);
+ }
+
+ foreach ($share->recipients as $recipient) {
+ $this->sharingBackend->addShareRecipient($share->id, $recipient);
+
+ foreach ($recipient->permissions as $permission) {
+ $this->sharingBackend->updateShareRecipientPermission($share->id, $recipient, $permission);
+ }
+ }
+
+ foreach ($share->properties as $property) {
+ $this->sharingBackend->updateShareProperty($share->id, $property);
+ }
+
+ foreach ($share->permissions as $permission) {
+ $this->sharingBackend->updateSharePermission($share->id, $permission);
+ }
+
+ if ($share->userStatus instanceof \NCU\Sharing\ShareUserStatus) {
+ throw new RuntimeException('Do not pass a user status directly.');
+ }
+
+ foreach ($shareUserStatuses as $userId => $userStatus) {
+ $this->sharingBackend->updateShareUserStatus($share->id, $userId, $userStatus);
+ }
+
+ // Only use the manager here, so the update event is dispatched.
+ $this->sharingManager->updateShareState(new ShareAccessContext(overrideChecks: true), $share, $share->state);
+ }
+
+ /**
+ * @param list $legacyShares
+ * @return list
+ */
+ private function createLegacyShares(array $legacyShares): array {
+ $legacyParentShares = array_values(array_filter($legacyShares, static fn (IShare $legacyShare): bool => $legacyShare->getShareType() !== IShare::TYPE_USERGROUP));
+ /** @var non-empty-list $legacyParentShares */
+ $this->assertNotEmpty($legacyParentShares);
+ foreach ($legacyParentShares as &$legacyShare) {
+ $legacyShareWithoutProvider = clone $legacyShare;
+ $this->invokePrivate($legacyShareWithoutProvider, 'providerId', [null]);
+ $legacyShareWithoutProvider = $this->legacySharingManager->createShare($legacyShareWithoutProvider);
+ // The share might get auto accepted, so we have to manually set the desired status.
+ $legacyShareWithoutProvider->setStatus($legacyShare->getStatus() ?? IShare::STATUS_PENDING);
+ $legacyShare = $this->legacySharingManager->updateShare($legacyShareWithoutProvider);
+ }
+
+ // Ensure child shares have been created
+ $this->assertCount(count($legacyShares), $this->getAllLegacyShares());
+
+ $legacyChildShares = array_filter($legacyShares, static fn (IShare $legacyShare): bool => $legacyShare->getShareType() === IShare::TYPE_USERGROUP);
+
+ $legacyChildShareStatuses = array_values(array_unique(array_map(static fn (IShare $legacyShare): ?int => $legacyShare->getStatus(), $legacyChildShares)));
+ if ($legacyChildShareStatuses !== []) {
+ if (count($legacyChildShareStatuses) > 1) {
+ throw new RuntimeException('More than one status value, not working yet.');
+ }
+
+ $this->assertNotNull($legacyChildShareStatuses[0]);
+
+ // The child shares get auto accepted, so we have to manually set the desired status.
+ $qb = $this->dbConnection->getQueryBuilder();
+ $rowCount = $qb
+ ->update('share')
+ ->set('accepted', $qb->createNamedParameter($legacyChildShareStatuses[0], IQueryBuilder::PARAM_INT))
+ ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP)))
+ ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter(array_map(static fn (IShare $legacyShare): int => (int)$legacyShare->getId(), $legacyParentShares), IQueryBuilder::PARAM_INT_ARRAY)))
+ ->executeStatement();
+ $this->assertEquals(count(array_filter($legacyShares, static fn (IShare $legacyShare): bool => $legacyShare->getShareType() === IShare::TYPE_USERGROUP)), $rowCount);
+ }
+
+ $legacyChildShareTargets = array_values(array_unique(array_map(static fn (IShare $legacyShare): string => $legacyShare->getTarget(), $legacyChildShares)));
+ if ($legacyChildShareTargets !== []) {
+ if (count($legacyChildShareTargets) > 1) {
+ throw new RuntimeException('More than one target value, not working yet.');
+ }
+
+ // The child shares get auto accepted, so we have to manually set the desired target.
+ $qb = $this->dbConnection->getQueryBuilder();
+ $rowCount = $qb
+ ->update('share')
+ ->set('file_target', $qb->createNamedParameter($legacyChildShareTargets[0]))
+ ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP)))
+ ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter(array_map(static fn (IShare $legacyShare): int => (int)$legacyShare->getId(), $legacyParentShares), IQueryBuilder::PARAM_INT_ARRAY)))
+ ->executeStatement();
+ $this->assertEquals(count(array_filter($legacyShares, static fn (IShare $legacyShare): bool => $legacyShare->getShareType() === IShare::TYPE_USERGROUP)), $rowCount);
+ }
+
+ $legacyChildSharePermissions = array_values(array_unique(array_map(static fn (IShare $legacyShare): int => $legacyShare->getPermissions(), $legacyChildShares)));
+ if ($legacyChildSharePermissions !== []) {
+ if (count($legacyChildSharePermissions) > 1) {
+ throw new RuntimeException('More than one permissions value, not working yet.');
+ }
+
+ // The child shares get auto accepted, so we have to manually set the desired permissions.
+ $qb = $this->dbConnection->getQueryBuilder();
+ $rowCount = $qb
+ ->update('share')
+ ->set('permissions', $qb->createNamedParameter($legacyChildSharePermissions[0], IQueryBuilder::PARAM_INT))
+ ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP)))
+ ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter(array_map(static fn (IShare $legacyShare): int => (int)$legacyShare->getId(), $legacyParentShares), IQueryBuilder::PARAM_INT_ARRAY)))
+ ->executeStatement();
+ $this->assertEquals(count(array_filter($legacyShares, static fn (IShare $legacyShare): bool => $legacyShare->getShareType() === IShare::TYPE_USERGROUP)), $rowCount);
+ }
+
+ if ($legacyChildShareStatuses !== [] || $legacyChildShareTargets !== [] || $legacyChildSharePermissions !== []) {
+ // Just update anything to trigger the syncing
+ $this->legacySharingManager->updateShare($legacyParentShares[0]);
+ }
+
+ return $legacyParentShares;
+ }
+
+ private function fixLegacyShare(IShare $legacyShare): IShare {
+ // Having the node set seems to trigger and infinite loop in PHPUnit when comparing the objects.
+ $node = $legacyShare->getNode();
+ $legacyShare->setNodeId($node->getId());
+ $legacyShare->setNodeType($node instanceof File ? 'file': 'folder');
+
+ // There might be a closure that we can't reproduce and we want the value the closure returns to be set.
+ $legacyShare->getSharedWithDisplayName();
+
+ // The original target is only an internal property and not stored in the DB.
+ $this->invokePrivate($legacyShare, 'originalTarget', [null]);
+
+ return $legacyShare;
+ }
+
+ private function fixShare(Share $share): Share {
+ foreach ($share->sources as $source) {
+ $this->invokePrivate($source, 'getMetadata', [$this->sharingRegistry]);
+ }
+
+ return $share;
+ }
+
+ /**
+ * @param list $expected
+ * @param list $actual
+ */
+ private function assertLegacySharesEquals(array $expected, array $actual): void {
+ $expected = array_map(
+ function (int $index, IShare $legacyShare) use ($actual): IShare {
+ $legacyShare->setId($actual[$index]->getId());
+ if (($parent = $legacyShare->getParent()) !== null) {
+ $legacyShare->setParent((int)$actual[$parent]->getId());
+ }
+
+ $shareTime = $legacyShare->getShareTime();
+ $legacyShare->setShareTime($shareTime->setTime(
+ (int)$shareTime->format('H'),
+ (int)$shareTime->format('i'),
+ (int)$shareTime->format('s'),
+ ));
+
+ if (($expirationDate = $legacyShare->getExpirationDate()) !== null) {
+ $legacyShare->setExpirationDate($expirationDate->setTime(
+ (int)$expirationDate->format('H'),
+ (int)$expirationDate->format('i'),
+ (int)$expirationDate->format('s'),
+ ));
+ }
+
+ return $legacyShare;
+ },
+ array_keys($expected),
+ $expected,
+ );
+
+ $this->assertEquals(array_map($this->fixLegacyShare(...), $expected), array_map($this->fixLegacyShare(...), $actual));
+ }
+
+ private function assertSharesEquals(Share $expected, Share $actual): void {
+ $this->assertEquals($this->fixShare($expected), $this->fixShare($actual));
+ }
+
+ /**
+ * @param list $expectedLegacyShares
+ * @param list $actualShares
+ * @param array> $shareUserStatuses
+ */
+ private function assertSyncingShareToLegacySharesWorks(array $expectedLegacyShares, array $actualShares, array $shareUserStatuses): void {
+ foreach ($actualShares as $index => $actualShare) {
+ $this->createShare($actualShare, $shareUserStatuses[$index] ?? []);
+ }
+
+ $actualLegacyShares = $this->getAllLegacyShares();
+ $this->assertCount(count($expectedLegacyShares), $actualLegacyShares);
+ $this->assertLegacySharesEquals($expectedLegacyShares, $actualLegacyShares);
+
+ $this->clearShares();
+ }
+
+ /**
+ * @param list $expectedShares
+ * @param list $actualLegacyShares
+ * @param array> $shareUserStatuses
+ */
+ private function assertSyncingLegacySharesToShareWorks(array $expectedShares, array $actualLegacyShares, array $shareUserStatuses, bool $ignoreRecipientValues): void {
+ $this->createLegacyShares($actualLegacyShares);
+
+ $actualShares = $this->sharingManager->getShares(new ShareAccessContext(currentUser: $this->user1, overrideChecks: true), null, null, null, null, null, null);
+ $this->assertCount(count($expectedShares), $actualShares);
+ foreach ($actualShares as $index => $actualShare) {
+ $expectedShare = $expectedShares[$index];
+
+ $this->assertSharesEquals(
+ new Share(
+ $actualShare->id,
+ $expectedShare->owner,
+ $actualShare->lastUpdated,
+ $expectedShare->state,
+ ($shareUserStatuses[$index] ?? [])[$this->user1->getUID()] ?? null,
+ $expectedShare->sources,
+ array_map(
+ fn (int $index, ShareRecipient $recipient): ShareRecipient => new ShareRecipient(
+ $recipient->class,
+ $ignoreRecipientValues ? $actualShare->recipients[$index]->value : $recipient->value,
+ $recipient->instance,
+ $actualShare->recipients[$index]->secret,
+ $recipient->initiator,
+ $recipient->permissions,
+ ),
+ array_keys($expectedShare->recipients),
+ $expectedShare->recipients,
+ ),
+ $expectedShare->properties,
+ $expectedShare->permissions,
+ ),
+ $actualShare,
+ );
+ }
+
+ $this->clearShares();
+ }
+
+ /**
+ * @param list $expectedLegacyShares
+ */
+ private function assertSyncingLegacySharesToShareAndBackWorks(array $expectedLegacyShares, bool $delete): void {
+ $filteredExpectedLegacyShares = $this->createLegacyShares($expectedLegacyShares);
+
+ $actualShares = $this->sharingManager->getShares(new ShareAccessContext(currentUser: $this->user1, overrideChecks: true), null, null, null, null, null, null);
+
+ if ($delete) {
+ $this->invokePrivate($this->legacySync, 'ignoreAllEvents', [true]);
+ foreach ($filteredExpectedLegacyShares as &$expectedLegacyShare) {
+ $this->legacySharingManager->deleteShare($expectedLegacyShare);
+ $this->invokePrivate($expectedLegacyShare, 'id', [null]);
+ }
+
+ foreach ($actualShares as $actualShare) {
+ $this->invokePrivate($this->legacyMapper, 'deleteLegacyMappings', [$actualShare->id]);
+ }
+
+ $this->invokePrivate($this->legacySync, 'ignoreAllEvents', [false]);
+ }
+
+ // Just update anything to trigger the syncing
+ foreach ($actualShares as $actualShare) {
+ $this->sharingManager->updateShareState(new ShareAccessContext(overrideChecks: true), $actualShare, $actualShare->state);
+ }
+
+ $actualLegacyShares = $this->getAllLegacyShares();
+ $this->assertCount(count($expectedLegacyShares), $actualLegacyShares);
+ $this->assertLegacySharesEquals($expectedLegacyShares, $actualLegacyShares);
+
+ $this->clearShares();
+ }
+
+ /**
+ * @param list $expectedShares
+ * @param array> $shareUserStatuses
+ */
+ private function assertSyncingShareToLegacySharesAndBackWorks(array $expectedShares, array $shareUserStatuses, bool $delete, bool $ignoreRecipientValues): void {
+ foreach ($expectedShares as $index => $expectedShare) {
+ $this->createShare($expectedShare, $shareUserStatuses[$index] ?? []);
+ }
+
+ $actualLegacyShares = $this->getAllLegacyShares();
+
+ if ($delete) {
+ $this->invokePrivate($this->legacySync, 'ignoreAllEvents', [true]);
+ foreach ($expectedShares as $expectedShare) {
+ $this->sharingManager->deleteShare(new ShareAccessContext(overrideChecks: true), $expectedShare);
+ $this->invokePrivate($this->legacyMapper, 'deleteLegacyMappings', [$expectedShare->id]);
+ }
+
+ $this->invokePrivate($this->legacySync, 'ignoreAllEvents', [false]);
+ }
+
+ // Just update anything to trigger the syncing
+ $this->legacySharingManager->updateShare($actualLegacyShares[0]);
+
+ $actualShares = $this->sharingManager->getShares(new ShareAccessContext(currentUser: $this->user1, overrideChecks: true), null, null, null, null, null, null);
+ $this->assertCount(count($expectedShares), $actualShares);
+ foreach ($actualShares as $index => $actualShare) {
+ $expectedShare = $expectedShares[$index];
+
+ $this->assertSharesEquals(
+ new Share(
+ $actualShare->id,
+ $expectedShare->owner,
+ $actualShare->lastUpdated,
+ $expectedShare->state,
+ ($shareUserStatuses[$index] ?? [])[$this->user1->getUID()] ?? null,
+ $expectedShare->sources,
+ array_map(
+ fn (int $index, ShareRecipient $recipient): ShareRecipient => new ShareRecipient(
+ $recipient->class,
+ $ignoreRecipientValues ? $actualShare->recipients[$index]->value : $recipient->value,
+ $recipient->instance,
+ $actualShare->recipients[$index]->secret,
+ $recipient->initiator,
+ $recipient->permissions,
+ ),
+ array_keys($expectedShare->recipients),
+ $expectedShare->recipients,
+ ),
+ $expectedShare->properties,
+ $expectedShare->permissions,
+ ),
+ $actualShare,
+ );
+ }
+
+ $this->clearShares();
+ }
+
+ /**
+ * @param list $shares
+ * @param list $legacyShares
+ * @param array> $shareUserStatuses
+ */
+ private function assertSyncingWorks(array $shares, array $legacyShares, array $shareUserStatuses = [], bool $ignoreRecipientValues = false): void {
+ $this->assertSyncingShareToLegacySharesWorks(array_map(static fn (IShare $legacyShare): IShare => clone $legacyShare, $legacyShares), $shares, $shareUserStatuses);
+ $this->assertSyncingLegacySharesToShareWorks($shares, array_map(static fn (IShare $legacyShare): IShare => clone $legacyShare, $legacyShares), $shareUserStatuses, $ignoreRecipientValues);
+
+ $this->assertSyncingShareToLegacySharesAndBackWorks($shares, $shareUserStatuses, false, false);
+ $this->assertSyncingShareToLegacySharesAndBackWorks($shares, $shareUserStatuses, true, $ignoreRecipientValues);
+
+ $this->assertSyncingLegacySharesToShareAndBackWorks(array_map(static fn (IShare $legacyShare): IShare => clone $legacyShare, $legacyShares), false);
+ $this->assertSyncingLegacySharesToShareAndBackWorks(array_map(static fn (IShare $legacyShare): IShare => clone $legacyShare, $legacyShares), true);
+ }
+
+ /**
+ * @return array{non-empty-string, DateTimeImmutable}
+ */
+ private function generateIdAndCreatedTimestamp(): array {
+ // Generate a Snowflake ID for the past, to find all codepaths that use the current time instead of the specified time.
+ $id = $this->snowflakeGenerator->nextId((new DateTimeImmutable())->sub(new DateInterval('PT1M')));
+ $created = $this->snowflakeDecoder->decode($id)->getCreatedAt();
+ return [$id, $created];
+ }
+
+ public function testDeleteShare(): void {
+ $this->legacySharingManager->createShare(
+ $this->legacySharingManager->newShare()
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedWith($this->user1->getUID())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ );
+
+ $shares = $this->sharingManager->getShares(new ShareAccessContext(overrideChecks: true), null, null, null, null, null, null);
+ $this->assertCount(1, $shares);
+
+ $this->sharingManager->deleteShare(new ShareAccessContext(overrideChecks: true), $shares[0]);
+
+ $this->assertEmpty(iterator_to_array($this->legacySharingManager->getAllShares()));
+ }
+
+ public function testDeleteLegacyShare(): void {
+ $legacyShare = $this->legacySharingManager->createShare(
+ $this->legacySharingManager->newShare()
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedWith($this->user1->getUID())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ );
+
+ $shares = $this->sharingManager->getShares(new ShareAccessContext(overrideChecks: true), null, null, null, null, null, null);
+ $this->assertCount(1, $shares);
+
+ $this->legacySharingManager->deleteShare($legacyShare);
+
+ $this->assertEmpty($this->sharingManager->getShares(new ShareAccessContext(overrideChecks: true), null, null, null, null, null, null));
+ }
+
+ /**
+ * @return list
+ */
+ public static function dataShareState(): array {
+ return [
+ [ShareState::Active, 1],
+ [ShareState::Draft, 0],
+ [ShareState::Deleted, 0],
+ ];
+ }
+
+ #[DataProvider('dataShareState')]
+ public function testShareStateToLegacy(ShareState $shareState, int $legacySharesCount): void {
+ $accessContext = new ShareAccessContext($this->owner);
+ $share = $this->sharingManager->createShare($accessContext);
+ $share = $this->sharingManager->addShareSource($accessContext, $share, new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId()));
+ $share = $this->sharingManager->addShareRecipient($accessContext, $share, new ShareRecipient(UserShareRecipientType::class, $this->user1->getUID(), null));
+ $share = $this->sharingManager->updateSharePermission($accessContext, $share, new SharePermission(NodeReadSharePermissionType::class, true));
+ $share = $this->sharingManager->updateSharePermission($accessContext, $share, new SharePermission(NodeDownloadSharePermissionType::class, true));
+
+ $this->sharingManager->updateShareState($accessContext, $share, $shareState);
+
+ $legacyShares = array_values(iterator_to_array($this->legacySharingManager->getAllShares()));
+ $this->assertCount($legacySharesCount, $legacyShares);
+ }
+
+ public function testShareStateFromLegacy(): void {
+ $this->legacySharingManager->createShare(
+ $this->legacySharingManager->newShare()
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedWith($this->user1->getUID())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ );
+
+ $shares = $this->sharingManager->getShares(new ShareAccessContext(overrideChecks: true), null, null, null, null, null, null);
+ $this->assertCount(1, $shares);
+ $this->assertEquals(ShareState::Active, $shares[0]->state);
+ }
+
+ /**
+ * @return list
+ */
+ public static function dataShareUserStatus(): array {
+ return [
+ [ShareUserStatus::Pending, IShare::STATUS_PENDING],
+ [ShareUserStatus::Accepted, IShare::STATUS_ACCEPTED],
+ [ShareUserStatus::Rejected, IShare::STATUS_REJECTED],
+ ];
+ }
+
+ /**
+ * @param IShare::STATUS_* $legacyStatus
+ */
+ #[DataProvider('dataShareUserStatus')]
+ public function testShareUserStatus(ShareUserStatus $shareUserStatus, int $legacyStatus): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(UserShareRecipientType::class, $this->user1->getUID(), null, $this->sharingManager->generateSecret(), $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedWith($this->user1->getUID())
+ ->setSharedWithDisplayName($this->user1->getDisplayName())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus($legacyStatus)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => $shareUserStatus,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses);
+ }
+
+ public function testDefaultTarget(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(UserShareRecipientType::class, $this->user1->getUID(), null, $this->sharingManager->generateSecret(), $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedWith($this->user1->getUID())
+ ->setSharedWithDisplayName($this->user1->getDisplayName())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_ACCEPTED)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Accepted,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses);
+ }
+
+ public function testNonDefaultTarget(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+
+ $shares = [new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(UserShareRecipientType::class, $this->user1->getUID(), null, $this->sharingManager->generateSecret(), $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $this->sourceNodeTargetManager->setTarget($this->user1->getUID(), $owner, $this->nodeFolder->getId(), '/abc');
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedWith($this->user1->getUID())
+ ->setSharedWithDisplayName($this->user1->getDisplayName())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_ACCEPTED)
+ ->setTarget('/abc')
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Accepted,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses);
+ }
+
+ public function testNonDefaultTargetGroup(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(GroupShareRecipientType::class, $this->group->getGID(), null, $this->sharingManager->generateSecret(), $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $this->sourceNodeTargetManager->setTarget($this->user1->getUID(), $owner, $this->nodeFolder->getId(), '/abc');
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_GROUP)
+ ->setSharedWith($this->group->getGID())
+ ->setSharedWithDisplayName($this->group->getDisplayName())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false),
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USERGROUP)
+ ->setSharedWith($this->user1->getUID())
+ ->setSharedWithDisplayName($this->user1->getDisplayName())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_ACCEPTED)
+ ->setTarget('/abc')
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setParent(0),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Accepted,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses);
+
+ }
+
+ /**
+ * @param IShare::STATUS_* $legacyStatus
+ */
+ #[DataProvider('dataShareUserStatus')]
+ public function testGroupUserStatus(ShareUserStatus $shareUserStatus, int $legacyStatus): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(GroupShareRecipientType::class, $this->group->getGID(), null, $this->sharingManager->generateSecret(), $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_GROUP)
+ ->setSharedWith($this->group->getGID())
+ ->setSharedWithDisplayName($this->group->getDisplayName())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false),
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USERGROUP)
+ ->setSharedWith($this->user1->getUID())
+ ->setSharedWithDisplayName($this->user1->getDisplayName())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions($shareUserStatus === ShareUserStatus::Rejected ? 0 : Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus($shareUserStatus === ShareUserStatus::Rejected ? IShare::STATUS_ACCEPTED : $legacyStatus)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setParent(0),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => $shareUserStatus,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses);
+ }
+
+ public function testReshareToLegacy(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [
+ new ShareRecipient(
+ UserShareRecipientType::class,
+ $this->user1->getUID(),
+ null,
+ $this->sharingManager->generateSecret(),
+ $owner,
+ ),
+ new ShareRecipient(
+ UserShareRecipientType::class,
+ $this->user2->getUID(),
+ null,
+ $this->sharingManager->generateSecret(),
+ new ShareUser($this->user1->getUID(), null),
+ [
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, false),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ ],
+ ),
+ ],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, true),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedWith($this->user1->getUID())
+ ->setSharedWithDisplayName($this->user1->getDisplayName())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_SHARE)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_ACCEPTED)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false),
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedWith($this->user2->getUID())
+ ->setSharedWithDisplayName($this->user2->getDisplayName())
+ ->setSharedBy($this->user1->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', false))
+ ->setStatus(IShare::STATUS_ACCEPTED)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Accepted,
+ $this->user2->getUID() => ShareUserStatus::Accepted,
+ ],
+ ];
+
+ // In this direction we can sync reshares correctly, because all information is contained in the share.
+ $this->assertSyncingShareToLegacySharesWorks(array_map(static fn (IShare $legacyShare): IShare => clone $legacyShare, $legacyShares), $shares, $shareUserStatuses);
+ $this->assertSyncingShareToLegacySharesAndBackWorks($shares, $shareUserStatuses, false, false);
+ // Can't test syncing forth and back with deletion, because the information for linking reshares is missing in legacy shares.
+ }
+
+ public function testReshareFromLegacy(): void {
+ [$id1, $created1] = $this->generateIdAndCreatedTimestamp();
+ [$id2, $created2] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+
+ $shares = [
+ new Share(
+ $id1,
+ $owner,
+ $created1,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [
+ new ShareRecipient(UserShareRecipientType::class, $this->user1->getUID(), null, $this->sharingManager->generateSecret(), $owner),
+ ],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, true),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ new Share(
+ $id2,
+ $owner,
+ $created2,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [
+ new ShareRecipient(UserShareRecipientType::class, $this->user2->getUID(), null, $this->sharingManager->generateSecret(), new ShareUser($this->user1->getUID(), null)),
+ ],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, false),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedWith($this->user1->getUID())
+ ->setSharedWithDisplayName($this->user1->getDisplayName())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_SHARE)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_ACCEPTED)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created1))
+ ->setMailSend(false),
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedWith($this->user2->getUID())
+ ->setSharedWithDisplayName($this->user2->getDisplayName())
+ ->setSharedBy($this->user1->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', false))
+ ->setStatus(IShare::STATUS_ACCEPTED)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created2))
+ ->setMailSend(false),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Accepted,
+ ],
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ // In this direction we can't sync reshares correctly, because the information for linking reshares is missing in legacy shares.
+ $this->assertSyncingLegacySharesToShareWorks($shares, array_map(static fn (IShare $legacyShare): IShare => clone $legacyShare, $legacyShares), $shareUserStatuses, false);
+ $this->assertSyncingLegacySharesToShareAndBackWorks(array_map(static fn (IShare $legacyShare): IShare => clone $legacyShare, $legacyShares), false);
+ $this->assertSyncingLegacySharesToShareAndBackWorks(array_map(static fn (IShare $legacyShare): IShare => clone $legacyShare, $legacyShares), true);
+ }
+
+ /**
+ * @return list
+ */
+ public static function dataDownloadPermission(): array {
+ return [
+ [true],
+ [false],
+ ];
+ }
+
+ #[DataProvider('dataDownloadPermission')]
+ public function testDownloadPermissionPrivateShare(bool $enabled): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(UserShareRecipientType::class, $this->user1->getUID(), null, $this->sharingManager->generateSecret(), $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, $enabled),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedWith($this->user1->getUID())
+ ->setSharedWithDisplayName($this->user1->getDisplayName())
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes(
+ (new ShareAttributes())
+ ->setAttribute('permissions', 'download', $enabled),
+ )
+ ->setStatus(IShare::STATUS_ACCEPTED)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setHideDownload(false),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Accepted,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses);
+ }
+
+ #[DataProvider('dataDownloadPermission')]
+ public function testEmailShareWithDownloadPermission(bool $enabled): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(EmailShareRecipientType::class, 'test@example.com', null, $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ PasswordSharePropertyType::class => new ShareProperty(PasswordSharePropertyType::class, null),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, $enabled),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocMailShare')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_EMAIL)
+ ->setSharedWith('test@example.com')
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes(
+ (new ShareAttributes())
+ ->setAttribute('permissions', 'download', $enabled),
+ )
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret)
+ ->setHideDownload(!$enabled),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses);
+ }
+
+ #[DataProvider('dataDownloadPermission')]
+ public function testLinkShareWithDownloadPermission(bool $enabled): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(TokenShareRecipientType::class, $this->sharingManager->generateSecret(), null, $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ PasswordSharePropertyType::class => new ShareProperty(PasswordSharePropertyType::class, null),
+ LabelSharePropertyType::class => new ShareProperty(LabelSharePropertyType::class, null),
+ NodeGridViewSharePropertyType::class => new ShareProperty(NodeGridViewSharePropertyType::class, 'false'),
+ NodeNicknameSharePropertyType::class => new ShareProperty(NodeNicknameSharePropertyType::class, 'false'),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, $enabled),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_LINK)
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes(
+ (new ShareAttributes())
+ ->setAttribute('permissions', 'download', $enabled),
+ )
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret)
+ ->setHideDownload(!$enabled),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses, true);
+ }
+
+ public function testEmailShareWithPassword(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+ $passwordHash = Server::get(IHasher::class)->hash('123');
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(EmailShareRecipientType::class, 'test@example.com', null, $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ PasswordSharePropertyType::class => new ShareProperty(PasswordSharePropertyType::class, $passwordHash),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocMailShare')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_EMAIL)
+ ->setSharedWith('test@example.com')
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret)
+ ->setPasswordHash($passwordHash),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses);
+ }
+
+ public function testLinkShare(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(TokenShareRecipientType::class, $this->sharingManager->generateSecret(), null, $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ PasswordSharePropertyType::class => new ShareProperty(PasswordSharePropertyType::class, null),
+ LabelSharePropertyType::class => new ShareProperty(LabelSharePropertyType::class, null),
+ NodeGridViewSharePropertyType::class => new ShareProperty(NodeGridViewSharePropertyType::class, 'false'),
+ NodeNicknameSharePropertyType::class => new ShareProperty(NodeNicknameSharePropertyType::class, 'false'),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_LINK)
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret)
+ ->setHideDownload(false),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses, true);
+ }
+
+ public function testLinkShareWithPassword(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+ $passwordHash = Server::get(IHasher::class)->hash('123');
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(TokenShareRecipientType::class, $this->sharingManager->generateSecret(), null, $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ PasswordSharePropertyType::class => new ShareProperty(PasswordSharePropertyType::class, $passwordHash),
+ LabelSharePropertyType::class => new ShareProperty(LabelSharePropertyType::class, null),
+ NodeGridViewSharePropertyType::class => new ShareProperty(NodeGridViewSharePropertyType::class, 'false'),
+ NodeNicknameSharePropertyType::class => new ShareProperty(NodeNicknameSharePropertyType::class, 'false'),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_LINK)
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret)
+ ->setHideDownload(false)
+ ->setPasswordHash($passwordHash),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses, true);
+ }
+
+ /**
+ * @return list
+ */
+ public static function dataGridView(): array {
+ return [
+ [true],
+ [false],
+ ];
+ }
+
+ #[DataProvider('dataGridView')]
+ public function testGridView(bool $enabled): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(TokenShareRecipientType::class, $this->sharingManager->generateSecret(), null, $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ PasswordSharePropertyType::class => new ShareProperty(PasswordSharePropertyType::class, null),
+ LabelSharePropertyType::class => new ShareProperty(LabelSharePropertyType::class, null),
+ NodeGridViewSharePropertyType::class => new ShareProperty(NodeGridViewSharePropertyType::class, $enabled ? 'true' : 'false'),
+ NodeNicknameSharePropertyType::class => new ShareProperty(NodeNicknameSharePropertyType::class, 'false'),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $attributes = new ShareAttributes();
+ $attributes->setAttribute('permissions', 'download', true);
+ if ($enabled) {
+ $attributes->setAttribute('config', 'grid_view', true);
+ }
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_LINK)
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes($attributes)
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret)
+ ->setHideDownload(false),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses, true);
+ }
+
+ public function testExpirationDate(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+ $expirationDate = $created->add(new DateInterval('PT1H'));
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(TokenShareRecipientType::class, $this->sharingManager->generateSecret(), null, $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, $expirationDate->format(DateTimeInterface::ATOM)),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ PasswordSharePropertyType::class => new ShareProperty(PasswordSharePropertyType::class, null),
+ LabelSharePropertyType::class => new ShareProperty(LabelSharePropertyType::class, null),
+ NodeGridViewSharePropertyType::class => new ShareProperty(NodeGridViewSharePropertyType::class, 'false'),
+ NodeNicknameSharePropertyType::class => new ShareProperty(NodeNicknameSharePropertyType::class, 'false'),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_LINK)
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret)
+ ->setHideDownload(false)
+ ->setExpirationDate(DateTime::createFromImmutable($expirationDate)),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses, true);
+ }
+
+ public function testLabel(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(TokenShareRecipientType::class, $this->sharingManager->generateSecret(), null, $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ PasswordSharePropertyType::class => new ShareProperty(PasswordSharePropertyType::class, null),
+ LabelSharePropertyType::class => new ShareProperty(LabelSharePropertyType::class, 'label'),
+ NodeGridViewSharePropertyType::class => new ShareProperty(NodeGridViewSharePropertyType::class, 'false'),
+ NodeNicknameSharePropertyType::class => new ShareProperty(NodeNicknameSharePropertyType::class, 'false'),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_LINK)
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret)
+ ->setHideDownload(false)
+ ->setLabel('label'),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses, true);
+ }
+
+ public function testNote(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(TokenShareRecipientType::class, $this->sharingManager->generateSecret(), null, $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, 'note'),
+ PasswordSharePropertyType::class => new ShareProperty(PasswordSharePropertyType::class, null),
+ LabelSharePropertyType::class => new ShareProperty(LabelSharePropertyType::class, null),
+ NodeGridViewSharePropertyType::class => new ShareProperty(NodeGridViewSharePropertyType::class, 'false'),
+ NodeNicknameSharePropertyType::class => new ShareProperty(NodeNicknameSharePropertyType::class, 'false'),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_LINK)
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret)
+ ->setHideDownload(false)
+ ->setNote('note'),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses, true);
+ }
+
+ public function testFileRequestWithoutEmails(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(TokenShareRecipientType::class, $this->sharingManager->generateSecret(), null, $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ PasswordSharePropertyType::class => new ShareProperty(PasswordSharePropertyType::class, null),
+ LabelSharePropertyType::class => new ShareProperty(LabelSharePropertyType::class, null),
+ NodeGridViewSharePropertyType::class => new ShareProperty(NodeGridViewSharePropertyType::class, 'false'),
+ NodeNicknameSharePropertyType::class => new ShareProperty(NodeNicknameSharePropertyType::class, 'true'),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, false),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, false),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, true),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocMailShare')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_EMAIL)
+ ->setSharedWith('')
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_CREATE)
+ ->setAttributes(
+ (new ShareAttributes())
+ ->setAttribute('permissions', 'download', false)
+ ->setAttribute('fileRequest', 'enabled', true)
+ ->setAttribute('shareWith', 'emails', []),
+ )
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret)
+ ->setHideDownload(true),
+ ];
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses, true);
+ }
+
+ public function testFileRequestWithEmails(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret1 = $this->sharingManager->generateSecret();
+ $secret2 = $this->sharingManager->generateSecret();
+ $secret3 = $this->sharingManager->generateSecret();
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [
+ new ShareRecipient(TokenShareRecipientType::class, $this->sharingManager->generateSecret(), null, $secret1, $owner),
+ new ShareRecipient(EmailShareRecipientType::class, 'example@example.com', null, $secret2, $owner),
+ new ShareRecipient(EmailShareRecipientType::class, 'example@example.org', null, $secret3, $owner),
+ ],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ PasswordSharePropertyType::class => new ShareProperty(PasswordSharePropertyType::class, null),
+ LabelSharePropertyType::class => new ShareProperty(LabelSharePropertyType::class, null),
+ NodeGridViewSharePropertyType::class => new ShareProperty(NodeGridViewSharePropertyType::class, 'false'),
+ NodeNicknameSharePropertyType::class => new ShareProperty(NodeNicknameSharePropertyType::class, 'true'),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, false),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, false),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, true),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocMailShare')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_EMAIL)
+ ->setSharedWith('')
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_CREATE)
+ ->setAttributes(
+ (new ShareAttributes())
+ ->setAttribute('permissions', 'download', false)
+
+ ->setAttribute('fileRequest', 'enabled', true)
+ ->setAttribute('shareWith', 'emails', ['example@example.com', 'example@example.org']),
+ )
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret1)
+ ->setHideDownload(true),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses, true);
+ }
+
+ public function testFileDrop(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(TokenShareRecipientType::class, $this->sharingManager->generateSecret(), null, $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ PasswordSharePropertyType::class => new ShareProperty(PasswordSharePropertyType::class, null),
+ LabelSharePropertyType::class => new ShareProperty(LabelSharePropertyType::class, null),
+ NodeGridViewSharePropertyType::class => new ShareProperty(NodeGridViewSharePropertyType::class, 'false'),
+ NodeNicknameSharePropertyType::class => new ShareProperty(NodeNicknameSharePropertyType::class, 'false'),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, false),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, true),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocinternal')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_LINK)
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_CREATE)
+ ->setAttributes((new ShareAttributes())->setAttribute('permissions', 'download', true))
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setTarget('/' . $this->nodeFolder->getName())
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret)
+ ->setHideDownload(false),
+ ];
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses, true);
+ }
+
+ public function testRemoteSharedWithUser(): void {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(UserShareRecipientType::class, 'remote', 'https://example.com', $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocFederatedSharing')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_REMOTE)
+ ->setSharedWith('remote@https://example.com')
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setTarget('')
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses);
+ }
+
+ public function testRemoteSharedWithGroup(): void {
+ Server::get(IAppConfig::class)->setValueString('files_sharing', 'outgoing_server2server_group_share_enabled', 'yes');
+
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+ $secret = $this->sharingManager->generateSecret();
+
+ $shares = [
+ new Share(
+ $id,
+ $owner,
+ $created,
+ ShareState::Active,
+ null,
+ [new ShareSource(NodeShareSourceType::class, (string)$this->nodeFolder->getId())],
+ [new ShareRecipient(GroupShareRecipientType::class, 'remote', 'https://example.com', $secret, $owner)],
+ [
+ ExpirationDateSharePropertyType::class => new ShareProperty(ExpirationDateSharePropertyType::class, null),
+ NoteSharePropertyType::class => new ShareProperty(NoteSharePropertyType::class, null),
+ ],
+ [
+ NodeReadSharePermissionType::class => new SharePermission(NodeReadSharePermissionType::class, true),
+ NodeDownloadSharePermissionType::class => new SharePermission(NodeDownloadSharePermissionType::class, true),
+ ReshareSharePermissionType::class => new SharePermission(ReshareSharePermissionType::class, false),
+ NodeCreateSharePermissionType::class => new SharePermission(NodeCreateSharePermissionType::class, false),
+ NodeUpdateSharePermissionType::class => new SharePermission(NodeUpdateSharePermissionType::class, false),
+ NodeDeleteSharePermissionType::class => new SharePermission(NodeDeleteSharePermissionType::class, false),
+ ],
+ ),
+ ];
+
+ $legacyShares = [
+ $this->legacySharingManager->newShare()
+ ->setProviderId('ocFederatedSharing')
+ ->setNode($this->nodeFolder)
+ ->setShareType(IShare::TYPE_REMOTE_GROUP)
+ ->setSharedWith('remote@https://example.com')
+ ->setSharedBy($this->owner->getUID())
+ ->setShareOwner($this->owner->getUID())
+ ->setPermissions(Constants::PERMISSION_READ)
+ ->setStatus(IShare::STATUS_PENDING)
+ ->setTarget('')
+ ->setShareTime(DateTime::createFromImmutable($created))
+ ->setMailSend(false)
+ ->setToken($secret),
+ ];
+
+ $shareUserStatuses = [
+ [
+ $this->user1->getUID() => ShareUserStatus::Pending,
+ ],
+ ];
+
+ $this->assertSyncingWorks($shares, $legacyShares, $shareUserStatuses);
+
+ Server::get(IAppConfig::class)->deleteKey('files_sharing', 'outgoing_server2server_group_share_enabled');
+ }
+
+ // Remote share owner and shared by are explicitly not tested, because the legacy sync doesn't handle them.
+}
diff --git a/tests/lib/Sharing/TestSharePropertyTypeModifyValue.php b/tests/lib/Sharing/TestSharePropertyTypeModifyValue.php
index fd67691631bcd..9c532d680fdab 100644
--- a/tests/lib/Sharing/TestSharePropertyTypeModifyValue.php
+++ b/tests/lib/Sharing/TestSharePropertyTypeModifyValue.php
@@ -33,9 +33,9 @@ public function modifyValueOnSave(?string $oldValue, ?string $newValue): ?string
}
#[\Override]
- public function modifyValueOnLoad(?string $value): ?string {
- if ($value === 'modify-on-load') {
- return 'modified-on-load';
+ public function modifyValueOnFormat(?string $value): ?string {
+ if ($value === 'modify-on-format') {
+ return 'modified-on-format';
}
return $value;
diff --git a/tests/lib/Sharing/TestShareRecipientType1.php b/tests/lib/Sharing/TestShareRecipientType1.php
index fcc2f1ac2df75..7a8a3a57446f8 100644
--- a/tests/lib/Sharing/TestShareRecipientType1.php
+++ b/tests/lib/Sharing/TestShareRecipientType1.php
@@ -54,6 +54,11 @@ public function getRecipients(?IUser $currentUser, mixed $arguments): array {
return $this->recipientValues[$currentUser->getUID()] ?? [];
}
+ #[\Override]
+ public function getUsers(string $recipient): array {
+ return [];
+ }
+
#[\Override]
public function getRecipientDisplayName(string $recipient, ?string $instance): ?string {
if ($instance !== null) {
diff --git a/tests/lib/Sharing/TestShareRecipientTypeArguments.php b/tests/lib/Sharing/TestShareRecipientTypeArguments.php
index 211a6bfb87620..4ff2c8982109f 100644
--- a/tests/lib/Sharing/TestShareRecipientTypeArguments.php
+++ b/tests/lib/Sharing/TestShareRecipientTypeArguments.php
@@ -38,6 +38,11 @@ public function getRecipients(?IUser $currentUser, mixed $arguments): array {
return [];
}
+ #[\Override]
+ public function getUsers(string $recipient): array {
+ return [];
+ }
+
#[\Override]
public function getRecipientDisplayName(string $recipient, ?string $instance): ?string {
return null;
diff --git a/tests/lib/Sharing/TestShareRecipientTypePublicSecret.php b/tests/lib/Sharing/TestShareRecipientTypePublicSecret.php
index f9fb118bacf01..d7966f1f0439d 100644
--- a/tests/lib/Sharing/TestShareRecipientTypePublicSecret.php
+++ b/tests/lib/Sharing/TestShareRecipientTypePublicSecret.php
@@ -52,6 +52,11 @@ public function getRecipients(?IUser $currentUser, mixed $arguments): array {
return $this->recipientValues[$currentUser->getUID()] ?? [];
}
+ #[\Override]
+ public function getUsers(string $recipient): array {
+ return [];
+ }
+
#[\Override]
public function getRecipientDisplayName(string $recipient, ?string $instance): ?string {
return $this->validRecipients[$recipient];