diff --git a/lib/Events/ApprovalStateChangedEvent.php b/lib/Events/ApprovalStateChangedEvent.php new file mode 100644 index 00000000..0027e08b --- /dev/null +++ b/lib/Events/ApprovalStateChangedEvent.php @@ -0,0 +1,67 @@ +fileId; + } + + public function getRuleId(): int { + return $this->ruleId; + } + + /** + * One of Application::STATE_PENDING, Application::STATE_APPROVED or Application::STATE_REJECTED + */ + public function getNewState(): int { + return $this->newState; + } + + /** + * User who performed the action: + * the requester on request, the approver on approve/reject + */ + public function getActorUserId(): ?string { + return $this->actorUserId; + } + + /** + * User who requested the approval, if known + */ + public function getRequesterUserId(): ?string { + return $this->requesterUserId; + } +} diff --git a/lib/Service/ApprovalService.php b/lib/Service/ApprovalService.php index 48db25b9..3d9408c6 100644 --- a/lib/Service/ApprovalService.php +++ b/lib/Service/ApprovalService.php @@ -10,8 +10,10 @@ use DateTime; use OCA\Approval\Activity\ActivityManager; use OCA\Approval\AppInfo\Application; +use OCA\Approval\Events\ApprovalStateChangedEvent; use OCA\Approval\Exceptions\OutdatedEtagException; use OCP\App\IAppManager; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\ICachedMountFileInfo; use OCP\Files\Config\IUserMountCache; use OCP\Files\FileInfo; @@ -50,6 +52,7 @@ public function __construct( private LoggerInterface $logger, private ?string $userId, private IUserMountCache $userMountCache, + private IEventDispatcher $eventDispatcher, ) { } @@ -381,8 +384,17 @@ public function approve(int $fileId, ?string $userId, string $etag, string $mess try { if ($this->tagObjectMapper->haveTag((string)$fileId, 'files', $rule['tagPending']) && $this->userIsAuthorizedByRule($userId, $rule, 'approvers')) { + // get the requester before storeAction() replaces the pending activity row + $pendingAction = $this->ruleService->getLastAction($fileId, $ruleId, Application::STATE_PENDING); + // store activity in our tables $this->ruleService->storeAction($fileId, $ruleId, $userId, Application::STATE_APPROVED, $message); + + // dispatch before the tags change so listeners reacting to the tag events have the context + $this->eventDispatcher->dispatchTyped( + new ApprovalStateChangedEvent($fileId, $ruleId, Application::STATE_APPROVED, $userId, $pendingAction['userId'] ?? null) + ); + // Change tags $this->tagObjectMapper->assignTags((string)$fileId, 'files', $rule['tagApproved']); $this->tagObjectMapper->unassignTags((string)$fileId, 'files', $rule['tagPending']); @@ -424,9 +436,17 @@ public function reject(int $fileId, ?string $userId, string $etag, string $messa try { if ($this->tagObjectMapper->haveTag((string)$fileId, 'files', $rule['tagPending']) && $this->userIsAuthorizedByRule($userId, $rule, 'approvers')) { + // get the requester before storeAction() replaces the pending activity row + $pendingAction = $this->ruleService->getLastAction($fileId, $ruleId, Application::STATE_PENDING); + // store activity in our tables $this->ruleService->storeAction($fileId, $ruleId, $userId, Application::STATE_REJECTED, $message); + // dispatch before the tags change so listeners reacting to the tag events have the context + $this->eventDispatcher->dispatchTyped( + new ApprovalStateChangedEvent($fileId, $ruleId, Application::STATE_REJECTED, $userId, $pendingAction['userId'] ?? null) + ); + // Change tags $this->tagObjectMapper->assignTags((string)$fileId, 'files', $rule['tagRejected']); $this->tagObjectMapper->unassignTags((string)$fileId, 'files', $rule['tagPending']); @@ -485,6 +505,11 @@ public function request(int $fileId, int $ruleId, ?string $requesterUserId, bool // store activity in our tables $this->ruleService->storeAction($fileId, $ruleId, $requesterUserId, Application::STATE_PENDING); + // dispatch before the tag is assigned so listeners reacting to the tag event have the context + $this->eventDispatcher->dispatchTyped( + new ApprovalStateChangedEvent($fileId, $ruleId, Application::STATE_PENDING, $requesterUserId, $requesterUserId) + ); + $this->tagObjectMapper->assignTags((string)$fileId, 'files', $rule['tagPending']); // still produce an activity entry for the user who requests @@ -530,6 +555,10 @@ public function requestViaTagAssignment(int $fileId, int $ruleId, string $reques // store activity in our tables $this->ruleService->storeAction($fileId, $ruleId, $requesterUserId, Application::STATE_PENDING); + $this->eventDispatcher->dispatchTyped( + new ApprovalStateChangedEvent($fileId, $ruleId, Application::STATE_PENDING, $requesterUserId, $requesterUserId) + ); + // still produce an activity entry for the user who requests $this->activityManager->triggerEvent( ActivityManager::APPROVAL_OBJECT_NODE, $fileId, diff --git a/tests/unit/Service/ApprovalServiceTest.php b/tests/unit/Service/ApprovalServiceTest.php index 40da6fea..194cd43a 100644 --- a/tests/unit/Service/ApprovalServiceTest.php +++ b/tests/unit/Service/ApprovalServiceTest.php @@ -10,7 +10,9 @@ use ChristophWurst\Nextcloud\Testing\TestCase; use OCA\Approval\Activity\ActivityManager; use OCA\Approval\AppInfo\Application; +use OCA\Approval\Events\ApprovalStateChangedEvent; use OCP\App\IAppManager; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\IUserMountCache; use OCP\Files\IRootFolder; use OCP\ICacheFactory; @@ -97,7 +99,8 @@ protected function setUp(): void { $c->get(IL10N::class), $c->get(LoggerInterface::class), 'user1', - $c->get(IUserMountCache::class) + $c->get(IUserMountCache::class), + $c->get(IEventDispatcher::class) ); // add some tags @@ -427,4 +430,38 @@ public function testRequestWithCreateSharesWhenUserCannotShareReturnsError(): vo $this->ruleService->deleteRule($ruleId); } + + public function testApprovalStateChangedEventIsEmitted(): void { + $app = new Application(); + $eventDispatcher = $app->getContainer()->get(IEventDispatcher::class); + + $emittedEvents = []; + $eventDispatcher->addListener(ApprovalStateChangedEvent::class, function (ApprovalStateChangedEvent $event) use (&$emittedEvents) { + $emittedEvents[] = $event; + }); + + // create a file + $uf1 = $this->root->getUserFolder('user1'); + $file = $uf1->newFile('eventFile.txt', 'content'); + + // requesting approval emits an event carrying the requester + $this->approvalService->request($file->getId(), $this->idRule1, 'user1', false); + $this->assertCount(1, $emittedEvents); + $event = $emittedEvents[0]; + $this->assertSame($file->getId(), $event->getFileId()); + $this->assertEquals($this->idRule1, $event->getRuleId()); + $this->assertSame(Application::STATE_PENDING, $event->getNewState()); + $this->assertSame('user1', $event->getActorUserId()); + $this->assertSame('user1', $event->getRequesterUserId()); + + // approving emits an event still carrying the requester + $this->approvalService->approve($file->getId(), 'user1', $file->getEtag()); + $this->assertCount(2, $emittedEvents); + $event = $emittedEvents[1]; + $this->assertSame($file->getId(), $event->getFileId()); + $this->assertEquals($this->idRule1, $event->getRuleId()); + $this->assertSame(Application::STATE_APPROVED, $event->getNewState()); + $this->assertSame('user1', $event->getActorUserId()); + $this->assertSame('user1', $event->getRequesterUserId()); + } }