Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions lib/Db/ShareWrapperRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,11 @@ public function getSharesBy(
}

/**
* @param FederatedUser $federatedUser
* A null $federatedUser returns the shares of every owner and initiator. This
* is needed for ownerless mounts (e.g. groupfolders), where no single user can
* be used to filter the shares.
*
* @param FederatedUser|null $federatedUser
* @param Folder $node
* @param bool $reshares
* @param bool $shallow Whether the method should stop at the first level, or look into sub-folders.
Expand All @@ -443,15 +447,17 @@ public function getSharesBy(
* @throws RequestBuilderException
*/
public function getSharesInFolder(
FederatedUser $federatedUser,
?FederatedUser $federatedUser,
Folder $node,
bool $reshares,
bool $shallow = true,
): array {
$qb = $this->getShareSelectSql();

$qb->leftJoinCircle(CoreQueryBuilder::SHARE, null, 'share_with');
$qb->limitToShareOwner(CoreQueryBuilder::SHARE, $federatedUser, $reshares);
if ($federatedUser !== null) {
$qb->limitToShareOwner(CoreQueryBuilder::SHARE, $federatedUser, $reshares);
}
$qb->leftJoinFileCache(CoreQueryBuilder::SHARE);

$aliasFileCache = $qb->generateAlias(CoreQueryBuilder::SHARE, CoreQueryBuilder::FILE_CACHE);
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/ShareWrapperService.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ public function getSharesBy(
* @throws RequestBuilderException
*/
public function getSharesInFolder(
FederatedUser $federatedUser,
?FederatedUser $federatedUser,
Folder $node,
bool $reshares,
bool $shallow = true,
Expand Down
59 changes: 49 additions & 10 deletions lib/ShareByCircleProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use OCA\Circles\FederatedItems\Files\FileShare;
use OCA\Circles\FederatedItems\Files\FileUnshare;
use OCA\Circles\Model\Federated\FederatedEvent;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\Member;
use OCA\Circles\Model\Probes\CircleProbe;
use OCA\Circles\Model\Probes\DataProbe;
Expand Down Expand Up @@ -60,14 +61,15 @@
use OCP\Share\IShare;
use OCP\Share\IShareProvider;
use OCP\Share\IShareProviderGetUsers;
use OCP\Share\IShareProviderSupportsAllSharesInFolder;
use Psr\Log\LoggerInterface;

/**
* Class ShareByCircleProvider
*
* @package OCA\Circles
*/
class ShareByCircleProvider implements IShareProvider, IPartialShareProvider, IShareProviderGetUsers {
class ShareByCircleProvider implements IShareProvider, IPartialShareProvider, IShareProviderGetUsers, IShareProviderSupportsAllSharesInFolder {
use TArrayTools;
use TStringTools;
use TNCLogger;
Expand Down Expand Up @@ -321,7 +323,7 @@ public function restore(IShare $share, string $recipient): IShare {
* @param bool $reshares
* @param bool $shallow Whether the method should stop at the first level, or look into sub-folders.
*
* @return array
* @return array<int, list<IShare>>
* @throws ContactAddressBookNotFoundException
* @throws ContactFormatException
* @throws ContactNotFoundException
Expand All @@ -336,21 +338,58 @@ public function restore(IShare $share, string $recipient): IShare {
*/
public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = true): array {
$federatedUser = $this->federatedUserService->getLocalFederatedUser($userId);
$wrappedShares = $this->shareWrapperService->getSharesInFolder(
$federatedUser,
$node,
$reshares,
$shallow
);

return $this->getSharesInFolderInternal($federatedUser, $node, $reshares, $shallow);
}

/**
* Get all shares in a folder, regardless of the share owner or initiator.
* Used for ownerless mounts (e.g. groupfolders) where there is no single
* user to filter shares by.
*
* @return array<int, list<IShare>>
* @throws ContactAddressBookNotFoundException
* @throws ContactFormatException
* @throws ContactNotFoundException
* @throws InvalidIdException
* @throws InvalidPathException
* @throws NotFoundException
* @throws RequestBuilderException
* @throws SingleCircleNotFoundException
*/
public function getAllSharesInFolder(Folder $node): array {
return $this->getSharesInFolderInternal(null, $node, false);
}

/**
* @return array<int, list<IShare>>
* @throws ContactAddressBookNotFoundException
* @throws ContactFormatException
* @throws ContactNotFoundException
* @throws InvalidIdException
* @throws InvalidPathException
* @throws NotFoundException
* @throws RequestBuilderException
* @throws SingleCircleNotFoundException
*/
private function getSharesInFolderInternal(
?FederatedUser $federatedUser,
Folder $node,
bool $reshares,
bool $shallow = true,
): array {
$wrappedShares = $this->shareWrapperService->getSharesInFolder($federatedUser, $node, $reshares, $shallow);

$result = [];
foreach ($wrappedShares as $wrappedShare) {
if (!array_key_exists($wrappedShare->getFileSource(), $result)) {
$result[$wrappedShare->getFileSource()] = [];
}
if ($wrappedShare->getFileCache()->isAccessible()) {
$result[$wrappedShare->getFileSource()][]
= $wrappedShare->getShare($this->rootFolder, $this->userManager, $this->urlGenerator);
$share = $wrappedShare->getShare($this->rootFolder, $this->userManager, $this->urlGenerator);
if ($share !== null) {
$result[$wrappedShare->getFileSource()][] = $share;
}
} else {
$this->logger->debug('shared document is not available anymore', ['wrappedShare' => $wrappedShare]);
if ($wrappedShare->getFileCache()->getPath() === '') {
Expand Down
1 change: 0 additions & 1 deletion tests/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,6 @@
<code><![CDATA[IShare]]></code>
</InvalidNullableReturnType>
<LessSpecificImplementedReturnType>
<code><![CDATA[array]]></code>
<code><![CDATA[iterable]]></code>
</LessSpecificImplementedReturnType>
<NullableReturnStatement>
Expand Down
145 changes: 145 additions & 0 deletions tests/unit/lib/ShareByCircleProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\Tests\Unit;

use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\FileCacheWrapper;
use OCA\Circles\Model\ShareWrapper;
use OCA\Circles\Service\CircleService;
use OCA\Circles\Service\EventService;
use OCA\Circles\Service\FederatedEventService;
use OCA\Circles\Service\FederatedUserService;
use OCA\Circles\Service\ShareTokenService;
use OCA\Circles\Service\ShareWrapperService;
use OCA\Circles\ShareByCircleProvider;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\Share\IShare;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class ShareByCircleProviderTest extends TestCase {
private ShareByCircleProvider $provider;
private ShareWrapperService&MockObject $shareWrapperService;
private FederatedUserService&MockObject $federatedUserService;

protected function setUp(): void {
parent::setUp();

$this->shareWrapperService = $this->createMock(ShareWrapperService::class);
$this->federatedUserService = $this->createMock(FederatedUserService::class);

$this->provider = new ShareByCircleProvider(
$this->createMock(IUserManager::class),
$this->createMock(IRootFolder::class),
$this->createMock(IL10N::class),
$this->createMock(LoggerInterface::class),
$this->createMock(IURLGenerator::class),
$this->shareWrapperService,
$this->createMock(ShareTokenService::class),
$this->federatedUserService,
$this->createMock(FederatedEventService::class),
$this->createMock(CircleService::class),
$this->createMock(EventService::class),
);
}

public function testGetAllSharesInFolderQueriesWithoutFederatedUser(): void {
$node = $this->createMock(Folder::class);

$this->federatedUserService->expects($this->never())
->method('getLocalFederatedUser');
$this->shareWrapperService->expects($this->once())
->method('getSharesInFolder')
->with(null, $node, false, true)
->willReturn([]);

$this->assertSame([], $this->provider->getAllSharesInFolder($node));
}

public function testGetAllSharesInFolderGroupsSharesByFileSource(): void {
$node = $this->createMock(Folder::class);
$firstShare = $this->createMock(IShare::class);
$secondShare = $this->createMock(IShare::class);
$thirdShare = $this->createMock(IShare::class);

$this->shareWrapperService->method('getSharesInFolder')
->willReturn([
$this->createWrappedShare(42, $firstShare),
$this->createWrappedShare(42, $secondShare),
$this->createWrappedShare(1337, $thirdShare),
]);

$this->assertSame([
42 => [$firstShare, $secondShare],
1337 => [$thirdShare],
], $this->provider->getAllSharesInFolder($node));
}

public function testGetAllSharesInFolderSkipsInaccessibleShares(): void {
$node = $this->createMock(Folder::class);

$this->shareWrapperService->method('getSharesInFolder')
->willReturn([
$this->createWrappedShare(42, $this->createMock(IShare::class), false),
]);

$this->assertSame([42 => []], $this->provider->getAllSharesInFolder($node));
}

public function testGetSharesInFolderQueriesWithFederatedUser(): void {
$node = $this->createMock(Folder::class);
$federatedUser = $this->createMock(FederatedUser::class);

$this->federatedUserService->expects($this->once())
->method('getLocalFederatedUser')
->with('test-user')
->willReturn($federatedUser);
$this->shareWrapperService->expects($this->once())
->method('getSharesInFolder')
->with($federatedUser, $node, true, true)
->willReturn([]);

$this->assertSame([], $this->provider->getSharesInFolder('test-user', $node, true));
}

public function testGetSharesInFolderForwardsNonShallow(): void {
$node = $this->createMock(Folder::class);
$federatedUser = $this->createMock(FederatedUser::class);

$this->federatedUserService->method('getLocalFederatedUser')
->willReturn($federatedUser);
$this->shareWrapperService->expects($this->once())
->method('getSharesInFolder')
->with($federatedUser, $node, true, false)
->willReturn([]);

$this->assertSame([], $this->provider->getSharesInFolder('test-user', $node, true, false));
}

private function createWrappedShare(
int $fileSource,
IShare $share,
bool $accessible = true,
): ShareWrapper&MockObject {
$fileCache = $this->createMock(FileCacheWrapper::class);
$fileCache->method('isAccessible')->willReturn($accessible);
$fileCache->method('getPath')->willReturn('__groupfolders/1/document.md');

$wrappedShare = $this->createMock(ShareWrapper::class);
$wrappedShare->method('getFileSource')->willReturn($fileSource);
$wrappedShare->method('getFileCache')->willReturn($fileCache);
$wrappedShare->method('getShare')->willReturn($share);

return $wrappedShare;
}
}
Loading