diff --git a/lib/Controller/AiController.php b/lib/Controller/AiController.php index baef0c65027..25ee5a93323 100644 --- a/lib/Controller/AiController.php +++ b/lib/Controller/AiController.php @@ -15,6 +15,7 @@ use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; use OCP\IRequest; class AiController extends ApiController { @@ -33,6 +34,8 @@ public function tagFile(int $fileId): DataResponse { return new DataResponse([]); } catch (NotFoundException) { return new DataResponse([], Http::STATUS_NOT_FOUND); + } catch (NotPermittedException) { + return new DataResponse([], Http::STATUS_FORBIDDEN); } } } diff --git a/lib/Service/AiTagService.php b/lib/Service/AiTagService.php index 5494e8de1e7..d8284f9f3c5 100644 --- a/lib/Service/AiTagService.php +++ b/lib/Service/AiTagService.php @@ -9,8 +9,10 @@ namespace OCA\Text\Service; +use OCP\Constants; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; use OCP\SystemTag\ISystemTagObjectMapper; use Psr\Log\LoggerInterface; @@ -29,22 +31,35 @@ public function __construct( * @return void * * @throws NotFoundException + * @throws NotPermittedException * * @since 34.0.0 (ISystemTagObjectMapper::assignGeneratedByAITag) */ public function tagFileAsAiGenerated(int $fileId): void { - if ($this->userId === null || $this->rootFolder->getUserFolder($this->userId)->getFirstNodeById($fileId) === null) { + if ($this->userId === null) { throw new NotFoundException('File not found'); } - try { - $this->systemTagObjectMapper->assignGeneratedByAITag((string)$fileId, 'files'); - } catch (\Exception $e) { - $this->logger->warning('Failed to tag file {fileId} as AI-generated: {error}', [ - 'fileId' => $fileId, - 'error' => $e->getMessage(), - 'exception' => $e, - ]); + $nodes = $this->rootFolder->getUserFolder($this->userId)->getById($fileId); + if ($nodes === []) { + throw new NotFoundException('File not found'); + } + + foreach ($nodes as $node) { + if (($node->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE) { + try { + $this->systemTagObjectMapper->assignGeneratedByAITag((string)$fileId, 'files'); + } catch (\Exception $e) { + $this->logger->warning('Failed to tag file {fileId} as AI-generated: {error}', [ + 'fileId' => $fileId, + 'error' => $e->getMessage(), + 'exception' => $e, + ]); + } + return; + } } + + throw new NotPermittedException('No permission to assign tag to ' . $fileId); } } diff --git a/tests/unit/Controller/AiControllerTest.php b/tests/unit/Controller/AiControllerTest.php index 9eca351068d..2840163fa15 100644 --- a/tests/unit/Controller/AiControllerTest.php +++ b/tests/unit/Controller/AiControllerTest.php @@ -13,6 +13,7 @@ use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; use OCP\IRequest; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -64,4 +65,16 @@ public function testTagFileReturnsNotFoundWhenInaccessible(): void { $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus()); $this->assertSame([], $response->getData()); } + + public function testTagFileReturnsForbiddenWhenNotPermitted(): void { + $this->aiTagService->expects($this->once()) + ->method('tagFileAsAiGenerated') + ->with(42) + ->willThrowException(new NotPermittedException('No permission to assign tag to 42')); + + $response = $this->controller->tagFile(42); + + $this->assertSame(Http::STATUS_FORBIDDEN, $response->getStatus()); + $this->assertSame([], $response->getData()); + } } diff --git a/tests/unit/Service/AiTagServiceTest.php b/tests/unit/Service/AiTagServiceTest.php index ac2a63e211a..c33d5753845 100644 --- a/tests/unit/Service/AiTagServiceTest.php +++ b/tests/unit/Service/AiTagServiceTest.php @@ -9,10 +9,12 @@ namespace OCA\Text\Service; +use OCP\Constants; use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\IUserFolder; use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; use OCP\SystemTag\ISystemTagObjectMapper; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; @@ -61,9 +63,9 @@ public function testTagFileAsAiGeneratedThrowsWhenUserMissing(): void { public function testTagFileAsAiGeneratedThrowsWhenFileNotAccessible(): void { $this->userFolder->expects($this->once()) - ->method('getFirstNodeById') + ->method('getById') ->with(42) - ->willReturn(null); + ->willReturn([]); $this->systemTagObjectMapper->expects($this->never()) ->method('assignGeneratedByAITag'); @@ -72,12 +74,32 @@ public function testTagFileAsAiGeneratedThrowsWhenFileNotAccessible(): void { $this->service->tagFileAsAiGenerated(42); } - public function testTagFileAsAiGeneratedTagsWhenAccessible(): void { + public function testTagFileAsAiGeneratedThrowsWhenFileIsReadOnly(): void { $file = $this->createMock(File::class); + $file->method('getPermissions')->willReturn(Constants::PERMISSION_READ); + + $this->userFolder->expects($this->once()) + ->method('getById') + ->with(42) + ->willReturn([$file]); + + $this->systemTagObjectMapper->expects($this->never()) + ->method('assignGeneratedByAITag'); + + $this->expectException(NotPermittedException::class); + $this->service->tagFileAsAiGenerated(42); + } + + public function testTagFileAsAiGeneratedTagsWhenUpdateIsAllowed(): void { + $readOnly = $this->createMock(File::class); + $readOnly->method('getPermissions')->willReturn(Constants::PERMISSION_READ); + $writable = $this->createMock(File::class); + $writable->method('getPermissions')->willReturn(Constants::PERMISSION_UPDATE); + $this->userFolder->expects($this->once()) - ->method('getFirstNodeById') + ->method('getById') ->with(42) - ->willReturn($file); + ->willReturn([$readOnly, $writable]); $this->systemTagObjectMapper->expects($this->once()) ->method('assignGeneratedByAITag')