diff --git a/lib/Controller/ThreadController.php b/lib/Controller/ThreadController.php index 7e364feeaf2..2c9c9583da9 100644 --- a/lib/Controller/ThreadController.php +++ b/lib/Controller/ThreadController.php @@ -185,10 +185,11 @@ public function getThread(int $threadId): DataResponse { * @param int $threadId The thread ID to get the info for * @psalm-param non-negative-int $threadId * @param string $threadTitle New thread title, must not be empty - * @return DataResponse|DataResponse|DataResponse + * @return DataResponse|DataResponse|DataResponse|DataResponse * * 200: Thread renamed successfully * 400: When the provided title is empty + * 403: Not allowed, either not the original author or not a moderator * 404: Thread not found */ #[FederationSupported] @@ -215,11 +216,25 @@ public function renameThread(int $threadId, string $threadTitle): DataResponse { return new DataResponse(['error' => 'thread'], Http::STATUS_NOT_FOUND); } - # FIXME Only allow for moderator and original author + $attendee = $this->participant->getAttendee(); + $isOwnMessage = false; + try { + $comment = $this->chatManager->getComment($this->room, (string)$threadId); + $isOwnMessage = $comment->getActorType() === $attendee->getActorType() + && $comment->getActorId() === $attendee->getActorId(); + } catch (NotFoundException) { + // Root message expired, only moderators can edit + } + + if (!$isOwnMessage + && !$this->participant->hasModeratorPermissions(false)) { + // Actor is not a moderator or not the owner of the message + return new DataResponse(['error' => 'permission'], Http::STATUS_FORBIDDEN); + } try { $this->threadService->renameThread($thread, $threadTitle); - } catch (\InvalidArgumentException $e) { + } catch (\InvalidArgumentException) { return new DataResponse(['error' => 'title'], Http::STATUS_BAD_REQUEST); } diff --git a/openapi-full.json b/openapi-full.json index 19da250d1a3..95414374019 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -23686,6 +23686,47 @@ } } }, + "403": { + "description": "Not allowed, either not the original author or not a moderator", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "permission" + ] + } + } + } + } + } + } + } + } + } + }, "404": { "description": "Thread not found", "content": { diff --git a/openapi.json b/openapi.json index 01030d80d8f..47b48bf5ec0 100644 --- a/openapi.json +++ b/openapi.json @@ -23591,6 +23591,47 @@ } } }, + "403": { + "description": "Not allowed, either not the original author or not a moderator", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "permission" + ] + } + } + } + } + } + } + } + } + } + }, "404": { "description": "Thread not found", "content": { diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 3449628793e..60f52fb6d0a 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -11573,6 +11573,23 @@ export interface operations { }; }; }; + /** @description Not allowed, either not the original author or not a moderator */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "permission"; + }; + }; + }; + }; + }; /** @description Thread not found */ 404: { headers: { diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 6af54752c87..015334c2167 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -11035,6 +11035,23 @@ export interface operations { }; }; }; + /** @description Not allowed, either not the original author or not a moderator */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "permission"; + }; + }; + }; + }; + }; /** @description Thread not found */ 404: { headers: { diff --git a/tests/integration/features/chat-4/threads.feature b/tests/integration/features/chat-4/threads.feature index 876d69d466e..43506182f62 100644 --- a/tests/integration/features/chat-4/threads.feature +++ b/tests/integration/features/chat-4/threads.feature @@ -47,6 +47,30 @@ Feature: chat-4/threads | room | users | participant1 | user_added | {actor} added you | !ISSET | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname","mention-id":"participant1"},"user":{"type":"user","id":"participant2","name":"participant2-displayname","mention-id":"participant2"}} | | room | users | participant1 | conversation_created | {actor} created the conversation | !ISSET | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname","mention-id":"participant1"}} | + Scenario: Non moderators can only rename their own threads + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds user "participant2" to room "room" with 200 (v4) + And user "participant1" sends thread "Thread 1" with message "Message 1" to room "room" with 201 + And user "participant2" sends thread "Thread 2" with message "Message 2" to room "room" with 201 + Then user "participant1" sees the following recent threads in room "room" with 200 + | t.id | t.title | t.numReplies | t.lastMessage | a.notificationLevel | firstMessage | lastMessage | + | Message 2 | Thread 2 | 0 | 0 | 0 | Message 2 | NULL | + | Message 1 | Thread 1 | 0 | 0 | 0 | Message 1 | NULL | + And user "participant2" renames thread "Thread 1" to "No permissions" in room "room" with 403 + And user "participant2" renames thread "Thread 2" to "My own thread" in room "room" with 200 + Then user "participant1" sees the following recent threads in room "room" with 200 + | t.id | t.title | t.numReplies | t.lastMessage | a.notificationLevel | firstMessage | lastMessage | + | Message 2 | My own thread | 0 | 0 | 0 | Message 2 | NULL | + | Message 1 | Thread 1 | 0 | 0 | 0 | Message 1 | NULL | + And user "participant1" renames thread "Thread 1" to "Moderator thread" in room "room" with 200 + And user "participant1" renames thread "Thread 2" to "User thread" in room "room" with 200 + Then user "participant1" sees the following recent threads in room "room" with 200 + | t.id | t.title | t.numReplies | t.lastMessage | a.notificationLevel | firstMessage | lastMessage | + | Message 2 | User thread | 0 | 0 | 0 | Message 2 | NULL | + | Message 1 | Moderator thread | 0 | 0 | 0 | Message 1 | NULL | + Scenario: Responding without replying does not trigger a notification Given user "participant1" creates room "room" (v4) | roomType | 2 |