From 43d1e8b58ca3ec1780c1681849c461ea19c82b3c Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 2 Oct 2026 16:27:40 +0200 Subject: [PATCH] fix(api): remove author email addresses from comments and activity getComment, getAllComments, getProjectActivity, getProjectActivities and getMyActivityStream returned the email address of the comment or event author to any project member. Comment events also carry it in their stored comment data. The models keep the column since the web UI needs it for avatars, the procedures drop it before answering. Fixes #5876 --- app/Api/Procedure/BaseProcedure.php | 30 +++++++++++ app/Api/Procedure/CommentProcedure.php | 4 +- app/Api/Procedure/MeProcedure.php | 2 +- app/Api/Procedure/ProjectProcedure.php | 4 +- .../Api/Procedure/CommentProcedureTest.php | 33 ++++++++++++ .../ProjectActivityProcedureTest.php | 54 +++++++++++++++++++ 6 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 tests/units/Api/Procedure/CommentProcedureTest.php create mode 100644 tests/units/Api/Procedure/ProjectActivityProcedureTest.php diff --git a/app/Api/Procedure/BaseProcedure.php b/app/Api/Procedure/BaseProcedure.php index aa00d0d..b6a8a82 100644 --- a/app/Api/Procedure/BaseProcedure.php +++ b/app/Api/Procedure/BaseProcedure.php @@ -42,6 +42,36 @@ abstract class BaseProcedure extends Base return is_array($users) ? array_map(array($this, 'filterUser'), $users) : $users; } + protected function filterComment($comment) + { + if (is_array($comment)) { + unset($comment['email']); + } + + return $comment; + } + + protected function filterComments($comments) + { + return is_array($comments) ? array_map(array($this, 'filterComment'), $comments) : $comments; + } + + protected function filterEvent(array $event) + { + unset($event['email']); + + if (isset($event['comment'])) { + $event['comment'] = $this->filterComment($event['comment']); + } + + return $event; + } + + protected function filterEvents(array $events) + { + return array_map(array($this, 'filterEvent'), $events); + } + protected function getClassName() { $reflection = new ReflectionClass(get_called_class()); diff --git a/app/Api/Procedure/CommentProcedure.php b/app/Api/Procedure/CommentProcedure.php index 3209924..f88b02f 100644 --- a/app/Api/Procedure/CommentProcedure.php +++ b/app/Api/Procedure/CommentProcedure.php @@ -17,7 +17,7 @@ class CommentProcedure extends BaseProcedure public function getComment($comment_id) { CommentAuthorization::getInstance($this->container)->check($this->getClassName(), 'getComment', $comment_id); - return $this->commentModel->getById($comment_id); + return $this->filterComment($this->commentModel->getById($comment_id)); } public function getAllComments($task_id) @@ -39,7 +39,7 @@ class CommentProcedure extends BaseProcedure $filteredComments[] = $comment; } - return $filteredComments; + return $this->filterComments($filteredComments); } public function removeComment($comment_id) diff --git a/app/Api/Procedure/MeProcedure.php b/app/Api/Procedure/MeProcedure.php index 3ccba0e..c1e1293 100644 --- a/app/Api/Procedure/MeProcedure.php +++ b/app/Api/Procedure/MeProcedure.php @@ -28,7 +28,7 @@ class MeProcedure extends BaseProcedure public function getMyActivityStream() { $project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId()); - return $this->helper->projectActivity->getProjectsEvents($project_ids, 100); + return $this->filterEvents($this->helper->projectActivity->getProjectsEvents($project_ids, 100)); } public function createMyPrivateProject($name, $description = null) diff --git a/app/Api/Procedure/ProjectProcedure.php b/app/Api/Procedure/ProjectProcedure.php index 6da00ae..8ad9629 100644 --- a/app/Api/Procedure/ProjectProcedure.php +++ b/app/Api/Procedure/ProjectProcedure.php @@ -97,13 +97,13 @@ class ProjectProcedure extends BaseProcedure ->check($this->getClassName(), 'getProjectActivities', $project_id); } - return $this->helper->projectActivity->getProjectsEvents($project_ids); + return $this->filterEvents($this->helper->projectActivity->getProjectsEvents($project_ids)); } public function getProjectActivity($project_id) { ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectActivity', $project_id); - return $this->helper->projectActivity->getProjectEvents($project_id); + return $this->filterEvents($this->helper->projectActivity->getProjectEvents($project_id)); } public function createProject($name, $description = null, $owner_id = 0, $identifier = null, $start_date = null, $end_date = null, $priority_default = null, $priority_start = null, $priority_end = null, $email = null) diff --git a/tests/units/Api/Procedure/CommentProcedureTest.php b/tests/units/Api/Procedure/CommentProcedureTest.php new file mode 100644 index 0000000..4be4517 --- /dev/null +++ b/tests/units/Api/Procedure/CommentProcedureTest.php @@ -0,0 +1,33 @@ +assertEquals(2, $this->container['userModel']->create(array('username' => 'owner', 'email' => 'owner@example.org'))); + $this->assertEquals(3, $this->container['userModel']->create(array('username' => 'peer'))); + $this->assertEquals(1, $this->container['projectModel']->create(array('name' => 'P1'))); + $this->assertTrue($this->container['projectUserRoleModel']->addUser(1, 2, Role::PROJECT_MANAGER)); + $this->assertTrue($this->container['projectUserRoleModel']->addUser(1, 3, Role::PROJECT_MEMBER)); + $this->assertEquals(1, $this->container['taskCreationModel']->create(array('title' => 'Task', 'project_id' => 1))); + $this->assertEquals(1, $this->container['commentModel']->create(array('task_id' => 1, 'user_id' => 2, 'comment' => 'hello'))); + + $this->container['userSession']->initialize($this->container['userModel']->getById(3)); + $procedure = new CommentProcedure($this->container); + + $comment = $procedure->getComment(1); + $this->assertEquals('owner', $comment['username']); + $this->assertArrayNotHasKey('email', $comment); + + $comments = $procedure->getAllComments(1); + $this->assertCount(1, $comments); + $this->assertEquals('owner', $comments[0]['username']); + $this->assertArrayNotHasKey('email', $comments[0]); + } +} diff --git a/tests/units/Api/Procedure/ProjectActivityProcedureTest.php b/tests/units/Api/Procedure/ProjectActivityProcedureTest.php new file mode 100644 index 0000000..6e6fb8c --- /dev/null +++ b/tests/units/Api/Procedure/ProjectActivityProcedureTest.php @@ -0,0 +1,54 @@ +createCommentEvent(); + $procedure = new ProjectProcedure($this->container); + + $this->assertEventsWithoutEmail($procedure->getProjectActivity(1)); + $this->assertEventsWithoutEmail($procedure->getProjectActivities(array(1))); + } + + public function testMyActivityStreamDoesNotExposeTheAuthorEmail() + { + $this->createCommentEvent(); + $procedure = new MeProcedure($this->container); + + $this->assertEventsWithoutEmail($procedure->getMyActivityStream()); + } + + private function assertEventsWithoutEmail(array $events) + { + $this->assertCount(1, $events); + $this->assertEquals('owner', $events[0]['author_username']); + $this->assertEquals('hello', $events[0]['comment']['comment']); + $this->assertStringNotContainsString('owner@example.org', json_encode($events)); + } + + private function createCommentEvent() + { + $this->assertEquals(2, $this->container['userModel']->create(array('username' => 'owner', 'email' => 'owner@example.org'))); + $this->assertEquals(3, $this->container['userModel']->create(array('username' => 'peer'))); + $this->assertEquals(1, $this->container['projectModel']->create(array('name' => 'P1'))); + $this->assertTrue($this->container['projectUserRoleModel']->addUser(1, 2, Role::PROJECT_MANAGER)); + $this->assertTrue($this->container['projectUserRoleModel']->addUser(1, 3, Role::PROJECT_MEMBER)); + $this->assertEquals(1, $this->container['taskCreationModel']->create(array('title' => 'Task', 'project_id' => 1))); + $this->assertEquals(1, $this->container['commentModel']->create(array('task_id' => 1, 'user_id' => 2, 'comment' => 'hello'))); + + $event = (new CommentEventBuilder($this->container))->withCommentId(1)->buildEvent(); + $this->assertNotFalse($this->container['projectActivityModel']->createEvent(1, 1, 2, CommentModel::EVENT_CREATE, $event->getAll())); + + $this->container['userSession']->initialize($this->container['userModel']->getById(3)); + } +}