From 5aea939554b923b49e75ac7fbf1e3d91d12e7b89 Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Fri, 9 Jan 2026 11:04:35 +0100 Subject: [PATCH 01/10] test(integration): Fix `composer install` command Signed-off-by: Louis Chmn --- tests/integration/run.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration/run.sh b/tests/integration/run.sh index 1dfe35b70..1ef66e721 100755 --- a/tests/integration/run.sh +++ b/tests/integration/run.sh @@ -7,8 +7,7 @@ HIDE_OC_LOGS=$2 # Nextcloud integration tests composer ( - cd ${OC_PATH}build/integration - composer install + composer --working-dir $OC_PATH install ) INSTALLED=$($OCC status | grep installed: | cut -d " " -f 5) From 1b71a24a1f639c6b4950431f0ac21e8003eea367 Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Fri, 9 Jan 2026 10:42:08 +0100 Subject: [PATCH 02/10] chore(deps): Bump justinrainbow/json-schema to v6.6.4 Was v6.4.2 Signed-off-by: Louis Chmn --- composer.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index b2d385960..3dcd69266 100644 --- a/composer.lock +++ b/composer.lock @@ -65,26 +65,26 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.4.2", + "version": "6.6.4", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02" + "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ce1fd2d47799bb60668643bc6220f6278a4c1d02", - "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2eeb75d21cf73211335888e7f5e6fd7440723ec7", + "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7", "shasum": "" }, "require": { "ext-json": "*", - "marc-mabe/php-enum": "^4.0", + "marc-mabe/php-enum": "^4.4", "php": "^7.2 || ^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "3.3.0", - "json-schema/json-schema-test-suite": "1.2.0", + "json-schema/json-schema-test-suite": "^23.2", "marc-mabe/php-enum-phpstan": "^2.0", "phpspec/prophecy": "^1.19", "phpstan/phpstan": "^1.12", @@ -134,9 +134,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.4.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.4" }, - "time": "2025-06-03T18:27:04+00:00" + "time": "2025-12-19T15:01:32+00:00" }, { "name": "marc-mabe/php-enum", From 6ad3d61d5a4611f1eebcaf54d4946242f6175fdc Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Fri, 9 Jan 2026 10:35:57 +0100 Subject: [PATCH 03/10] chore(deps): Bump nextcloud/ocp Signed-off-by: Louis Chmn --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index 3dcd69266..4506e9333 100644 --- a/composer.lock +++ b/composer.lock @@ -437,12 +437,12 @@ "source": { "type": "git", "url": "https://github.com/nextcloud-deps/ocp.git", - "reference": "d42bd36d0413b881b7459b220e1328bc57e48b38" + "reference": "6c1e5686d00c40f57abd83e8ad268e9ab34825bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/d42bd36d0413b881b7459b220e1328bc57e48b38", - "reference": "d42bd36d0413b881b7459b220e1328bc57e48b38", + "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/6c1e5686d00c40f57abd83e8ad268e9ab34825bb", + "reference": "6c1e5686d00c40f57abd83e8ad268e9ab34825bb", "shasum": "" }, "require": { @@ -478,7 +478,7 @@ "issues": "https://github.com/nextcloud-deps/ocp/issues", "source": "https://github.com/nextcloud-deps/ocp/tree/master" }, - "time": "2025-12-11T00:55:32+00:00" + "time": "2026-01-09T00:57:54+00:00" }, { "name": "nikic/php-parser", From 513445d322bf5c4eb2ee44900e82a39d726c61d4 Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Fri, 9 Jan 2026 10:36:12 +0100 Subject: [PATCH 04/10] fix: Correct typing Signed-off-by: Louis Chmn --- lib/Controller/CardApiController.php | 4 ++-- lib/Db/Assignment.php | 1 - lib/Db/Session.php | 1 - lib/Service/FilesAppService.php | 2 +- lib/Sharing/DeckShareProvider.php | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/Controller/CardApiController.php b/lib/Controller/CardApiController.php index bc1359250..4a3187e01 100644 --- a/lib/Controller/CardApiController.php +++ b/lib/Controller/CardApiController.php @@ -72,11 +72,11 @@ public function create($title, $type = 'plain', $order = 999, $description = '', $card = $this->cardService->create($title, $this->request->getParam('stackId'), $type, $order, $this->userId, $description, $duedate); foreach ($labels as $labelId) { - $this->cardService->assignLabel($card->id, $labelId); + $this->cardService->assignLabel($card->getId(), $labelId); } foreach ($users as $user) { - $this->assignmentService->assignUser($card->id, $user['id'], $user['type']); + $this->assignmentService->assignUser($card->getId(), $user['id'], $user['type']); } return new DataResponse($card, HTTP::STATUS_OK); diff --git a/lib/Db/Assignment.php b/lib/Db/Assignment.php index 6c273240a..4160aed12 100644 --- a/lib/Db/Assignment.php +++ b/lib/Db/Assignment.php @@ -10,7 +10,6 @@ use JsonSerializable; class Assignment extends RelationalEntity implements JsonSerializable { - public $id; protected $participant; protected $cardId; protected $type; diff --git a/lib/Db/Session.php b/lib/Db/Session.php index 7f65f8839..886fdfb4c 100644 --- a/lib/Db/Session.php +++ b/lib/Db/Session.php @@ -12,7 +12,6 @@ use OCP\AppFramework\Db\Entity; class Session extends Entity implements \JsonSerializable { - public $id; protected $userId; protected $token; protected $lastContact; diff --git a/lib/Service/FilesAppService.php b/lib/Service/FilesAppService.php index 38f3cf373..dda58be0f 100644 --- a/lib/Service/FilesAppService.php +++ b/lib/Service/FilesAppService.php @@ -312,7 +312,7 @@ public function markAsDeleted(Attachment $attachment) { */ private function getShareForAttachment(Attachment $attachment): IShare { try { - $share = $this->shareProvider->getShareById($attachment->getId()); + $share = $this->shareProvider->getShareById((string)$attachment->getId()); } catch (ShareNotFound $e) { throw new NoPermissionException('No permission to access the attachment from the card'); } diff --git a/lib/Sharing/DeckShareProvider.php b/lib/Sharing/DeckShareProvider.php index ca0dbc5e7..ad705aa6f 100644 --- a/lib/Sharing/DeckShareProvider.php +++ b/lib/Sharing/DeckShareProvider.php @@ -419,7 +419,7 @@ public function restore(IShare $share, string $recipient): IShare { $qb->executeStatement(); - return $this->getShareById((int)$share->getId(), $recipient); + return $this->getShareById((string)$share->getId(), $recipient); } /** From 3b59aa0b4f39582c503a9e26eeac7d4ca4b026c8 Mon Sep 17 00:00:00 2001 From: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com> Date: Tue, 30 Dec 2025 12:03:17 +0100 Subject: [PATCH 05/10] feat: implement IPartialShareProvider support Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com> --- lib/Sharing/DeckShareProvider.php | 74 ++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/lib/Sharing/DeckShareProvider.php b/lib/Sharing/DeckShareProvider.php index ad705aa6f..2d71b7b71 100644 --- a/lib/Sharing/DeckShareProvider.php +++ b/lib/Sharing/DeckShareProvider.php @@ -32,6 +32,7 @@ use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; +use OCP\Share\IPartialShareProvider; use OCP\Share\IShare; /** Taken from the talk shareapicontroller helper */ @@ -42,7 +43,7 @@ public function formatShare(IShare $share): array; public function canAccessShare(IShare $share, string $user): bool; } -class DeckShareProvider implements \OCP\Share\IShareProvider { +class DeckShareProvider implements \OCP\Share\IShareProvider, IPartialShareProvider { public const DECK_FOLDER = '/Deck'; public const DECK_FOLDER_PLACEHOLDER = '/{DECK_PLACEHOLDER}'; @@ -772,6 +773,77 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset): arra return $shares; } + /** + * @inheritDoc + */ + public function getSharedWithByPath( + string $userId, + int $shareType, + string $path, + bool $forChildren, + int $limit, + int $offset, + ): iterable { + /** @var IShare[] $shares */ + $shares = []; + + $qb = $this->dbConnection->getQueryBuilder(); + // s is the parent share, s2 the child + $qb->select('s.*', 's2.permissions AS s2_permissions', 's2.file_target AS s2_file_target', + 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', + 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', + 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum' + ) + ->selectAlias('st.id', 'storage_string_id') + ->from('share', 's2') + ->orderBy('s2.id') + ->leftJoin('s2', 'share', 's', $qb->expr()->eq('s2.parent', 's.id')) + ->leftJoin('s2', 'filecache', 'f', $qb->expr()->eq('s2.file_source', 'f.fileid')) + ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id')) + ->leftJoin('s2', 'deck_cards', 'dc', $qb->expr()->eq($qb->expr() + ->castColumn('dc.id', IQueryBuilder::PARAM_STR), 's.share_with')); + + if ($limit !== -1) { + $qb->setMaxResults($limit); + } + + $qb->andWhere($qb->expr()->eq('s2.share_with', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('s2.share_type', + $qb->createNamedParameter(IShare::TYPE_DECK_USER))) + ->andWhere($qb->expr()->orX( + $qb->expr()->eq('s2.item_type', $qb->createNamedParameter('file')), + $qb->expr()->eq('s2.item_type', $qb->createNamedParameter('folder')) + )); + + if ($forChildren) { + $qb->andWhere($qb->expr()->like('s2.file_target', $qb->createNamedParameter($this->dbConnection->escapeLikeParameter($path) . '_%'))); + } else { + $qb->andWhere($qb->expr()->eq('s2.file_target', $qb->createNamedParameter($path))); + } + + $qb->andWhere($qb->expr()->eq('dc.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); + + $cursor = $qb->executeQuery(); + while ($data = $cursor->fetch()) { + if (!$this->isAccessibleResult($data)) { + continue; + } + + if ($offset > 0) { + $offset--; + continue; + } + $share = $this->createShareObject($data); + // patch the share with the user-specific information + $this->applyBoardPermission($share, (int)$data['s2_permissions'], $userId); + $share->setTarget($data['s2_file_target']); + $shares[] = $share; + } + $cursor->closeCursor(); + + return $shares; + } + /** * Get shared with the card * From 104d414b54d492a6e1be9543acf5d812a8f5b563 Mon Sep 17 00:00:00 2001 From: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:16:44 +0100 Subject: [PATCH 06/10] REMOVEME: test on top of IPartialMountProvider setup Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com> --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 03e2738b6..7dba407b1 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -55,7 +55,7 @@ jobs: uses: actions/checkout@v4.2.2 with: repository: nextcloud/server - ref: ${{ matrix.server-versions }} + ref: 'feature/54562/files-sharing-authoritative' submodules: true - name: Checkout app From a5cac5cb066b7a27f4c77989251ea83f24ca9f04 Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Fri, 9 Jan 2026 15:36:31 +0100 Subject: [PATCH 07/10] fixup! feat: implement IPartialShareProvider support Signed-off-by: Louis Chmn --- lib/Sharing/DeckShareProvider.php | 104 +++++++++++------------------- 1 file changed, 37 insertions(+), 67 deletions(-) diff --git a/lib/Sharing/DeckShareProvider.php b/lib/Sharing/DeckShareProvider.php index 2d71b7b71..6a09e91cc 100644 --- a/lib/Sharing/DeckShareProvider.php +++ b/lib/Sharing/DeckShareProvider.php @@ -693,16 +693,17 @@ public function getSharesByPath(Node $path): array { } /** - * Get shared with the given user - * - * @param string $userId get shares where this user is the recipient - * @param int $shareType - * @param Node|null $node - * @param int $limit The max number of entries returned, -1 for all - * @param int $offset - * @return IShare[] + * Get received shared for the given user. + * You can optionally provide a node or a path to filter the shares. */ - public function getSharedWith($userId, $shareType, $node, $limit, $offset): array { + public function _getSharedWith( + string $userId, + int $limit, + int $offset, + ?Node $node = null, + ?string $path = null, + bool $forChildren = false, + ): array { $allBoards = $this->boardMapper->findBoardIds($userId); /** @var IShare[] $shares */ @@ -741,6 +742,18 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset): arra $qb->andWhere($qb->expr()->eq('s.file_source', $qb->createNamedParameter($node->getId()))); } + if ($path !== null) { + $qb->leftJoin('s', 'share', 'sc', $qb->expr()->eq('s.parent', 'sc.id')) + ->andWhere($qb->expr()->eq('sc.share_with', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('sc.share_type', $qb->createNamedParameter(IShare::TYPE_DECK_USER))); + + if ($forChildren) { + $qb->andWhere($qb->expr()->like('sc.file_target', $qb->createNamedParameter($this->dbConnection->escapeLikeParameter($path) . '_%'))); + } else { + $qb->andWhere($qb->expr()->eq('sc.file_target', $qb->createNamedParameter($path))); + } + } + $qb->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_DECK))) ->andWhere($qb->expr()->in('db.id', $qb->createNamedParameter( $boards, @@ -773,6 +786,20 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset): arra return $shares; } + /** + * Get shared with the given user + * + * @param string $userId get shares where this user is the recipient + * @param int $shareType + * @param Node|null $node + * @param int $limit The max number of entries returned, -1 for all + * @param int $offset + * @return IShare[] + */ + public function getSharedWith($userId, $shareType, $node, $limit, $offset): array { + return $this->_getSharedWith($userId, $limit, $offset, $node); + } + /** * @inheritDoc */ @@ -784,64 +811,7 @@ public function getSharedWithByPath( int $limit, int $offset, ): iterable { - /** @var IShare[] $shares */ - $shares = []; - - $qb = $this->dbConnection->getQueryBuilder(); - // s is the parent share, s2 the child - $qb->select('s.*', 's2.permissions AS s2_permissions', 's2.file_target AS s2_file_target', - 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', - 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', - 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum' - ) - ->selectAlias('st.id', 'storage_string_id') - ->from('share', 's2') - ->orderBy('s2.id') - ->leftJoin('s2', 'share', 's', $qb->expr()->eq('s2.parent', 's.id')) - ->leftJoin('s2', 'filecache', 'f', $qb->expr()->eq('s2.file_source', 'f.fileid')) - ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id')) - ->leftJoin('s2', 'deck_cards', 'dc', $qb->expr()->eq($qb->expr() - ->castColumn('dc.id', IQueryBuilder::PARAM_STR), 's.share_with')); - - if ($limit !== -1) { - $qb->setMaxResults($limit); - } - - $qb->andWhere($qb->expr()->eq('s2.share_with', $qb->createNamedParameter($userId))) - ->andWhere($qb->expr()->eq('s2.share_type', - $qb->createNamedParameter(IShare::TYPE_DECK_USER))) - ->andWhere($qb->expr()->orX( - $qb->expr()->eq('s2.item_type', $qb->createNamedParameter('file')), - $qb->expr()->eq('s2.item_type', $qb->createNamedParameter('folder')) - )); - - if ($forChildren) { - $qb->andWhere($qb->expr()->like('s2.file_target', $qb->createNamedParameter($this->dbConnection->escapeLikeParameter($path) . '_%'))); - } else { - $qb->andWhere($qb->expr()->eq('s2.file_target', $qb->createNamedParameter($path))); - } - - $qb->andWhere($qb->expr()->eq('dc.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); - - $cursor = $qb->executeQuery(); - while ($data = $cursor->fetch()) { - if (!$this->isAccessibleResult($data)) { - continue; - } - - if ($offset > 0) { - $offset--; - continue; - } - $share = $this->createShareObject($data); - // patch the share with the user-specific information - $this->applyBoardPermission($share, (int)$data['s2_permissions'], $userId); - $share->setTarget($data['s2_file_target']); - $shares[] = $share; - } - $cursor->closeCursor(); - - return $shares; + return $this->_getSharedWith($userId, $limit, $offset, null, $path, $forChildren); } /** From db7b174f2555ab23ec68ed33bc0751e281c9b185 Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Fri, 9 Jan 2026 15:54:33 +0100 Subject: [PATCH 08/10] fixup! feat: implement IPartialShareProvider support Signed-off-by: Louis Chmn --- lib/Sharing/DeckShareProvider.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/Sharing/DeckShareProvider.php b/lib/Sharing/DeckShareProvider.php index 6a09e91cc..995ed06a7 100644 --- a/lib/Sharing/DeckShareProvider.php +++ b/lib/Sharing/DeckShareProvider.php @@ -744,7 +744,6 @@ public function _getSharedWith( if ($path !== null) { $qb->leftJoin('s', 'share', 'sc', $qb->expr()->eq('s.parent', 'sc.id')) - ->andWhere($qb->expr()->eq('sc.share_with', $qb->createNamedParameter($userId))) ->andWhere($qb->expr()->eq('sc.share_type', $qb->createNamedParameter(IShare::TYPE_DECK_USER))); if ($forChildren) { @@ -800,9 +799,6 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset): arra return $this->_getSharedWith($userId, $limit, $offset, $node); } - /** - * @inheritDoc - */ public function getSharedWithByPath( string $userId, int $shareType, From 1cc0e76d8335b0a8965f109797e354a43fee68fa Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Fri, 9 Jan 2026 15:55:19 +0100 Subject: [PATCH 09/10] fixup! feat: implement IPartialShareProvider support Signed-off-by: Louis Chmn --- lib/Sharing/DeckShareProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Sharing/DeckShareProvider.php b/lib/Sharing/DeckShareProvider.php index 995ed06a7..3f1b26a8c 100644 --- a/lib/Sharing/DeckShareProvider.php +++ b/lib/Sharing/DeckShareProvider.php @@ -702,7 +702,7 @@ public function _getSharedWith( int $offset, ?Node $node = null, ?string $path = null, - bool $forChildren = false, + ?bool $forChildren = false, ): array { $allBoards = $this->boardMapper->findBoardIds($userId); From 9407db5755f360ebf507839e95d47d2f78280b04 Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Fri, 9 Jan 2026 15:57:38 +0100 Subject: [PATCH 10/10] fixup! feat: implement IPartialShareProvider support Signed-off-by: Louis Chmn --- lib/Sharing/DeckShareProvider.php | 50 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/Sharing/DeckShareProvider.php b/lib/Sharing/DeckShareProvider.php index 3f1b26a8c..f841fe3c8 100644 --- a/lib/Sharing/DeckShareProvider.php +++ b/lib/Sharing/DeckShareProvider.php @@ -692,6 +692,31 @@ public function getSharesByPath(Node $path): array { return $shares; } + /** + * Get shared with the given user + * + * @param string $userId get shares where this user is the recipient + * @param int $shareType + * @param Node|null $node + * @param int $limit The max number of entries returned, -1 for all + * @param int $offset + * @return IShare[] + */ + public function getSharedWith($userId, $shareType, $node, $limit, $offset): array { + return $this->_getSharedWith($userId, $limit, $offset, $node); + } + + public function getSharedWithByPath( + string $userId, + int $shareType, + string $path, + bool $forChildren, + int $limit, + int $offset, + ): iterable { + return $this->_getSharedWith($userId, $limit, $offset, null, $path, $forChildren); + } + /** * Get received shared for the given user. * You can optionally provide a node or a path to filter the shares. @@ -785,31 +810,6 @@ public function _getSharedWith( return $shares; } - /** - * Get shared with the given user - * - * @param string $userId get shares where this user is the recipient - * @param int $shareType - * @param Node|null $node - * @param int $limit The max number of entries returned, -1 for all - * @param int $offset - * @return IShare[] - */ - public function getSharedWith($userId, $shareType, $node, $limit, $offset): array { - return $this->_getSharedWith($userId, $limit, $offset, $node); - } - - public function getSharedWithByPath( - string $userId, - int $shareType, - string $path, - bool $forChildren, - int $limit, - int $offset, - ): iterable { - return $this->_getSharedWith($userId, $limit, $offset, null, $path, $forChildren); - } - /** * Get shared with the card *