diff --git a/apps/federatedfilesharing/lib/FederatedShareProvider.php b/apps/federatedfilesharing/lib/FederatedShareProvider.php
index 81cbd45bd9226..edca572764c0f 100644
--- a/apps/federatedfilesharing/lib/FederatedShareProvider.php
+++ b/apps/federatedfilesharing/lib/FederatedShareProvider.php
@@ -8,8 +8,10 @@
namespace OCA\FederatedFileSharing;
+use DateTime;
use OC\Share20\Exception\InvalidShare;
use OC\Share20\Share;
+use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Constants;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Federation\ICloudFederationProviderManager;
@@ -61,6 +63,7 @@ public function __construct(
private \OCP\GlobalScale\IConfig $gsConfig,
private ICloudFederationProviderManager $cloudFederationProviderManager,
private LoggerInterface $logger,
+ private ITimeFactory $timeFactory,
) {
}
@@ -135,7 +138,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() ?? \DateTime::createFromImmutable($this->timeFactory->now()));
[$token, $remoteId] = $this->notifications->requestReShare(
$remoteShare['refresh_token'],
$remoteShare['remote_id'],
@@ -170,7 +173,11 @@ public function create(IShare $share): IShare {
* @throws \Exception
*/
protected function createFederatedShare(IShare $share): string {
- $token = $this->tokenHandler->generateToken();
+ $token = $share->getToken();
+ if ($token === null || $token === '') {
+ $token = $this->tokenHandler->generateToken();
+ }
+
$shareId = $this->addShareToDB(
$share->getNodeId(),
$share->getNodeType(),
@@ -180,7 +187,8 @@ protected function createFederatedShare(IShare $share): string {
$share->getPermissions(),
$token,
$share->getShareType(),
- $share->getExpirationDate()
+ $share->getExpirationDate(),
+ $share->getShareTime() ?? \DateTime::createFromImmutable($this->timeFactory->now()),
);
$failure = false;
@@ -261,7 +269,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 +282,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/federatedfilesharing/tests/FederatedShareProviderReshareRemoteTest.php b/apps/federatedfilesharing/tests/FederatedShareProviderReshareRemoteTest.php
index fff9fb23d4801..adfa86767609c 100644
--- a/apps/federatedfilesharing/tests/FederatedShareProviderReshareRemoteTest.php
+++ b/apps/federatedfilesharing/tests/FederatedShareProviderReshareRemoteTest.php
@@ -15,6 +15,7 @@
use OCA\FederatedFileSharing\FederatedShareProvider;
use OCA\FederatedFileSharing\Notifications;
use OCA\FederatedFileSharing\TokenHandler;
+use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Constants;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IExpressionBuilder;
@@ -28,6 +29,7 @@
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IUserManager;
+use OCP\Server;
use OCP\Share\IShare;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
@@ -76,6 +78,7 @@ protected function setUp(): void {
$this->gsConfig,
$this->cloudFederationProviderManager,
$this->logger,
+ Server::get(ITimeFactory::class),
);
}
diff --git a/apps/federatedfilesharing/tests/FederatedShareProviderTest.php b/apps/federatedfilesharing/tests/FederatedShareProviderTest.php
index 8b99954cf856b..f27114c2782fd 100644
--- a/apps/federatedfilesharing/tests/FederatedShareProviderTest.php
+++ b/apps/federatedfilesharing/tests/FederatedShareProviderTest.php
@@ -15,6 +15,7 @@
use OCA\FederatedFileSharing\FederatedShareProvider;
use OCA\FederatedFileSharing\Notifications;
use OCA\FederatedFileSharing\TokenHandler;
+use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Constants;
use OCP\Contacts\IManager as IContactsManager;
use OCP\EventDispatcher\IEventDispatcher;
@@ -101,6 +102,7 @@ protected function setUp(): void {
$this->gsConfig,
$this->cloudFederationProviderManager,
$this->logger,
+ Server::get(ITimeFactory::class),
);
$this->shareManager = Server::get(IManager::class);
@@ -417,6 +419,7 @@ public function testUpdate(string $owner, string $sharedBy, ?\DateTime $expirati
$this->gsConfig,
$this->cloudFederationProviderManager,
$this->logger,
+ Server::get(ITimeFactory::class),
]
)
->onlyMethods(['sendPermissionUpdate'])
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 3a778805d545b..4358bc30a3a0b 100644
--- a/apps/files/composer/composer/autoload_classmap.php
+++ b/apps/files/composer/composer/autoload_classmap.php
@@ -87,6 +87,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',
@@ -107,6 +108,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 c92f1b55ff3df..94198591a7a04 100644
--- a/apps/files/composer/composer/autoload_static.php
+++ b/apps/files/composer/composer/autoload_static.php
@@ -102,6 +102,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',
@@ -122,6 +123,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..fbfa8bd2739d6
--- /dev/null
+++ b/apps/files/lib/Migration/Version35000Date20260910082613.php
@@ -0,0 +1,38 @@
+createTable('sharing_source_node_target');
+ $sourceNodeTargetTable->addColumn('user_id', Types::STRING, ['length' => 64]);
+ // Can't add a FK on sharing_instance_map here, because that table is created by the sharing app.
+ $sourceNodeTargetTable->addColumn('source_instance_id', Types::INTEGER, ['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_id', '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..fadc6924b5dd6
--- /dev/null
+++ b/apps/files/lib/Sharing/SharesUpdatedListener.php
@@ -0,0 +1,75 @@
+
+ */
+final readonly class SharesUpdatedListener implements IEventListener {
+ public function __construct(
+ private ISharingRegistry $registry,
+ private SourceNodeTargetManager $sourceNodeTargetManager,
+ private IRootFolder $rootFolder,
+ ) {
+ }
+
+ #[Override]
+ public function handle(Event $event): void {
+ foreach ($event->getShares() as $share) {
+ if ($share->state !== ShareState::Active) {
+ continue;
+ }
+
+ $ownerUserFolder = $this->rootFolder->getUserFolder($share->owner->userId);
+
+ foreach ($share->sources as $source) {
+ if ($source->class !== NodeShareSourceType::class) {
+ continue;
+ }
+
+ $sourceNode = $ownerUserFolder->getFirstNodeById((int)$source->value);
+ if (!$sourceNode instanceof Node) {
+ throw new NotFoundException();
+ }
+
+ /** @var array */
+ $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 $user) {
+ $userIds[$user->getUID()] = true;
+ }
+ }
+
+ // Casting is needed, because using the user ID as the array key could convert it to an integer if it is numeric.
+ $this->sourceNodeTargetManager->getTargets(array_map(static fn (int|string $userId): string => (string)$userId, array_keys($userIds)), $share->owner, $sourceNode);
+ }
+ }
+ }
+}
diff --git a/apps/files/lib/Sharing/SourceNodeTargetManager.php b/apps/files/lib/Sharing/SourceNodeTargetManager.php
new file mode 100644
index 0000000000000..6047864e3756b
--- /dev/null
+++ b/apps/files/lib/Sharing/SourceNodeTargetManager.php
@@ -0,0 +1,141 @@
+ $userIds
+ * @return array
+ */
+ private function getTargetsInternal(array $userIds, ShareUser $owner, Node $sourceNode): 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_id')
+ : $qb->expr()->eq('source_instance_id', $qb->createNamedParameter($this->instanceMapper->getInstanceId($owner->instance), IQueryBuilder::PARAM_INT))
+ )
+ ->andWhere($qb->expr()->eq('source_node_id', $qb->createNamedParameter($sourceNode->getId(), 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, Node $sourceNode): string {
+ if ($owner->instance !== null) {
+ throw new RuntimeException('Federation is not supported yet.');
+ }
+
+ $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 . '/' . $sourceNode->getName());
+
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb
+ ->insert('sharing_source_node_target')
+ ->values([
+ 'user_id' => $qb->createNamedParameter($userId),
+ 'source_instance_id' => $qb->createNamedParameter($this->instanceMapper->getInstanceId($owner->instance), IQueryBuilder::PARAM_INT),
+ 'source_node_id' => $qb->createNamedParameter($sourceNode->getId()),
+ 'target' => $qb->createNamedParameter($target),
+ ])
+ ->executeStatement();
+
+ return $target;
+ }
+
+ public function getTarget(string $userId, ShareUser $owner, Node $sourceNode): ?string {
+ return $this->getTargetsInternal([$userId], $owner, $sourceNode)[$userId] ?? null;
+ }
+
+ /**
+ * @param list $userIds
+ * @return array
+ */
+ public function getTargets(array $userIds, ShareUser $owner, Node $sourceNode): array {
+ $targets = $this->getTargetsInternal($userIds, $owner, $sourceNode);
+ foreach ($userIds as $userId) {
+ if (isset($targets[$userId])) {
+ continue;
+ }
+
+ $targets[$userId] = $this->createDefaultTarget($userId, $owner, $sourceNode);
+ }
+
+ return $targets;
+ }
+
+ public function setTarget(string $userId, ShareUser $owner, Node $sourceNode, 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_id')
+ : $qb->expr()->eq('source_instance_id', $qb->createNamedParameter($this->instanceMapper->getInstanceId($owner->instance), IQueryBuilder::PARAM_INT))
+ )
+ ->andWhere($qb->expr()->eq('source_node_id', $qb->createNamedParameter($sourceNode->getId(), 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_id' => $qb->createNamedParameter($this->instanceMapper->getInstanceId($owner->instance), IQueryBuilder::PARAM_INT),
+ 'source_node_id' => $qb->createNamedParameter($sourceNode->getId()),
+ '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..aa55fb78874a4
--- /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]);
+ }
+
+ 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..5fd7393fba239
--- /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));
+ }
+
+ 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));
+
+ $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));
+
+ $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));
+
+ $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));
+
+ $this->manager->setTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node, '/bar.txt');
+ $this->assertEquals('/bar.txt', $this->manager->getTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node));
+
+ $this->manager->setTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node, '/baz.txt');
+ $this->assertEquals('/baz.txt', $this->manager->getTarget($this->user->getUID(), new ShareUser($this->owner->getUID(), null), $this->node));
+ }
+}
diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php
index 7cf5a235892c8..7006e0b328e60 100644
--- a/apps/files_sharing/lib/Controller/ShareAPIController.php
+++ b/apps/files_sharing/lib/Controller/ShareAPIController.php
@@ -705,6 +705,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);
@@ -1384,6 +1385,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 0e7fe075d6063..04f8810db7bfe 100644
--- a/apps/sharing/composer/composer/autoload_classmap.php
+++ b/apps/sharing/composer/composer/autoload_classmap.php
@@ -34,5 +34,6 @@
'OCA\\Sharing\\Migration\\Version1000Date20260826122228' => $baseDir . '/../lib/Migration/Version1000Date20260826122228.php',
'OCA\\Sharing\\Migration\\Version2000Date20261013142538' => $baseDir . '/../lib/Migration/Version2000Date20261013142538.php',
'OCA\\Sharing\\Migration\\Version2000Date20261014182648' => $baseDir . '/../lib/Migration/Version2000Date20261014182648.php',
+ 'OCA\\Sharing\\Migration\\Version2000Date20261101154320' => $baseDir . '/../lib/Migration/Version2000Date20261101154320.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 79b905c629927..2152bc1a34902 100644
--- a/apps/sharing/composer/composer/autoload_static.php
+++ b/apps/sharing/composer/composer/autoload_static.php
@@ -49,6 +49,7 @@ class ComposerStaticInitSharing
'OCA\\Sharing\\Migration\\Version1000Date20260826122228' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20260826122228.php',
'OCA\\Sharing\\Migration\\Version2000Date20261013142538' => __DIR__ . '/..' . '/../lib/Migration/Version2000Date20261013142538.php',
'OCA\\Sharing\\Migration\\Version2000Date20261014182648' => __DIR__ . '/..' . '/../lib/Migration/Version2000Date20261014182648.php',
+ 'OCA\\Sharing\\Migration\\Version2000Date20261101154320' => __DIR__ . '/..' . '/../lib/Migration/Version2000Date20261101154320.php',
'OCA\\Sharing\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
);
diff --git a/apps/sharing/lib/Command/AddShareRecipient.php b/apps/sharing/lib/Command/AddShareRecipient.php
index e45a347943a1d..94899abcb9f32 100644
--- a/apps/sharing/lib/Command/AddShareRecipient.php
+++ b/apps/sharing/lib/Command/AddShareRecipient.php
@@ -31,7 +31,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
/** @var class-string $class */
$class = $input->getArgument('class');
diff --git a/apps/sharing/lib/Command/AddShareSource.php b/apps/sharing/lib/Command/AddShareSource.php
index 2a4b37aa8e741..82e7ea492bb3c 100644
--- a/apps/sharing/lib/Command/AddShareSource.php
+++ b/apps/sharing/lib/Command/AddShareSource.php
@@ -30,7 +30,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
/** @var class-string $class */
$class = $input->getArgument('class');
diff --git a/apps/sharing/lib/Command/DeleteShare.php b/apps/sharing/lib/Command/DeleteShare.php
index d921b9a4ad296..c37697899419c 100644
--- a/apps/sharing/lib/Command/DeleteShare.php
+++ b/apps/sharing/lib/Command/DeleteShare.php
@@ -29,7 +29,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
$this->applyActor($input);
diff --git a/apps/sharing/lib/Command/GetShare.php b/apps/sharing/lib/Command/GetShare.php
index 1f27891b6a7e8..fb1ddf9f99d15 100644
--- a/apps/sharing/lib/Command/GetShare.php
+++ b/apps/sharing/lib/Command/GetShare.php
@@ -26,7 +26,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
return $this->wrapExecution($input, $output, fn (): Share => $this->manager->getShare($this->accessContext, $id));
diff --git a/apps/sharing/lib/Command/GetShares.php b/apps/sharing/lib/Command/GetShares.php
index 01fafd9ef741b..9b0097d449237 100644
--- a/apps/sharing/lib/Command/GetShares.php
+++ b/apps/sharing/lib/Command/GetShares.php
@@ -66,7 +66,7 @@ public function execute(InputInterface $input, OutputInterface $output): int {
}
}
- /** @var ?string $lastShareID */
+ /** @var ?non-empty-string $lastShareID */
$lastShareID = $input->getOption('last-share-id');
/** @var ?string $limit */
$limit = $input->getOption('limit');
diff --git a/apps/sharing/lib/Command/RemoveShareRecipient.php b/apps/sharing/lib/Command/RemoveShareRecipient.php
index 50873cfb07207..1227087263df0 100644
--- a/apps/sharing/lib/Command/RemoveShareRecipient.php
+++ b/apps/sharing/lib/Command/RemoveShareRecipient.php
@@ -31,7 +31,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
/** @var class-string $class */
$class = $input->getArgument('class');
diff --git a/apps/sharing/lib/Command/RemoveShareSource.php b/apps/sharing/lib/Command/RemoveShareSource.php
index 732d73176fc99..445cd0646f844 100644
--- a/apps/sharing/lib/Command/RemoveShareSource.php
+++ b/apps/sharing/lib/Command/RemoveShareSource.php
@@ -30,7 +30,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
/** @var class-string $class */
$class = $input->getArgument('class');
diff --git a/apps/sharing/lib/Command/SelectSharePermissionPreset.php b/apps/sharing/lib/Command/SelectSharePermissionPreset.php
index 6932961656368..bb21050d0676f 100644
--- a/apps/sharing/lib/Command/SelectSharePermissionPreset.php
+++ b/apps/sharing/lib/Command/SelectSharePermissionPreset.php
@@ -28,7 +28,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
/** @var class-string $permissionPresetClass */
$permissionPresetClass = $input->getArgument('permission-preset');
diff --git a/apps/sharing/lib/Command/UpdateSharePermission.php b/apps/sharing/lib/Command/UpdateSharePermission.php
index d7b0067715269..a02ecb48fceca 100644
--- a/apps/sharing/lib/Command/UpdateSharePermission.php
+++ b/apps/sharing/lib/Command/UpdateSharePermission.php
@@ -30,7 +30,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
/** @var class-string $class */
$class = $input->getArgument('class');
diff --git a/apps/sharing/lib/Command/UpdateShareProperty.php b/apps/sharing/lib/Command/UpdateShareProperty.php
index b197355e5037a..fc0f40689f989 100644
--- a/apps/sharing/lib/Command/UpdateShareProperty.php
+++ b/apps/sharing/lib/Command/UpdateShareProperty.php
@@ -30,7 +30,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
/** @var class-string $class */
$class = $input->getArgument('class');
diff --git a/apps/sharing/lib/Command/UpdateShareRecipientPermission.php b/apps/sharing/lib/Command/UpdateShareRecipientPermission.php
index 09e7c7849c580..456d6c06eef4e 100644
--- a/apps/sharing/lib/Command/UpdateShareRecipientPermission.php
+++ b/apps/sharing/lib/Command/UpdateShareRecipientPermission.php
@@ -35,7 +35,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
/** @var class-string $permissionClass */
$permissionClass = $input->getArgument('permission-class');
diff --git a/apps/sharing/lib/Command/UpdateShareRecipientSecret.php b/apps/sharing/lib/Command/UpdateShareRecipientSecret.php
index bd3e0998640c7..66a326de07087 100644
--- a/apps/sharing/lib/Command/UpdateShareRecipientSecret.php
+++ b/apps/sharing/lib/Command/UpdateShareRecipientSecret.php
@@ -32,7 +32,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
/** @var class-string $class */
$class = $input->getArgument('class');
diff --git a/apps/sharing/lib/Command/UpdateShareState.php b/apps/sharing/lib/Command/UpdateShareState.php
index ac34b885664a3..1b34b8661d131 100644
--- a/apps/sharing/lib/Command/UpdateShareState.php
+++ b/apps/sharing/lib/Command/UpdateShareState.php
@@ -28,7 +28,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
/** @var string $state */
$state = $input->getArgument('state');
diff --git a/apps/sharing/lib/Command/UpdateShareUserStatus.php b/apps/sharing/lib/Command/UpdateShareUserStatus.php
index 9dd7d719f9d42..66d53903e765d 100644
--- a/apps/sharing/lib/Command/UpdateShareUserStatus.php
+++ b/apps/sharing/lib/Command/UpdateShareUserStatus.php
@@ -28,7 +28,7 @@ public function configure(): void {
#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
- /** @var string $id */
+ /** @var non-empty-string $id */
$id = $input->getArgument('id');
/** @var string $userStatus */
$userStatus = $input->getArgument('user-status');
diff --git a/apps/sharing/lib/Controller/ApiV1Controller.php b/apps/sharing/lib/Controller/ApiV1Controller.php
index 1e5b4d42ad3b8..06e9759d8e64a 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
@@ -83,11 +81,11 @@ public function __construct(
/**
* Search for recipients that can be added to a share.
*
- * @param ?list> $filterRecipientTypeClasses Type classes of recipients to filter by
+ * @param ?non-empty-list> $filterRecipientTypeClasses Type classes of recipients to filter by
* @param string $query The query to search for, if the query is empty, recommended recipients will be returned
* @param int<1, 100> $limit The maximum number of participants
* @param non-negative-int $offset The offset of the participants
- * @param ?string $id If provided, recipients that are already part of the share will not be returned.
+ * @param ?non-empty-string $id If provided, recipients that are already part of the share will not be returned.
* @return DataResponse, array{Link?: string}>|DataResponse
*
* 200: Recipients returned
@@ -185,7 +183,7 @@ public function createShare(): DataResponse {
/**
* Update the state of a share.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @param SharingState $state New state of the share
* @return DataResponse|DataResponse
*
@@ -226,7 +224,7 @@ public function updateShareState(string $id, string $state): DataResponse {
/**
* Update the user status for a share.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @param SharingUserStatus $userStatus New user status for the share
* @return DataResponse|DataResponse
*
@@ -264,7 +262,7 @@ public function updateShareUserStatus(string $id, string $userStatus): DataRespo
/**
* Add a new source to a share.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @param class-string $class Type class of the source
* @param non-empty-string $value Value of the source
* @return DataResponse|DataResponse
@@ -302,7 +300,7 @@ public function addShareSource(string $id, string $class, string $value): DataRe
/**
* Remove an existing source from a share.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @param class-string $class Type class of the source
* @param non-empty-string $value Value of the source
* @return DataResponse|DataResponse
@@ -337,7 +335,7 @@ public function removeShareSource(string $id, string $class, string $value): Dat
/**
* Add a new recipient to a share.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @param class-string $class Type class of the recipient
* @param non-empty-string $value Value of the recipient
* @param ?non-empty-string $instance Instance of the recipient
@@ -376,7 +374,7 @@ public function addShareRecipient(string $id, string $class, string $value, ?str
/**
* Remove an existing recipient from a share.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @param class-string $class Type class of the recipient
* @param non-empty-string $value Value of the recipient
* @param ?non-empty-string $instance Instance of the recipient
@@ -412,7 +410,7 @@ public function removeShareRecipient(string $id, string $class, string $value, ?
/**
* Update the secret of a recipient.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @param class-string $class Type class of the recipient
* @param non-empty-string $value Value of the recipient
* @param ?non-empty-string $instance Instance of the recipient
@@ -452,7 +450,7 @@ public function updateShareRecipientSecret(string $id, string $class, string $va
/**
* Update a property of a share.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @param class-string $class Type class of the property
* @param ?string $value Value of the property
* @return DataResponse|DataResponse
@@ -490,7 +488,7 @@ public function updateShareProperty(string $id, string $class, ?string $value):
/**
* Update a permission of a share.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @param class-string $class Type class of the permission
* @param bool $enabled Enabled state of the permission
* @return DataResponse|DataResponse
@@ -528,7 +526,7 @@ public function updateSharePermission(string $id, string $class, bool $enabled):
/**
* Update a permission for a recipient of a share.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @param class-string $recipientClass Type class of the recipient
* @param non-empty-string $recipientValue Value of the recipient
* @param ?non-empty-string $recipientInstance Instance of the recipient
@@ -566,7 +564,7 @@ public function updateShareRecipientPermission(string $id, string $recipientClas
/**
* Select a permission preset for a share.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @param class-string $permissionPresetClass New permission preset of the share
* @return DataResponse|DataResponse
*
@@ -601,7 +599,7 @@ public function selectSharePermissionPreset(string $id, string $permissionPreset
/**
* Delete a share.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @return DataResponse, array{}>|DataResponse
*
* 204: Share deleted
@@ -634,7 +632,7 @@ public function deleteShare(string $id): DataResponse {
/**
* Get a share.
*
- * @param string $id ID of the share
+ * @param non-empty-string $id ID of the share
* @param ?string $secret Secret of the share
* @param array, mixed> $arguments Arguments for accessing the share
* @return DataResponse|DataResponse
@@ -675,7 +673,7 @@ public function getShare(string $id, ?string $secret = null, array $arguments =
* @param ?non-empty-string $filterSourceTypeValue Source type value to filter by.
* @param ?SharingState $filterState State to filter by.
* @param ?SharingUserStatus $filterUserStatus User status to filter by.
- * @param ?string $lastShareID The ID of the previous share. This is used as an offset and only shares with higher IDs are returned.
+ * @param ?non-empty-string $lastShareID The ID of the previous share. This is used as an offset and only shares with higher IDs are returned.
* @param int<1, 100> $limit The number of shares to return.
* @return DataResponse, array{}>|DataResponse
*
diff --git a/apps/sharing/lib/Migration/Version2000Date20261101154320.php b/apps/sharing/lib/Migration/Version2000Date20261101154320.php
new file mode 100644
index 0000000000000..4f330c65e0a3d
--- /dev/null
+++ b/apps/sharing/lib/Migration/Version2000Date20261101154320.php
@@ -0,0 +1,39 @@
+getTable('sharing_classmap');
+
+ $legacyMappingTable = $schema->createTable('sharing_share_legacy_mapping');
+ $legacyMappingTable->addColumn('id', Types::BIGINT);
+ $legacyMappingTable->addColumn('legacy_provider_class_id', Types::INTEGER);
+ $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_class_id', 'legacy_id']);
+ $legacyMappingTable->addIndex(['id']);
+ $legacyMappingTable->addForeignKeyConstraint($mappingTable->getName(), ['legacy_provider_class_id'], ['class_id']);
+
+ return $schema;
+ }
+}
diff --git a/apps/sharing/openapi.json b/apps/sharing/openapi.json
index 0a70734e06617..ed841886e9bd3 100644
--- a/apps/sharing/openapi.json
+++ b/apps/sharing/openapi.json
@@ -649,7 +649,8 @@
"items": {
"type": "string",
"minLength": 1
- }
+ },
+ "minItems": 1
}
},
{
@@ -691,7 +692,8 @@
"schema": {
"type": "string",
"nullable": true,
- "default": null
+ "default": null,
+ "minLength": 1
}
},
{
@@ -1029,7 +1031,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -1236,7 +1239,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -1420,7 +1424,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -1606,7 +1611,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -1816,7 +1822,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -2002,7 +2009,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -2228,7 +2236,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -2441,7 +2450,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -2654,7 +2664,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -2885,7 +2896,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -3063,7 +3075,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -3251,7 +3264,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -3407,7 +3421,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -3553,7 +3568,8 @@
"description": "The ID of the previous share. This is used as an offset and only shares with higher IDs are returned.",
"schema": {
"type": "string",
- "nullable": true
+ "nullable": true,
+ "minLength": 1
}
},
{
diff --git a/apps/sharing/tests/Controller/ApiV1ControllerTest.php b/apps/sharing/tests/Controller/ApiV1ControllerTest.php
index d439badd0a81a..03b7093e26113 100644
--- a/apps/sharing/tests/Controller/ApiV1ControllerTest.php
+++ b/apps/sharing/tests/Controller/ApiV1ControllerTest.php
@@ -149,7 +149,6 @@ protected function removeShareRecipient(ShareAccessContext $accessContext, Share
#[Override]
protected function updateShareRecipientSecret(ShareAccessContext $accessContext, Share $share, ShareRecipient $recipient, string $secret): array {
- /** @psalm-suppress ArgumentTypeCoercion */
/** @var SharingShare */
return $this->executeRequest($accessContext, fn (ApiV1Controller $controller): DataResponse => $controller->updateShareRecipientSecret($share->id, $recipient->class, $recipient->value, $recipient->instance, $secret));
}
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..7bd1a75ad2d10 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_values($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..d7ed51df90c55 100644
--- a/core/Sharing/Recipient/TeamShareRecipientType.php
+++ b/core/Sharing/Recipient/TeamShareRecipientType.php
@@ -25,6 +25,7 @@
use OCP\Interaction\InteractionReceiver;
use OCP\Interaction\Receivers\CircleReceiver;
use OCP\IUser;
+use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Server;
use OCP\Share\IShare;
@@ -41,6 +42,7 @@ public function __construct(
IEventDispatcher $eventDispatcher,
private readonly IDBConnection $dbConnection,
private readonly ISharingManager $manager,
+ private readonly IUserManager $userManager,
) {
$eventDispatcher->addServiceListener(DestroyingCircleEvent::class, self::class);
}
@@ -69,6 +71,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_map($this->userManager->getExistingUser(...), 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..e4ad7efa91af7 100644
--- a/core/Sharing/Recipient/UserShareRecipientType.php
+++ b/core/Sharing/Recipient/UserShareRecipientType.php
@@ -63,6 +63,14 @@ public function getRecipients(?IUser $currentUser, mixed $arguments): array {
return [$currentUser->getUID()];
}
+ /**
+ * @return list
+ */
+ #[\Override]
+ public function getUsers(string $recipient): array {
+ return [$this->userManager->getExistingUser($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 358118e8d059e..bd5c7f98c691c 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -46,6 +46,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',
@@ -953,7 +955,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',
@@ -2400,7 +2404,10 @@
'OC\\Sharing\\AbstractStringMapper' => $baseDir . '/lib/private/Sharing/AbstractStringMapper.php',
'OC\\Sharing\\ClassMapper' => $baseDir . '/lib/private/Sharing/ClassMapper.php',
'OC\\Sharing\\InstanceMapper' => $baseDir . '/lib/private/Sharing/InstanceMapper.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 5caaf52478f18..faf6c0d5a694c 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -87,6 +87,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',
@@ -994,7 +996,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',
@@ -2441,7 +2445,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Sharing\\AbstractStringMapper' => __DIR__ . '/../../..' . '/lib/private/Sharing/AbstractStringMapper.php',
'OC\\Sharing\\ClassMapper' => __DIR__ . '/../../..' . '/lib/private/Sharing/ClassMapper.php',
'OC\\Sharing\\InstanceMapper' => __DIR__ . '/../../..' . '/lib/private/Sharing/InstanceMapper.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 042f98529b3fd..7792a6613622b 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)) {
@@ -772,6 +775,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',
@@ -825,6 +832,7 @@ public function acceptShare(IShare $share, string $recipientId): IShare {
$event = new ShareAcceptedEvent($share);
$this->dispatchEvent($event, 'share accepted');
+ $share = $event->getShare();
return $share;
}
@@ -1090,7 +1098,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]
@@ -1121,11 +1135,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]
@@ -1511,6 +1527,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..279f6b29ebe22
--- /dev/null
+++ b/lib/private/Sharing/LegacyMapper.php
@@ -0,0 +1,163 @@
+ $legacyProviderClass
+ * @param non-empty-list $recipientSecrets
+ */
+ public function createLegacyMapping(string $id, string $legacyProviderClass, 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_class_id' => $qb->createNamedParameter($this->classMapper->getClassId($legacyProviderClass), IQueryBuilder::PARAM_INT),
+ '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,
+ $legacyProviderClass,
+ $legacyId,
+ $lastUpdated,
+ $recipientSecrets,
+ $recipientValue,
+ );
+ }
+
+ /**
+ * @param class-string $legacyProviderClass
+ */
+ public function getLegacyMappingByLegacyProviderClassAndId(string $legacyProviderClass, 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_class_id', $qb->createNamedParameter($this->classMapper->getClassId($legacyProviderClass), IQueryBuilder::PARAM_INT)))
+ ->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'],
+ $legacyProviderClass,
+ $legacyId,
+ SharingBackend::parseTimestamp((string)$row['last_updated']),
+ $this->decodeRecipientSecrets($row['recipient_secrets']),
+ $row['recipient_value'],
+ );
+ }
+
+ /**
+ * @param non-empty-string $id
+ * @return list
+ */
+ public function getLegacyMappings(string $id): array {
+ $legacyMappings = [];
+
+ $qb = $this->dbConnection->getQueryBuilder();
+ $result = $qb
+ ->select('id', 'legacy_provider_class_id', '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) {
+ /** @var class-string $legacyProviderClass */
+ $legacyProviderClass = $this->classMapper->getClassName((int)$row['legacy_provider_class_id']);
+ $legacyMappings[] = new LegacyMapping(
+ (string)$row['id'],
+ $legacyProviderClass,
+ $row['legacy_id'],
+ SharingBackend::parseTimestamp((string)$row['last_updated']),
+ $this->decodeRecipientSecrets($row['recipient_secrets']),
+ $row['recipient_value'],
+ );
+ }
+
+ return $legacyMappings;
+ }
+
+ /**
+ * @param non-empty-string $id
+ */
+ 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();
+ }
+
+ /**
+ * @param class-string $legacyProviderClass
+ */
+ public function deleteLegacyMapping(string $legacyProviderClass, int $legacyId): void {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb
+ ->delete('sharing_share_legacy_mapping')
+ ->where($qb->expr()->eq('legacy_provider_class_id', $qb->createNamedParameter($this->classMapper->getClassId($legacyProviderClass), IQueryBuilder::PARAM_INT)))
+ ->andWhere($qb->expr()->eq('legacy_id', $qb->createNamedParameter($legacyId, IQueryBuilder::PARAM_INT)))
+ ->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_class_id', $qb->createNamedParameter($this->classMapper->getClassId($legacyMapping->legacyProviderClass), IQueryBuilder::PARAM_INT)))
+ ->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..aa00856587eed
--- /dev/null
+++ b/lib/private/Sharing/LegacyMapping.php
@@ -0,0 +1,42 @@
+ $legacyProviderClass */
+ public string $legacyProviderClass,
+ public int $legacyId,
+ public DateTimeImmutable $lastUpdated,
+ /**
+ * A single legacy share can correspond to multiple share recipients, so we have to save the secret for all of them.
+ * @var non-empty-list $recipientSecrets
+ */
+ public array $recipientSecrets,
+ public string $recipientValue,
+ ) {
+ }
+
+ /**
+ * @throws ShareNotFound
+ */
+ public function getLegacyShare(): IShare {
+ // Not cached on purpose, so we always get the current state of the share.
+ return Server::get($this->legacyProviderClass)->getShareById((string)$this->legacyId);
+ }
+}
diff --git a/lib/private/Sharing/SharingBackend.php b/lib/private/Sharing/SharingBackend.php
index 17d4122975827..2d525658e6459 100644
--- a/lib/private/Sharing/SharingBackend.php
+++ b/lib/private/Sharing/SharingBackend.php
@@ -19,7 +19,6 @@
use NCU\Sharing\Permission\SharePermission;
use NCU\Sharing\Property\ISharePropertyType;
use NCU\Sharing\Property\ISharePropertyTypeFilter;
-use NCU\Sharing\Property\ISharePropertyTypeModifyValue;
use NCU\Sharing\Property\ShareProperty;
use NCU\Sharing\Recipient\IShareRecipientType;
use NCU\Sharing\Recipient\ShareRecipient;
@@ -94,7 +93,7 @@ public function onOwnerDeleted(ShareUser $owner): array {
$result = $qb->executeQuery();
- /** @var list $ids */
+ /** @var list $ids */
$ids = $result->fetchFirstColumn();
if ($ids === []) {
return [];
@@ -206,7 +205,7 @@ public function onSourceDeleted(ShareSource $source): array {
->andWhere($qb->expr()->eq('source_value', $qb->createNamedParameter($source->value)))
->executeQuery();
- /** @var list $ids */
+ /** @var list $ids */
$ids = $result->fetchFirstColumn();
if ($ids === []) {
return [];
@@ -300,7 +299,7 @@ public function onRecipientDeleted(ShareRecipient $recipient): array {
->andWhere($this->isNullOrEqual($qb, 'recipient_instance_id', $this->instanceMapper->getInstanceId($recipient->instance)))
->executeQuery();
- /** @var list $ids */
+ /** @var list $ids */
$ids = $result->fetchFirstColumn();
if ($ids === []) {
return [];
@@ -334,7 +333,7 @@ public function onInitiatorDeleted(ShareUser $initiator): array {
$result = $qb->executeQuery();
- /** @var list $ids */
+ /** @var list $ids */
$ids = $result->fetchFirstColumn();
if ($ids === []) {
return [];
@@ -381,38 +380,13 @@ public function updateShareRecipientSecret(string $id, ShareRecipient $recipient
}
#[\Override]
- public function updateShareProperty(string $id, ShareProperty $property): ?string {
+ public function updateShareProperty(string $id, ShareProperty $property): void {
$this->assertInTransaction();
- $value = $property->value;
-
- $propertyType = $this->registry->getPropertyTypes()[$property->class];
-
- if ($propertyType instanceof ISharePropertyTypeModifyValue) {
- $qb = $this->connection->getQueryBuilder();
- $qb
- ->select('sp.property_value')
- ->from('sharing_share_properties', 'sp')
- ->where($qb->expr()->eq('sp.share_id', $qb->createNamedParameter($id)))
- ->andWhere(
- $qb->expr()->eq(
- 'sp.property_class_id', $qb->createNamedParameter($this->classMapper->getClassId($property->class), IQueryBuilder::PARAM_INT)
- )
- );
-
- /** @var string|false $oldValue */
- $oldValue = $qb->executeQuery()->fetchOne();
- if ($oldValue === false) {
- $oldValue = null;
- }
-
- $value = $propertyType->modifyValueOnSave($oldValue, $property->value);
- }
-
$qb = $this->connection->getQueryBuilder();
$rowCount = $qb
->update('sharing_share_properties')
- ->set('property_value', $qb->createNamedParameter($value))
+ ->set('property_value', $qb->createNamedParameter($property->value))
->where($qb->expr()->eq('share_id', $qb->createNamedParameter($id)))
->andWhere(
$qb->expr()->eq('property_class_id', $qb->createNamedParameter($this->classMapper->getClassId($property->class), IQueryBuilder::PARAM_INT))
@@ -425,10 +399,30 @@ public function updateShareProperty(string $id, ShareProperty $property): ?strin
->values([
'share_id' => $qb->createNamedParameter($id),
'property_class_id' => $qb->createNamedParameter($this->classMapper->getClassId($property->class), IQueryBuilder::PARAM_INT),
- 'property_value' => $qb->createNamedParameter($value),
+ 'property_value' => $qb->createNamedParameter($property->value),
])
->executeStatement();
}
+ }
+
+ #[\Override]
+ public function getSharePropertyValue(string $id, string $propertyTypeClass): ?string {
+ $qb = $this->connection->getQueryBuilder();
+ $qb
+ ->select('sp.property_value')
+ ->from('sharing_share_properties', 'sp')
+ ->where($qb->expr()->eq('sp.share_id', $qb->createNamedParameter($id)))
+ ->andWhere(
+ $qb->expr()->eq(
+ 'sp.property_class_id', $qb->createNamedParameter($this->classMapper->getClassId($propertyTypeClass), IQueryBuilder::PARAM_INT)
+ )
+ );
+
+ /** @var string|false $value */
+ $value = $qb->executeQuery()->fetchOne();
+ if ($value === false) {
+ return null;
+ }
return $value;
}
@@ -550,7 +544,7 @@ public function deleteShare(string $id): void {
public function getShare(ShareAccessContext $accessContext, string $id): Share {
$this->assertInTransaction();
- $shares = $this->list($accessContext, $id, null, null, null, null, null, null);
+ $shares = $this->list($accessContext, [$id], null, null, null, null, null, null);
if (count($shares) !== 1) {
throw new ShareNotFoundException();
}
@@ -561,6 +555,7 @@ public function getShare(ShareAccessContext $accessContext, string $id): Share {
#[\Override]
public function getShares(
ShareAccessContext $accessContext,
+ ?array $filterShareIDs,
?string $filterSourceTypeClass,
?string $filterSourceTypeValue,
?ShareState $filterState,
@@ -570,22 +565,7 @@ public function getShares(
): array {
$this->assertInTransaction();
- return $this->list($accessContext, null, $filterSourceTypeClass, $filterSourceTypeValue, $filterState, $filterUserStatus, $lastShareID, $limit);
- }
-
- #[\Override]
- public function hasShare(string $id): bool {
- $this->assertInTransaction();
-
- $qb = $this->connection->getQueryBuilder();
-
- $result = $qb
- ->select('id')
- ->from('sharing_share')
- ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
- ->executeQuery();
-
- return $result->fetchOne() !== false;
+ return $this->list($accessContext, $filterShareIDs, $filterSourceTypeClass, $filterSourceTypeValue, $filterState, $filterUserStatus, $lastShareID, $limit);
}
#[\Override]
@@ -638,13 +618,14 @@ private function hideDisabledUserShares(): bool {
// TODO: Split up the method
/**
+ * @param ?non-empty-list $filterShareIDs
* @param ?class-string $filterSourceTypeClass
* @param ?non-empty-string $filterSourceTypeValue
* @return list
*/
private function list(
ShareAccessContext $accessContext,
- ?string $filterShareID,
+ ?array $filterShareIDs,
?string $filterSourceTypeClass,
?string $filterSourceTypeValue,
?ShareState $filterState,
@@ -672,7 +653,7 @@ private function list(
if ($accessContext->currentUser instanceof IUser) {
$qb = $this->connection->getQueryBuilder();
// if we're filtering by source or id, we need to also check for non-owned shares
- if ($filterSourceTypeValue === null && $filterShareID === null) {
+ if ($filterSourceTypeValue === null && $filterShareIDs === null) {
$qb->where($qb->expr()->eq('s.owner_user_id', $qb->createNamedParameter($accessContext->currentUser->getUID())));
}
@@ -714,7 +695,7 @@ private function list(
$queries[] = $qb;
}
- if ($filterShareID !== null && $accessContext->secret !== null) {
+ if ($filterShareIDs !== null && $accessContext->secret !== null) {
$qb = $this->connection->getQueryBuilder();
$qb->innerJoin(
's', 'sharing_share_recipients', 'sr', $qb->expr()->andX(
@@ -743,8 +724,8 @@ private function list(
->from('sharing_share', 's')
->orderBy('s.id', 'ASC');
- if ($filterShareID !== null) {
- $qb->andWhere($qb->expr()->eq('s.id', $qb->createNamedParameter($filterShareID)));
+ if ($filterShareIDs !== null) {
+ $qb->andWhere($qb->expr()->in('s.id', $qb->createNamedParameter($filterShareIDs, IQueryBuilder::PARAM_STR_ARRAY)));
}
if ($filterSourceType !== null && $filterSourceTypeClass !== null) {
@@ -1132,12 +1113,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);
}
}
@@ -1304,15 +1279,7 @@ 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,
- $value,
- );
+ $this->updateShareProperty($share->id, $property);
$properties = $share->properties;
$properties[$propertyTypeClass] = $property;
@@ -1456,7 +1423,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 */
@@ -1482,4 +1449,27 @@ private function isNullOrEqual(IQueryBuilder $query, string $field, null|int|str
? $query->expr()->isNull($field)
: $query->expr()->eq($field, $query->createNamedParameter($value), is_int($value) ? IQueryBuilder::PARAM_INT : IQueryBuilder::PARAM_STR);
}
+
+ #[\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..c81e187cbf281
--- /dev/null
+++ b/lib/private/Sharing/SharingLegacySync.php
@@ -0,0 +1,1132 @@
+
+ */
+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,
+ private readonly IRootFolder $rootFolder,
+ private readonly IProviderFactory $legacyShareProviderFactory,
+ ) {
+ $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;
+ }
+
+ #[\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();
+ $legacyProviderClass = $this->legacyShareProviderFactory->getProvider($legacyShare->getProviderId())::class;
+ $legacyId = (int)$legacyShare->getId();
+ $legacyMapping = $this->legacyMapper->getLegacyMappingByLegacyProviderClassAndId($legacyProviderClass, $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 {
+ $legacyMappings = $this->legacyMapper->getLegacyMappings($legacyMapping->id);
+ // If this is the last mapping, delete the entire share.
+ // If there are more mappings, only delete that mapping and then sync.
+ if (count($legacyMappings) === 1) {
+ 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 {
+ $this->legacyMapper->deleteLegacyMapping($legacyMapping->legacyProviderClass, $legacyMapping->legacyId);
+ /** @var non-empty-list $legacyMappings The list can't be empty, because there is at least one more legacy mapping than $legacyMapping */
+ $legacyMappings = $this->legacyMapper->getLegacyMappings($legacyMapping->id);
+ $this->syncLegacySharesToShare($legacyMapping->id, $legacyMappings);
+ }
+ });
+ }
+ } 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, $legacyProviderClass, $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,
+ $legacyProviderClass,
+ $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());
+ } catch (ShareNotFound) {
+ }
+ }
+
+ $this->legacyMapper->deleteLegacyMappings($id);
+ });
+ }
+
+ return;
+ }
+
+ /** @psalm-suppress RedundantConditionGivenDocblockType */
+ if ($event instanceof SharesUpdatedEvent) {
+ foreach ($event->getShares() as $share) {
+ $this->wrapSync($share->id, function () use ($share): void {
+ // Only active shares are synced, everything else is dropped
+ if ($share->state === ShareState::Active) {
+ $this->syncShareToLegacyShares($share);
+ } else {
+ $legacyMappings = $this->legacyMapper->getLegacyMappings($share->id);
+ foreach ($legacyMappings as $legacyMapping) {
+ try {
+ $this->legacySharingManager->deleteShare($legacyMapping->getLegacyShare());
+ } catch (ShareNotFound) {
+ }
+ }
+
+ $this->legacyMapper->deleteLegacyMappings($share->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();
+ $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();
+
+ $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.');
+ }
+
+ $initiatorUser = new ShareUser(
+ $shareInitiator['value'],
+ $shareInitiator['instance'],
+ );
+
+ // 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],
+ $initiatorUser,
+ $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],
+ $initiatorUser,
+ $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],
+ $initiatorUser,
+ $recipientPermissions,
+ );
+ }
+ }
+
+ $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'],
+ );
+
+ $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) {
+ $legacySharedWith = $legacyShare->getSharedWith();
+ if ($legacySharedWith === '') {
+ throw new RuntimeException('Shared with must be a non-empty string.');
+ }
+
+ $this->backend->updateShareUserStatus($share->id, $legacySharedWith, $this->legacyUserStatusToUserStatus($legacyShare->getStatus() ?? IShare::STATUS_PENDING));
+ $this->getSourceNodeTargetManager()->setTarget($legacySharedWith, $share->owner, $legacyShare->getNode(), $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->getNode(), $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();
+
+ $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 {
+ $ownerUserFolder = null;
+ $sourceNode = null;
+
+ 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) {
+ $ownerUserFolder ??= $this->rootFolder->getUserFolder($share->owner->userId);
+ $sourceNode ??= $ownerUserFolder->getFirstNodeById((int)$source->value);
+ if (!$sourceNode instanceof Node) {
+ throw new NotFoundException();
+ }
+
+ $target = $this->getSourceNodeTargetManager()->getTarget($recipient->value, $share->owner, $sourceNode) ?? $this->getSourceNodeTargetManager()->createDefaultTarget($recipient->value, $share->owner, $sourceNode);
+ $legacyShare->setTarget($target);
+ }
+
+ $this->setCommonLegacyShareFields($share, $source, $recipient, $legacyShare);
+
+ $legacyShare = $this->createOrUpdateLegacyShare($share, $legacyMappings, $legacyShare, $recipient->value, [$recipientSecret]);
+
+ // 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);
+ }
+
+ $ownerUserFolder ??= $this->rootFolder->getUserFolder($share->owner->userId);
+ $sourceNode ??= $ownerUserFolder->getFirstNodeById((int)$source->value);
+ if (!$sourceNode instanceof Node) {
+ throw new NotFoundException();
+ }
+
+ $userIds = array_map(static fn (IUser $user): string => $user->getUID(), $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 => $ids) {
+ foreach (array_chunk($ids, 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, $sourceNode) as $userId => $target) {
+ $userIdsByTarget[$target] ??= [];
+ $userIdsByTarget[$target][] = $userId;
+ }
+
+ foreach ($userIdsByTarget as $target => $ids) {
+ foreach (array_chunk($ids, 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();
+
+ $legacyShare->setId($oldLegacyShare->getId());
+ $legacyShare->setProviderId($oldLegacyShare->getProviderId());
+
+ $this->legacyMapper->updateLegacyMapping(new LegacyMapping(
+ $legacyMapping->id,
+ $legacyMapping->legacyProviderClass,
+ $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);
+ } catch (Exception $exception) {
+ throw $exception;
+ } finally {
+ $this->ignoreNewLegacyShare = false;
+ }
+
+ $this->legacyMapper->createLegacyMapping(
+ $share->id,
+ $this->legacyShareProviderFactory->getProvider($legacyShare->getProviderId())::class,
+ (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|string|false */
+ $mimeTypeId = $result->fetchOne();
+ if ($mimeTypeId === false) {
+ throw new RuntimeException('Share source does not exist: ' . $source->value);
+ }
+
+ $nodeIsFile = $this->mimeTypeLoader->getMimetypeById((int)$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),
+ };
+ }
+
+ /**
+ * @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..0d62900c0ba26 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;
@@ -51,9 +53,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
@@ -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]
@@ -642,9 +645,9 @@ public function updateShareProperty(ShareAccessContext $accessContext, Share $sh
$time = $this->getTime();
$this->backend->setLastUpdated([$share->id], $time);
- $value = $this->backend->updateShareProperty($share->id, $property);
+ $value = $property->value;
if ($propertyType instanceof ISharePropertyTypeModifyValue) {
- $value = $propertyType->modifyValueOnLoad($value);
+ $value = $propertyType->modifyValueOnSave($this->backend->getSharePropertyValue($share->id, $property->class), $value);
}
$property = new ShareProperty(
@@ -652,6 +655,8 @@ public function updateShareProperty(ShareAccessContext $accessContext, Share $sh
$value,
);
+ $this->backend->updateShareProperty($share->id, $property);
+
$properties = $share->properties;
$properties[$property->class] = $property;
@@ -731,11 +736,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 +841,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,7 +867,7 @@ public function getShares(
?string $lastShareID,
?int $limit,
): array {
- return $this->backend->getShares($accessContext, $filterSourceTypeClass, $filterSourceTypeValue, $filterState, $filterUserStatus, $lastShareID, $limit);
+ return $this->backend->getShares($accessContext, null, $filterSourceTypeClass, $filterSourceTypeValue, $filterState, $filterUserStatus, $lastShareID, $limit);
}
#[\Override]
@@ -873,8 +877,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 +894,8 @@ public function handle(Event $event): void {
$this->dbConnection->rollBack();
throw $exception;
}
+
+ return;
}
}
@@ -927,7 +936,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 +967,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 +1082,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..174bf97619c58
--- /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..ba64779e58116
--- /dev/null
+++ b/lib/unstable/Sharing/Event/SharesUpdatedEvent.php
@@ -0,0 +1,56 @@
+ $shares */
+ private ?array $shares = null;
+
+ /**
+ * @param non-empty-list $shareIds
+ * @experimental 35.0.0
+ */
+ public function __construct(
+ public readonly array $shareIds,
+ ) {
+ parent::__construct();
+ }
+
+ /**
+ * @return non-empty-list
+ * @experimental 35.0.0
+ */
+ public function getShares(): array {
+ if ($this->shares !== null) {
+ return $this->shares;
+ }
+
+ $shares = Server::get(ISharingBackend::class)->getShares(new ShareAccessContext(overrideChecks: true), $this->shareIds, null, null, null, null, null, null);
+ if ($shares === [] || count($shares) !== count($this->shareIds)) {
+ throw new ShareNotFoundException();
+ }
+
+ return $this->shares = $shares;
+ }
+}
diff --git a/lib/unstable/Sharing/ISharingBackend.php b/lib/unstable/Sharing/ISharingBackend.php
index 410c591a123e8..8a74895c721be 100644
--- a/lib/unstable/Sharing/ISharingBackend.php
+++ b/lib/unstable/Sharing/ISharingBackend.php
@@ -13,6 +13,7 @@
use NCU\Sharing\Exception\ShareNotFoundException;
use NCU\Sharing\Permission\ISharePermissionPreset;
use NCU\Sharing\Permission\SharePermission;
+use NCU\Sharing\Property\ISharePropertyType;
use NCU\Sharing\Property\ShareProperty;
use NCU\Sharing\Recipient\IShareRecipientType;
use NCU\Sharing\Recipient\ShareRecipient;
@@ -28,6 +29,7 @@ interface ISharingBackend {
/**
* Create a new share.
*
+ * @param non-empty-string $id
* @experimental 35.0.0
*/
public function createShare(string $id, ShareUser $owner, \DateTimeImmutable $lastUpdated): void;
@@ -35,7 +37,7 @@ public function createShare(string $id, ShareUser $owner, \DateTimeImmutable $la
/**
* Perform all updates when the owner was deleted.
*
- * @return list
+ * @return list
* @experimental 35.0.0
*/
public function onOwnerDeleted(ShareUser $owner): array;
@@ -43,6 +45,7 @@ public function onOwnerDeleted(ShareUser $owner): array;
/**
* Update the state of a share.
*
+ * @param non-empty-string $id
* @throws ShareNotFoundException
* @experimental 35.0.0
*/
@@ -51,6 +54,8 @@ public function updateShareState(string $id, ShareState $state): void;
/**
* Update the user status for a share.
*
+ * @param non-empty-string $id
+ * @param non-empty-string $userId
* @experimental 35.0.0
*/
public function updateShareUserStatus(string $id, string $userId, ShareUserStatus $userStatus): void;
@@ -58,6 +63,7 @@ public function updateShareUserStatus(string $id, string $userId, ShareUserStatu
/**
* Add a new source to a share.
*
+ * @param non-empty-string $id
* @throws ShareInvalidException
* @throws ShareNotFoundException
* @experimental 35.0.0
@@ -67,6 +73,7 @@ public function addShareSource(string $id, ShareSource $source): void;
/**
* Remove an existing source from a share.
*
+ * @param non-empty-string $id
* @throws ShareNotFoundException
* @experimental 35.0.0
*/
@@ -75,7 +82,7 @@ public function removeShareSource(string $id, ShareSource $source): void;
/**
* Perform all updates when the source was deleted.
*
- * @return list
+ * @return list
* @experimental 35.0.0
*/
public function onSourceDeleted(ShareSource $source): array;
@@ -83,6 +90,7 @@ public function onSourceDeleted(ShareSource $source): array;
/**
* Add a new recipient to a share.
*
+ * @param non-empty-string $id
* @throws ShareInvalidException
* @throws ShareNotFoundException
* @experimental 35.0.0
@@ -92,6 +100,7 @@ public function addShareRecipient(string $id, ShareRecipient $recipient): void;
/**
* Remove an existing recipient from a share.
*
+ * @param non-empty-string $id
* @throws ShareNotFoundException
* @experimental 35.0.0
*/
@@ -100,7 +109,7 @@ public function removeShareRecipient(string $id, ShareRecipient $recipient): voi
/**
* Perform all updates when the recipient was deleted.
*
- * @return list
+ * @return list
* @experimental 35.0.0
*/
public function onRecipientDeleted(ShareRecipient $recipient): array;
@@ -108,7 +117,7 @@ public function onRecipientDeleted(ShareRecipient $recipient): array;
/**
* Perform all updates when the initiator was deleted.
*
- * @return list
+ * @return list
* @experimental 35.0.0
*/
public function onInitiatorDeleted(ShareUser $initiator): array;
@@ -116,6 +125,8 @@ public function onInitiatorDeleted(ShareUser $initiator): array;
/**
* Update the secret of a recipient.
*
+ * @param non-empty-string $id
+ * @param non-empty-string $secret
* @throws ShareNotFoundException
* @experimental 35.0.0
*/
@@ -124,14 +135,26 @@ public function updateShareRecipientSecret(string $id, ShareRecipient $recipient
/**
* Update a property of a share.
*
+ * @param non-empty-string $id
* @throws ShareNotFoundException
* @experimental 35.0.0
*/
- public function updateShareProperty(string $id, ShareProperty $property): ?string;
+ public function updateShareProperty(string $id, ShareProperty $property): void;
+
+ /**
+ * Get the value of a property of a share.
+ *
+ * @param non-empty-string $id
+ * @param class-string $propertyTypeClass
+ * @throws ShareNotFoundException
+ * @experimental 35.0.0
+ */
+ public function getSharePropertyValue(string $id, string $propertyTypeClass): ?string;
/**
* Update a permission of a share.
*
+ * @param non-empty-string $id
* @throws ShareNotFoundException
* @experimental 35.0.0
*/
@@ -140,6 +163,7 @@ public function updateSharePermission(string $id, SharePermission $permission):
/**
* Update a permission for a recipient of a share.
*
+ * @param non-empty-string $id
* @throws ShareNotFoundException
* @experimental 35.0.0
*/
@@ -148,6 +172,7 @@ public function updateShareRecipientPermission(string $id, ShareRecipient $recip
/**
* Select a permission preset for a share.
*
+ * @param non-empty-string $id
* @param class-string $permissionPresetClass
* @throws ShareNotFoundException
* @experimental 35.0.0
@@ -157,6 +182,7 @@ public function selectSharePermissionPreset(string $id, string $permissionPreset
/**
* Delete a share.
*
+ * @param non-empty-string $id
* @throws ShareNotFoundException
* @experimental 35.0.0
*/
@@ -165,6 +191,7 @@ public function deleteShare(string $id): void;
/**
* Get a share.
*
+ * @param non-empty-string $id
* @throws ShareNotFoundException
* @experimental 35.0.0
*/
@@ -173,24 +200,20 @@ public function getShare(ShareAccessContext $accessContext, string $id): Share;
/**
* Get multiple shares.
*
+ * @param ?non-empty-list $filterShareIDs
* @param ?class-string $filterSourceTypeClass
* @param ?non-empty-string $filterSourceTypeValue
+ * @param ?non-empty-string $lastShareID
* @param ?positive-int $limit
* @return list
* @experimental 35.0.0
*/
- public function getShares(ShareAccessContext $accessContext, ?string $filterSourceTypeClass, ?string $filterSourceTypeValue, ?ShareState $filterState, ?ShareUserStatus $filterUserStatus, ?string $lastShareID, ?int $limit): array;
-
- /**
- * Check if a share ID belongs to this backend.
- *
- * @experimental 35.0.0
- */
- public function hasShare(string $id): bool;
+ public function getShares(ShareAccessContext $accessContext, ?array $filterShareIDs, ?string $filterSourceTypeClass, ?string $filterSourceTypeValue, ?ShareState $filterState, ?ShareUserStatus $filterUserStatus, ?string $lastShareID, ?int $limit): array;
/**
* Get the owner of a share.
*
+ * @param non-empty-string $id
* @throws ShareNotFoundException
* @experimental 35.0.0
*/
@@ -199,7 +222,7 @@ public function getShareOwner(string $id): ShareUser;
/**
* Set the last updated timestamp for multiple shares.
*
- * @param non-empty-list $ids
+ * @param non-empty-list $ids
* @throws ShareNotFoundException
* @experimental 35.0.0
*/
@@ -219,7 +242,7 @@ public function ensureDefaults(array $shares): array;
*
* "shared with" includes both shares owned by the user, and reshares initiated by the user
*
- * @param ?list> $filterRecipientTypeClasses
+ * @param ?non-empty-list> $filterRecipientTypeClasses
* @param null|non-empty-string $notInShare
* @param non-negative-int $count
* @return list
@@ -232,4 +255,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/ISharingManager.php b/lib/unstable/Sharing/ISharingManager.php
index 044cf78f0202b..4fb6592cc73b8 100644
--- a/lib/unstable/Sharing/ISharingManager.php
+++ b/lib/unstable/Sharing/ISharingManager.php
@@ -29,10 +29,10 @@ interface ISharingManager {
/**
* Search for recipients that can be added to a share.
*
- * @param ?list> $filterRecipientTypeClasses
+ * @param ?non-empty-list> $filterRecipientTypeClasses
* @param positive-int $limit
* @param non-negative-int $offset
- * @param ?Share $excludeShare If provided, recipients that are already part of the share will not be returned.
+ * @param ?Share $forShare If provided, recipients that are already part of the share will not be returned.
* @return list
* @throws ShareNotFoundException
* @experimental 35.0.0
@@ -152,6 +152,7 @@ public function onInitiatorDeleted(ShareAccessContext $accessContext, ShareUser
/**
* Update the secret of a recipient.
*
+ * @param non-empty-string $secret
* @throws ShareInvalidException
* @throws ShareNotFoundException
* @throws ShareOperationForbiddenException
@@ -210,6 +211,7 @@ public function deleteShare(ShareAccessContext $accessContext, Share $share): vo
/**
* Get a share.
*
+ * @param non-empty-string $id
* @throws ShareNotFoundException
* @experimental 35.0.0
*/
@@ -220,6 +222,7 @@ public function getShare(ShareAccessContext $accessContext, string $id): Share;
*
* @param ?class-string $filterSourceTypeClass
* @param ?non-empty-string $filterSourceTypeValue
+ * @param ?non-empty-string $lastShareID
* @param ?positive-int $limit
* @return list
* @experimental 35.0.0
diff --git a/lib/unstable/Sharing/Property/APasswordSharePropertyType.php b/lib/unstable/Sharing/Property/APasswordSharePropertyType.php
index 4ff5ba8a62eb3..9121a5a9af968 100644
--- a/lib/unstable/Sharing/Property/APasswordSharePropertyType.php
+++ b/lib/unstable/Sharing/Property/APasswordSharePropertyType.php
@@ -80,7 +80,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..b91229e90538b 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/openapi.json b/openapi.json
index b6a818a3c6c88..4ec31b6d1936d 100644
--- a/openapi.json
+++ b/openapi.json
@@ -38422,7 +38422,8 @@
"items": {
"type": "string",
"minLength": 1
- }
+ },
+ "minItems": 1
}
},
{
@@ -38464,7 +38465,8 @@
"schema": {
"type": "string",
"nullable": true,
- "default": null
+ "default": null,
+ "minLength": 1
}
},
{
@@ -38802,7 +38804,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -39009,7 +39012,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -39193,7 +39197,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -39379,7 +39384,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -39589,7 +39595,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -39775,7 +39782,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -40001,7 +40009,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -40214,7 +40223,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -40427,7 +40437,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -40658,7 +40669,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -40836,7 +40848,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -41024,7 +41037,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -41180,7 +41194,8 @@
"description": "ID of the share",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "minLength": 1
}
},
{
@@ -41326,7 +41341,8 @@
"description": "The ID of the previous share. This is used as an offset and only shares with higher IDs are returned.",
"schema": {
"type": "string",
- "nullable": true
+ "nullable": true,
+ "minLength": 1
}
},
{
diff --git a/psalm-strict.xml b/psalm-strict.xml
index 383d80f2d8b49..19da06773e4f1 100644
--- a/psalm-strict.xml
+++ b/psalm-strict.xml
@@ -69,6 +69,9 @@
+
+
+
@@ -82,6 +85,7 @@
+
@@ -90,6 +94,7 @@
+
diff --git a/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php b/tests/Core/Sharing/Recipient/GroupShareRecipientTypeTest.php
index 7e932a79e61de..56cb1ee01c4fb 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], $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..3104e00fd8aa6 100644
--- a/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php
+++ b/tests/Core/Sharing/Recipient/TeamShareRecipientTypeTest.php
@@ -14,6 +14,8 @@
use NCU\Sharing\Recipient\ShareRecipient;
use NCU\Sharing\ShareAccessContext;
use OC\Core\Sharing\Recipient\TeamShareRecipientType;
+use OC\Sharing\SharingManager;
+use OC\User\LazyUser;
use OCA\Circles\CirclesManager;
use OCA\Circles\Exceptions\CircleNotFoundException;
use OCA\Circles\Service\CircleService;
@@ -98,7 +100,7 @@ public function setUp(): void {
$this->team2 = $this->createTeam($teamManager, 'team2');
$this->team3 = $this->createTeam($teamManager, 'team3');
- $this->recipientType = new TeamShareRecipientType(Server::get(IEventDispatcher::class), $this->dbConnection, $this->manager);
+ $this->recipientType = new TeamShareRecipientType(Server::get(IEventDispatcher::class), $this->dbConnection, $this->manager, Server::get(IUserManager::class));
}
#[\Override]
@@ -125,14 +127,20 @@ 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], array_map(fn (LazyUser $user): IUser => $this->invokePrivate($user, 'getUser'), $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 🤷♀️');
- $this->assertEquals('Team 1', $this->recipientType->getRecipientDisplayName($this->team1->getId()));
+ $this->assertEquals('Team 1', $this->recipientType->getRecipientDisplayName($this->team1->getId(), null));
}
public function testSearchRecipients(): void {
@@ -177,8 +185,8 @@ public function testDelete(): void {
$this->dbConnection->beginTransaction();
$share = $this->manager->getShare($accessContext, $share->id);
- $this->assertGreaterThanOrEqual($before, $share->lastUpdated->getTimestamp());
- $this->assertLessThanOrEqual($after, $share->lastUpdated->getTimestamp());
+ $this->assertGreaterThanOrEqual(SharingManager::timeToMs($before), SharingManager::timeToMs($share->lastUpdated));
+ $this->assertLessThanOrEqual(SharingManager::timeToMs($after), SharingManager::timeToMs($share->lastUpdated));
$this->assertEquals([], $share->recipients);
$this->manager->deleteShare($accessContext, $share);
diff --git a/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php b/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php
index 20978861bb5d6..f58f3d229c79b 100644
--- a/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php
+++ b/tests/Core/Sharing/Recipient/UserShareRecipientTypeTest.php
@@ -17,6 +17,7 @@
use OC\Core\Sharing\Recipient\UserShareRecipientType;
use OC\Sharing\SharingManager;
use OC\User\Database;
+use OC\User\LazyUser;
use OCA\DAV\CardDAV\CardDavBackend;
use OCA\DAV\CardDAV\ContactsManager;
use OCP\Contacts\IManager;
@@ -95,8 +96,13 @@ 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 {
+ /** @psalm-suppress ArgumentTypeCoercion, MixedReturnStatement */
+ $this->assertEquals([$this->user1], array_map(fn (LazyUser $user): IUser => $this->invokePrivate($user, 'getUser'), $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..3af97a6019647 100644
--- a/tests/lib/Sharing/AbstractSharingManagerTests.php
+++ b/tests/lib/Sharing/AbstractSharingManagerTests.php
@@ -88,6 +88,7 @@ abstract protected function addShareRecipient(ShareAccessContext $accessContext,
abstract protected function removeShareRecipient(ShareAccessContext $accessContext, Share $share, ShareRecipient $recipient): array;
/**
+ * @param non-empty-string $secret
* @return SharingShare
*/
abstract protected function updateShareRecipientSecret(ShareAccessContext $accessContext, Share $share, ShareRecipient $recipient, string $secret): array;
@@ -117,6 +118,7 @@ abstract protected function selectSharePermissionPreset(ShareAccessContext $acce
abstract protected function deleteShare(ShareAccessContext $accessContext, Share $share): void;
/**
+ * @param non-empty-string $id
* @return SharingShare
*/
abstract protected function getShare(ShareAccessContext $accessContext, string $id): array;
@@ -1746,9 +1748,26 @@ public function testUpdateSharePropertyRequired(): void {
], $formatted['properties']);
}
- public function testUpdateSharePropertyModifyProperties(): void {
+ /**
+ * @return list, ?string}>
+ */
+ public static function dataUpdateSharePropertyModifyProperties(): array {
+ return [
+ [[], null],
+ [['modify-on-save'], 'modified-on-save'],
+ [['old-value'], 'old-value'],
+ [['old-value', 'modify-on-save-old-value'], 'old-value'],
+ [['modify-on-format'], 'modified-on-format'],
+ ];
+ }
+
+ /**
+ * @param list $values
+ */
+ #[DataProvider('dataUpdateSharePropertyModifyProperties')]
+ public function testUpdateSharePropertyModifyProperties(array $values, ?string $expectedValue): 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);
@@ -1762,102 +1781,14 @@ public function testUpdateSharePropertyModifyProperties(): void {
$this->dbConnection->commit();
- // We cannot test for modify-on-save, because we will always see modified-on-save as the returned value.
-
- $formatted = $this->getShare($accessContext, $share->id);
- $this->assertEquals([
- [
- 'class' => TestSharePropertyType1::class,
- 'display_name' => 'TestSharePropertyType1',
- 'hint' => 'hint TestSharePropertyType1',
- 'priority' => 1,
- 'advanced' => false,
- 'required' => false,
- 'value' => null,
- 'type' => 'enum',
- 'valid_values' => ['valid1'],
- ],
- [
- 'class' => TestSharePropertyTypeModifyValue::class,
- 'display_name' => 'TestSharePropertyTypeModifyValue',
- 'hint' => 'hint TestSharePropertyTypeModifyValue',
- 'priority' => 1,
- 'advanced' => false,
- 'required' => false,
- 'value' => 'modified-on-save',
- 'type' => 'enum',
- 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load'],
- ],
- ], $formatted['properties']);
-
- $this->dbConnection->beginTransaction();
- $share = $this->manager->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'old-value'));
- $this->dbConnection->commit();
-
- $before = $this->manager->getTime();
- $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-save-old-value'));
- $share = $this->reloadShare($accessContext, $share);
- $after = $this->manager->getTime();
- $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated']));
- $this->assertEquals([
- [
- 'class' => TestSharePropertyType1::class,
- 'display_name' => 'TestSharePropertyType1',
- 'hint' => 'hint TestSharePropertyType1',
- 'priority' => 1,
- 'advanced' => false,
- 'required' => false,
- 'value' => null,
- 'type' => 'enum',
- 'valid_values' => ['valid1'],
- ],
- [
- 'class' => TestSharePropertyTypeModifyValue::class,
- 'display_name' => 'TestSharePropertyTypeModifyValue',
- 'hint' => 'hint TestSharePropertyTypeModifyValue',
- 'priority' => 1,
- 'advanced' => false,
- 'required' => false,
- 'value' => 'old-value',
- 'type' => 'enum',
- 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load'],
- ],
- ], $formatted['properties']);
-
- $before = $this->manager->getTime();
- $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-save'));
- $share = $this->reloadShare($accessContext, $share);
- $after = $this->manager->getTime();
- $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated']));
- $this->assertEquals([
- [
- 'class' => TestSharePropertyType1::class,
- 'display_name' => 'TestSharePropertyType1',
- 'hint' => 'hint TestSharePropertyType1',
- 'priority' => 1,
- 'advanced' => false,
- 'required' => false,
- 'value' => null,
- 'type' => 'enum',
- 'valid_values' => ['valid1'],
- ],
- [
- 'class' => TestSharePropertyTypeModifyValue::class,
- 'display_name' => 'TestSharePropertyTypeModifyValue',
- 'hint' => 'hint TestSharePropertyTypeModifyValue',
- 'priority' => 1,
- 'advanced' => false,
- 'required' => false,
- 'value' => 'modified-on-save',
- 'type' => 'enum',
- 'valid_values' => ['old-value', 'modify-on-save-old-value', 'modify-on-save', 'modify-on-load'],
- ],
- ], $formatted['properties']);
+ if ($values === []) {
+ $formatted = $this->getShare($accessContext, $share->id);
+ } else {
+ foreach ($values as $value) {
+ $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeModifyValue::class, $value));
+ }
+ }
- $before = $this->manager->getTime();
- $formatted = $this->updateShareProperty($accessContext, $share, new ShareProperty(TestSharePropertyTypeModifyValue::class, 'modify-on-load'));
- $after = $this->manager->getTime();
- $this->assertDateBetween($before, $after, $this->parseTime($formatted['last_updated']));
$this->assertEquals([
[
'class' => TestSharePropertyType1::class,
@@ -1877,9 +1808,9 @@ public function testUpdateSharePropertyModifyProperties(): void {
'priority' => 1,
'advanced' => false,
'required' => false,
- 'value' => 'modified-on-load',
+ 'value' => $expectedValue,
'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..959bdeee5b4a1
--- /dev/null
+++ b/tests/lib/Sharing/SharingLegacySyncTest.php
@@ -0,0 +1,2120 @@
+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');
+
+ /** @var Notifications $realNotifications */
+ $realNotifications = $this->invokePrivate(Server::get(FederatedShareProvider::class), 'notifications');
+ $this->realNotifications = $realNotifications;
+ $this->invokePrivate(Server::get(FederatedShareProvider::class), 'notifications', [$this->getAutoMock(Notifications::class)]);
+
+ $this->dbConnection->beginTransaction();
+ }
+
+ #[\Override]
+ public function tearDown(): void {
+ $this->clearShares();
+
+ $this->invokePrivate(Server::get(FederatedShareProvider::class), 'notifications', [$this->realNotifications]);
+
+ $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 {
+ [$id, $created] = $this->generateIdAndCreatedTimestamp();
+ $owner = new ShareUser($this->owner->getUID(), null);
+
+ $groupRecipient = new ShareRecipient(GroupShareRecipientType::class, $this->group->getGID(), null, $this->sharingManager->generateSecret(), $owner);
+
+ $share = 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),
+ $groupRecipient,
+ ],
+ [
+ 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->createShare($share, []);
+
+ $legacyShares = iterator_to_array($this->legacySharingManager->getAllShares());
+ $this->assertCount(2, $legacyShares);
+
+ $this->legacySharingManager->deleteShare($legacyShares[0]);
+
+ $legacyShares = iterator_to_array($this->legacySharingManager->getAllShares());
+ $this->assertCount(1, $legacyShares);
+
+ $share = $this->sharingBackend->getShare(new ShareAccessContext(overrideChecks: true), $id);
+ $this->assertEquals([$groupRecipient], $share->recipients);
+
+ $this->legacySharingManager->deleteShare($legacyShares[0]);
+ $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, '/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, '/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..c6fce3c501645 100644
--- a/tests/lib/Sharing/TestSharePropertyTypeModifyValue.php
+++ b/tests/lib/Sharing/TestSharePropertyTypeModifyValue.php
@@ -10,15 +10,8 @@
namespace Test\Sharing;
use NCU\Sharing\Property\ISharePropertyTypeModifyValue;
-use NCU\Sharing\Share;
final class TestSharePropertyTypeModifyValue extends TestSharePropertyType1 implements ISharePropertyTypeModifyValue {
-
- #[\Override]
- public function getDefaultValue(Share $share): string {
- return 'modify-on-save';
- }
-
#[\Override]
public function modifyValueOnSave(?string $oldValue, ?string $newValue): ?string {
if ($newValue === 'modify-on-save') {
@@ -33,9 +26,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];