diff --git a/lib/Folder/FolderManager.php b/lib/Folder/FolderManager.php index 0821956c5..6ae5369b4 100644 --- a/lib/Folder/FolderManager.php +++ b/lib/Folder/FolderManager.php @@ -830,6 +830,53 @@ public function getFoldersForCircle(string $circleId): array { return array_map($this->rowToFolder(...), $rows); } + /** + * Resolve the folder mounted with the given file cache root. Trash and + * versions mounts have their own root ids and are not matched. + * + * @throws Exception + */ + public function getFolderIdByRootId(int $rootId): ?int { + $query = $this->connection->getQueryBuilder(); + $query->select('folder_id') + ->from('group_folders') + ->where($query->expr()->eq('root_id', $query->createNamedParameter($rootId, IQueryBuilder::PARAM_INT))); + + $folderId = $query->executeQuery()->fetchOne(); + + return is_numeric($folderId) ? (int)$folderId : null; + } + + /** + * Circle single ids assigned to a folder, either as an applicable group or as + * a team space. Inverse of {@see FolderManager::getFoldersForCircle()}. + * + * @return list + * @throws Exception + */ + public function getCircleIdsForFolder(int $folderId): array { + $query = $this->connection->getQueryBuilder(); + $query->select('f.team_circle_id', 'g.circle_id') + ->from('group_folders', 'f') + ->leftJoin('f', 'group_folders_groups', 'g', $query->expr()->eq('f.folder_id', 'g.folder_id')) + ->where($query->expr()->eq('f.folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT))); + + /** @var list $rows */ + $rows = $query->executeQuery()->fetchAll(); + + // The left join repeats team_circle_id per applicable group row. + $circleIds = []; + foreach ($rows as $row) { + foreach ([$row['team_circle_id'], $row['circle_id']] as $circleId) { + if ($circleId !== null && $circleId !== '') { + $circleIds[$circleId] = true; + } + } + } + + return array_map('strval', array_keys($circleIds)); + } + /** * @param list $paths * @return list diff --git a/lib/TeamSpace/TeamSpaceProvider.php b/lib/TeamSpace/TeamSpaceProvider.php index 3264b4035..41119e359 100644 --- a/lib/TeamSpace/TeamSpaceProvider.php +++ b/lib/TeamSpace/TeamSpaceProvider.php @@ -11,12 +11,13 @@ use OCP\IL10N; use OCP\IURLGenerator; +use OCP\Teams\ITeamFileResolver; use OCP\Teams\ITeamFolderProvider; use OCP\Teams\Team; use OCP\Teams\TeamFolder; use OCP\Teams\TeamResource; -class TeamSpaceProvider implements ITeamFolderProvider { +class TeamSpaceProvider implements ITeamFolderProvider, ITeamFileResolver { public function __construct( private readonly TeamSpaceService $service, private readonly IL10N $l10n, @@ -97,11 +98,21 @@ public function isSharedWithTeam(string $teamId, string $resourceId): bool { return false; } - #[\Override] /** - * @return list + * Our resource ids are folder ids, not file ids. + * + * @return list */ + #[\Override] public function getTeamsForResource(string $resourceId): array { - return []; + return $this->service->getCircleIdsForFolder((int)$resourceId); + } + + /** + * @return list + */ + #[\Override] + public function getTeamsForFile(int $fileId): array { + return $this->service->getCircleIdsForFile($fileId); } } diff --git a/lib/TeamSpace/TeamSpaceService.php b/lib/TeamSpace/TeamSpaceService.php index a277ece6f..745183499 100644 --- a/lib/TeamSpace/TeamSpaceService.php +++ b/lib/TeamSpace/TeamSpaceService.php @@ -11,6 +11,8 @@ use OCA\GroupFolders\Folder\FolderDefinition; use OCA\GroupFolders\Folder\FolderManager; +use OCA\GroupFolders\Mount\MountProvider; +use OCP\Files\Config\IUserMountCache; use OCP\Teams\Team; use OCP\Teams\TeamFolder; use Psr\Log\LoggerInterface; @@ -34,6 +36,7 @@ class TeamSpaceService { public function __construct( private readonly FolderManager $folderManager, private readonly LoggerInterface $logger, + private readonly IUserMountCache $userMountCache, ) { } @@ -215,6 +218,37 @@ public function getGroupFoldersForCircle(string $circleId): array { ); } + /** + * @return list + */ + public function getCircleIdsForFolder(int $folderId): array { + return $this->folderManager->getCircleIdsForFolder($folderId); + } + + /** + * Circle single ids of the group folder a file lives in. Resolved through the + * mount cache, so files nested anywhere inside the folder are covered. + * + * @return list + */ + public function getCircleIdsForFile(int $fileId): array { + $circleIds = []; + foreach ($this->userMountCache->getMountsForFileId($fileId) as $mount) { + if ($mount->getMountProvider() !== MountProvider::class) { + continue; + } + + $folderId = $this->folderManager->getFolderIdByRootId($mount->getRootId()); + if ($folderId === null) { + continue; + } + + $circleIds = array_merge($circleIds, $this->getCircleIdsForFolder($folderId)); + } + + return array_values(array_unique($circleIds)); + } + /** * Whether the given circle (looked up by single id) owns a team space. */ diff --git a/tests/Folder/FolderManagerTest.php b/tests/Folder/FolderManagerTest.php index ed16d2424..d23de4b38 100644 --- a/tests/Folder/FolderManagerTest.php +++ b/tests/Folder/FolderManagerTest.php @@ -849,4 +849,54 @@ public function testGuardsLiftedAfterUnlink(): void { // No exception thrown — the folder is a regular team folder again. $this->addToAssertionCount(1); } + + public function testGetCircleIdsForFolderCoversBothWaysACircleIsLinked(): void { + $this->config->method('getSystemValueInt')->willReturn(FileInfo::SPACE_UNLIMITED); + $applied = $this->manager->createFolder('Design'); + $teamSpace = $this->manager->createFolder('Engineering'); + $this->addCircleApplicable($applied, 'circle-applied'); + $this->manager->setTeamCircleId($teamSpace, 'circle-owner'); + + $this->assertSame(['circle-applied'], $this->manager->getCircleIdsForFolder($applied)); + $this->assertSame(['circle-owner'], $this->manager->getCircleIdsForFolder($teamSpace)); + } + + public function testGetCircleIdsForFolderIgnoresGroupsAndUnknownFolders(): void { + $this->config->method('getSystemValueInt')->willReturn(FileInfo::SPACE_UNLIMITED); + $folderId = $this->manager->createFolder('Design'); + $this->manager->addApplicableGroup($folderId, 'g1'); + + $this->assertSame([], $this->manager->getCircleIdsForFolder($folderId)); + $this->assertSame([], $this->manager->getCircleIdsForFolder(0)); + } + + public function testGetCircleIdsForFolderReturnsEachCircleOnce(): void { + $this->config->method('getSystemValueInt')->willReturn(FileInfo::SPACE_UNLIMITED); + $folderId = $this->manager->createFolder('Design'); + $this->addCircleApplicable($folderId, 'circle-1'); + $this->manager->setTeamCircleId($folderId, 'circle-1'); + + $this->assertSame(['circle-1'], $this->manager->getCircleIdsForFolder($folderId)); + } + + public function testGetFolderIdByRootIdMatchesOnlyTheFolderMount(): void { + $this->config->method('getSystemValueInt')->willReturn(FileInfo::SPACE_UNLIMITED); + $folderId = $this->manager->createFolder('Design'); + $rootId = $this->manager->getAllFolders()[$folderId]->rootId; + + $this->assertSame($folderId, $this->manager->getFolderIdByRootId($rootId)); + $this->assertNull($this->manager->getFolderIdByRootId($rootId + 10000)); + } + + private function addCircleApplicable(int $folderId, string $circleId): void { + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->insert('group_folders_groups') + ->values([ + 'folder_id' => $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT), + 'group_id' => $query->createNamedParameter(''), + 'circle_id' => $query->createNamedParameter($circleId), + 'permissions' => $query->createNamedParameter(31, IQueryBuilder::PARAM_INT), + ]) + ->executeStatement(); + } } diff --git a/tests/TeamSpace/TeamSpaceProviderTest.php b/tests/TeamSpace/TeamSpaceProviderTest.php index 6a590dc0b..41135f563 100644 --- a/tests/TeamSpace/TeamSpaceProviderTest.php +++ b/tests/TeamSpace/TeamSpaceProviderTest.php @@ -70,4 +70,22 @@ public function testIsSharedWithTeamChecksAllFoldersAssignedToTeam(): void { $this->assertTrue($this->provider->isSharedWithTeam('team-1', '43')); $this->assertFalse($this->provider->isSharedWithTeam('team-1', '44')); } + + public function testGetTeamsForResourceReturnsCirclesAssignedToFolder(): void { + $this->service->expects($this->once()) + ->method('getCircleIdsForFolder') + ->with(43) + ->willReturn(['team-1', 'team-2']); + + $this->assertSame(['team-1', 'team-2'], $this->provider->getTeamsForResource('43')); + } + + public function testGetTeamsForFileResolvesThroughTheContainingFolder(): void { + $this->service->expects($this->once()) + ->method('getCircleIdsForFile') + ->with(1688) + ->willReturn(['team-1']); + + $this->assertSame(['team-1'], $this->provider->getTeamsForFile(1688)); + } } diff --git a/tests/TeamSpace/TeamSpaceServiceTest.php b/tests/TeamSpace/TeamSpaceServiceTest.php index 5a60de7e8..e1f043a2a 100644 --- a/tests/TeamSpace/TeamSpaceServiceTest.php +++ b/tests/TeamSpace/TeamSpaceServiceTest.php @@ -11,7 +11,10 @@ use OCA\GroupFolders\Folder\FolderDefinition; use OCA\GroupFolders\Folder\FolderManager; +use OCA\GroupFolders\Mount\MountProvider; use OCA\GroupFolders\TeamSpace\TeamSpaceService; +use OCP\Files\Config\ICachedMountInfo; +use OCP\Files\Config\IUserMountCache; use OCP\Teams\Team; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; @@ -19,15 +22,18 @@ class TeamSpaceServiceTest extends TestCase { private FolderManager&MockObject $folderManager; + private IUserMountCache&MockObject $userMountCache; private TeamSpaceService $service; #[\Override] protected function setUp(): void { parent::setUp(); $this->folderManager = $this->createMock(FolderManager::class); + $this->userMountCache = $this->createMock(IUserMountCache::class); $this->service = new TeamSpaceService( $this->folderManager, $this->createMock(LoggerInterface::class), + $this->userMountCache, ); } @@ -84,4 +90,53 @@ public function testPickBaseNameUsesDisplayName(): void { public function testSanitizeMountPointStripsControlCharsAndSeparators(): void { $this->assertSame('Engineering', $this->service->sanitizeMountPoint("Engi\nnee/rin\\g")); } + + public function testGetCircleIdsForFileResolvesThroughTheContainingFolder(): void { + $this->userMountCache->method('getMountsForFileId') + ->with(1688) + ->willReturn([$this->mount(MountProvider::class, 1686)]); + $this->folderManager->method('getFolderIdByRootId')->with(1686)->willReturn(3); + $this->folderManager->method('getCircleIdsForFolder')->with(3)->willReturn(['team-1']); + + $this->assertSame(['team-1'], $this->service->getCircleIdsForFile(1688)); + } + + public function testGetCircleIdsForFileIgnoresMountsFromOtherProviders(): void { + $this->userMountCache->method('getMountsForFileId') + ->willReturn([$this->mount('OCA\\Files_External\\Config\\ConfigAdapter', 99)]); + $this->folderManager->expects($this->never())->method('getFolderIdByRootId'); + + $this->assertSame([], $this->service->getCircleIdsForFile(1688)); + } + + public function testGetCircleIdsForFileIgnoresMountsWithoutAFolder(): void { + $this->userMountCache->method('getMountsForFileId') + ->willReturn([$this->mount(MountProvider::class, 1686)]); + $this->folderManager->method('getFolderIdByRootId')->willReturn(null); + $this->folderManager->expects($this->never())->method('getCircleIdsForFolder'); + + $this->assertSame([], $this->service->getCircleIdsForFile(1688)); + } + + public function testGetCircleIdsForFileReturnsEachTeamOnce(): void { + $this->userMountCache->method('getMountsForFileId')->willReturn([ + $this->mount(MountProvider::class, 1686), + $this->mount(MountProvider::class, 1786), + ]); + $this->folderManager->method('getFolderIdByRootId')->willReturnMap([[1686, 3], [1786, 4]]); + $this->folderManager->method('getCircleIdsForFolder')->willReturnMap([ + [3, ['team-1', 'team-2']], + [4, ['team-2']], + ]); + + $this->assertSame(['team-1', 'team-2'], $this->service->getCircleIdsForFile(1688)); + } + + private function mount(string $mountProvider, int $rootId): ICachedMountInfo&MockObject { + $mount = $this->createMock(ICachedMountInfo::class); + $mount->method('getMountProvider')->willReturn($mountProvider); + $mount->method('getRootId')->willReturn($rootId); + + return $mount; + } }