diff --git a/appinfo/routes/routesPollController.php b/appinfo/routes/routesPollController.php index 57f99d300d2..bcb89470d64 100644 --- a/appinfo/routes/routesPollController.php +++ b/appinfo/routes/routesPollController.php @@ -21,6 +21,8 @@ 'ocs' => [ /** @see \OCA\Talk\Controller\PollController::createPoll() */ ['name' => 'Poll#createPoll', 'url' => '/api/{apiVersion}/poll/{token}', 'verb' => 'POST', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\PollController::updateDraftPoll() */ + ['name' => 'Poll#updateDraftPoll', 'url' => '/api/{apiVersion}/poll/{token}/draft/{pollId}', 'verb' => 'POST', 'requirements' => $requirementsWithPollId], /** @see \OCA\Talk\Controller\PollController::getAllDraftPolls() */ ['name' => 'Poll#getAllDraftPolls', 'url' => '/api/{apiVersion}/poll/{token}/drafts', 'verb' => 'GET', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\PollController::showPoll() */ diff --git a/docs/capabilities.md b/docs/capabilities.md index 87802a453c7..b39f889ff1e 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -166,6 +166,7 @@ * `config => call => start-without-media` (local) - Boolean, whether media should be disabled when starting or joining a conversation * `config => call => max-duration` - Integer, maximum call duration in seconds. Please note that this should only be used with system cron and with a reasonable high value, due to the expended duration until the background job ran. * `config => call => blur-virtual-background` (local) - Boolean, whether blur background is set by default when joining a conversation ++ `edit-draft-poll` - Whether moderators can edit draft polls ## 21 * `config => conversations => force-passwords` - Whether passwords are enforced for public rooms diff --git a/docs/poll.md b/docs/poll.md index 7ea130a3adf..6fe24850500 100644 --- a/docs/poll.md +++ b/docs/poll.md @@ -30,6 +30,31 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` See [Poll data](#poll-data) +# Edit a draft poll in a conversation + +* Required capability: `edit-draft-poll` +* Method: `POST` +* Endpoint: `/poll/{token}/draft/{pollId}` +* Data: + +| field | type | Description | +|--------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `question` | string | The question of the poll | +| `options` | string[] | Array of strings with the voting options | +| `resultMode` | int | The result and voting mode of the poll, `0` means participants can immediatelly see the result and who voted for which option. `1` means the result is hidden until the poll is closed and then only the summary is published. | +| `maxVotes` | int | Maximum amount of options a participant can vote for | + +* Response: + - Status code: + + `200 OK` + + `400 Bad Request` Modifying poll is not possible + + `403 Forbidden` No permission to modify this poll + + `404 Not Found` When the draft poll could not be found + + - Data: + + See [Poll data](#poll-data) + ## Get state or result of a poll * Federation capability: `federation-v1` diff --git a/lib/Capabilities.php b/lib/Capabilities.php index b8817a629f2..4b1e5a12754 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -111,6 +111,7 @@ class Capabilities implements IPublicCapability { 'email-csv-import', 'conversation-creation-password', 'call-notification-state-api', + 'edit-draft-poll', ]; public const CONDITIONAL_FEATURES = [ diff --git a/lib/Controller/PollController.php b/lib/Controller/PollController.php index a9c3fb3d132..8f0808dd2fc 100644 --- a/lib/Controller/PollController.php +++ b/lib/Controller/PollController.php @@ -61,7 +61,7 @@ public function __construct( * @psalm-param Poll::MODE_* $resultMode Mode how the results will be shown * @param int $maxVotes Number of maximum votes per voter * @param bool $draft Whether the poll should be saved as a draft (only allowed for moderators and with `talk-polls-drafts` capability) - * @return DataResponse|DataResponse|DataResponse + * @return DataResponse|DataResponse|DataResponse * * 200: Draft created successfully * 201: Poll created successfully @@ -104,7 +104,7 @@ public function createPoll(string $question, array $options, int $resultMode, in ); } catch (PollPropertyException $e) { $this->logger->error('Error creating poll', ['exception' => $e]); - return new DataResponse(['error' => $e->getReason()], Http::STATUS_BAD_REQUEST); + return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_BAD_REQUEST); } if ($draft) { @@ -133,6 +133,79 @@ public function createPoll(string $question, array $options, int $resultMode, in return new DataResponse($this->renderPoll($poll), Http::STATUS_CREATED); } + /** + * Modify a draft poll + * + * Required capability: `edit-draft-poll` + * + * @param int $pollId The poll id + * @param string $question Question of the poll + * @param string[] $options Options of the poll + * @psalm-param list $options + * @param 0|1 $resultMode Mode how the results will be shown + * @psalm-param Poll::MODE_* $resultMode Mode how the results will be shown + * @param int $maxVotes Number of maximum votes per voter + * @return DataResponse|DataResponse|DataResponse + * + * 200: Draft modified successfully + * 400: Modifying poll is not possible + * 403: No permission to modify this poll + * 404: No draft poll exists + */ + #[FederationSupported] + #[PublicPage] + #[RequireModeratorOrNoLobby] + #[RequireParticipant] + #[RequirePermission(permission: RequirePermission::CHAT)] + #[RequireReadWriteConversation] + public function updateDraftPoll(int $pollId, string $question, array $options, int $resultMode, int $maxVotes): DataResponse { + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController::class); + return $proxy->updateDraftPoll($pollId, $this->room, $this->participant, $question, $options, $resultMode, $maxVotes); + } + + if ($this->room->getType() !== Room::TYPE_GROUP + && $this->room->getType() !== Room::TYPE_PUBLIC) { + return new DataResponse(['error' => PollPropertyException::REASON_ROOM], Http::STATUS_BAD_REQUEST); + } + + try { + $poll = $this->pollService->getPoll($this->room->getId(), $pollId); + } catch (DoesNotExistException $e) { + return new DataResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + + if (!$poll->isDraft()) { + return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_BAD_REQUEST); + } + + if (!$this->participant->hasModeratorPermissions() + && ($poll->getActorType() !== $this->participant->getAttendee()->getActorType() + || $poll->getActorId() !== $this->participant->getAttendee()->getActorId())) { + return new DataResponse(['error' => PollPropertyException::REASON_DRAFT], Http::STATUS_BAD_REQUEST); + } + + try { + $poll->setQuestion($question); + $poll->setOptions($options); + $poll->setResultMode($resultMode); + $poll->setMaxVotes($maxVotes); + } catch (PollPropertyException $e) { + $this->logger->error('Error modifying poll', ['exception' => $e]); + return new DataResponse(['error' => $e->getReason()], Http::STATUS_BAD_REQUEST); + } + + try { + $this->pollService->updatePoll($this->participant, $poll); + } catch (WrongPermissionsException $e) { + $this->logger->error('Error modifying poll', ['exception' => $e]); + return new DataResponse(['error' => 'poll'], Http::STATUS_FORBIDDEN); + } + + return new DataResponse($poll->renderAsDraft()); + } + /** * Get all drafted polls * @@ -273,7 +346,7 @@ public function votePoll(int $pollId, array $optionIds = []): DataResponse { * * @param int $pollId ID of the poll * @psalm-param non-negative-int $pollId - * @return DataResponse|DataResponse|DataResponse + * @return DataResponse|DataResponse|DataResponse * * 200: Poll closed successfully * 202: Poll draft was deleted successfully @@ -295,7 +368,7 @@ public function closePoll(int $pollId): DataResponse { try { $poll = $this->pollService->getPoll($this->room->getId(), $pollId); } catch (DoesNotExistException) { - return new DataResponse(['error' => 'poll'], Http::STATUS_NOT_FOUND); + return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_NOT_FOUND); } if ($poll->getStatus() === Poll::STATUS_DRAFT) { @@ -304,15 +377,13 @@ public function closePoll(int $pollId): DataResponse { } if ($poll->getStatus() === Poll::STATUS_CLOSED) { - return new DataResponse(['error' => 'poll'], Http::STATUS_BAD_REQUEST); + return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_BAD_REQUEST); } - $poll->setStatus(Poll::STATUS_CLOSED); - try { - $this->pollService->updatePoll($this->participant, $poll); - } catch (WrongPermissionsException) { - return new DataResponse(['error' => 'poll'], Http::STATUS_FORBIDDEN); + $this->pollService->closePoll($this->participant, $poll); + } catch (WrongPermissionsException $e) { + return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_FORBIDDEN); } $attendee = $this->participant->getAttendee(); diff --git a/lib/Exceptions/PollPropertyException.php b/lib/Exceptions/PollPropertyException.php index 56200dfe2e1..ecd6ba80e06 100644 --- a/lib/Exceptions/PollPropertyException.php +++ b/lib/Exceptions/PollPropertyException.php @@ -10,6 +10,7 @@ class PollPropertyException extends \InvalidArgumentException { public const REASON_DRAFT = 'draft'; + public const REASON_POLL = 'poll'; public const REASON_QUESTION = 'question'; public const REASON_OPTIONS = 'options'; public const REASON_ROOM = 'room'; diff --git a/lib/Federation/Proxy/TalkV1/Controller/PollController.php b/lib/Federation/Proxy/TalkV1/Controller/PollController.php index 8601806ecfa..5e4b53e92bc 100644 --- a/lib/Federation/Proxy/TalkV1/Controller/PollController.php +++ b/lib/Federation/Proxy/TalkV1/Controller/PollController.php @@ -10,6 +10,7 @@ namespace OCA\Talk\Federation\Proxy\TalkV1\Controller; use OCA\Talk\Exceptions\CannotReachRemoteException; +use OCA\Talk\Exceptions\PollPropertyException; use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest; use OCA\Talk\Federation\Proxy\TalkV1\UserConverter; use OCA\Talk\Participant; @@ -171,7 +172,46 @@ public function createPoll(Room $room, Participant $participant, string $questio } /** - * @return DataResponse|DataResponse|DataResponse + * @return DataResponse|DataResponse|DataResponse + * @throws CannotReachRemoteException + * + * 200: Draft created successfully + * 201: Poll created successfully + * 400: Creating poll is not possible + * + * @see \OCA\Talk\Controller\PollController::createPoll() + */ + public function updateDraftPoll(int $pollId, Room $room, Participant $participant, string $question, array $options, int $resultMode, int $maxVotes): DataResponse { + $proxy = $this->proxy->post( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/draft/' . $pollId, + [ + 'question' => $question, + 'options' => $options, + 'resultMode' => $resultMode, + 'maxVotes' => $maxVotes + ], + ); + + $status = $proxy->getStatusCode(); + if ($status === Http::STATUS_BAD_REQUEST) { + $data = $this->proxy->getOCSData($proxy, [Http::STATUS_BAD_REQUEST]); + return new DataResponse($data, Http::STATUS_BAD_REQUEST); + } + + /** @var TalkPollDraft $data */ + $data = $this->proxy->getOCSData($proxy, [Http::STATUS_OK, Http::STATUS_CREATED]); + $data = $this->userConverter->convertPoll($room, $data); + + if ($status === Http::STATUS_OK) { + return new DataResponse($data); + } + return new DataResponse($data); + } + + /** + * @return DataResponse|DataResponse|DataResponse * @throws CannotReachRemoteException * * 200: Poll closed successfully @@ -199,7 +239,7 @@ public function closePoll(Room $room, Participant $participant, int $pollId): Da } /** @var array{error?: string} $data */ $data = $this->proxy->getOCSData($proxy); - return new DataResponse(['error' => $data['error'] ?? 'poll'], $statusCode); + return new DataResponse(['error' => $data['error'] ?? PollPropertyException::REASON_POLL], $statusCode); } /** @var TalkPoll $data */ diff --git a/lib/Model/Poll.php b/lib/Model/Poll.php index 7a1cc8f4a96..09dabab23b7 100644 --- a/lib/Model/Poll.php +++ b/lib/Model/Poll.php @@ -9,6 +9,7 @@ namespace OCA\Talk\Model; +use OCA\Talk\Exceptions\PollPropertyException; use OCA\Talk\ResponseDefinitions; use OCP\AppFramework\Db\Entity; use OCP\DB\Types; @@ -18,10 +19,8 @@ * @method void setRoomId(int $roomId) * @method int getRoomId() * @psalm-method int<1, max> getRoomId() - * @method void setQuestion(string $question) * @method string getQuestion() * @psalm-method non-empty-string getQuestion() - * @method void setOptions(string $options) * @method string getOptions() * @method void setVotes(string $votes) * @method string getVotes() @@ -121,4 +120,57 @@ public function renderAsDraft(): array { 'maxVotes' => $this->getMaxVotes(), ]; } + + public function isDraft(): bool { + return $this->getStatus() === self::STATUS_DRAFT; + } + + /** + * @param array $options + * @return void + * @throws PollPropertyException + */ + public function setOptions(array $options): void { + try { + $jsonOptions = json_encode($options, JSON_THROW_ON_ERROR, 1); + } catch (\Exception) { + throw new PollPropertyException(PollPropertyException::REASON_OPTIONS); + } + + $validOptions = []; + foreach ($options as $option) { + if (!is_string($option)) { + throw new PollPropertyException(PollPropertyException::REASON_OPTIONS); + } + + $option = trim($option); + if ($option !== '') { + $validOptions[] = $option; + } + } + + if (count($validOptions) < 2) { + throw new PollPropertyException(PollPropertyException::REASON_OPTIONS); + } + + if (strlen($jsonOptions) > 60_000) { + throw new PollPropertyException(PollPropertyException::REASON_OPTIONS); + } + + $this->setter('options', [$jsonOptions]); + } + + /** + * @param string $question + * @return void + * @throws PollPropertyException + */ + public function setQuestion(string $question): void { + $question = trim($question); + if ($question === '' || strlen($question) > 32_000) { + throw new PollPropertyException(PollPropertyException::REASON_QUESTION); + } + + $this->setter('question', [$question]); + } } diff --git a/lib/Model/PollMapper.php b/lib/Model/PollMapper.php index 5e4e311a98f..9d021011ea6 100644 --- a/lib/Model/PollMapper.php +++ b/lib/Model/PollMapper.php @@ -44,12 +44,13 @@ public function getDraftsByRoomId(int $roomId): array { * @throws DoesNotExistException * @throws MultipleObjectsReturnedException */ - public function getByPollId(int $pollId): Poll { + public function getPollByRoomIdAndPollId(int $roomId, int $pollId): Poll { $query = $this->db->getQueryBuilder(); $query->select('*') ->from($this->getTableName()) - ->where($query->expr()->eq('id', $query->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))); + ->where($query->expr()->eq('id', $query->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))); return $this->findEntity($query); } diff --git a/lib/Service/PollService.php b/lib/Service/PollService.php index 0deeab56217..90bc5ea1dc5 100644 --- a/lib/Service/PollService.php +++ b/lib/Service/PollService.php @@ -33,51 +33,13 @@ public function __construct( * @throws PollPropertyException */ public function createPoll(int $roomId, string $actorType, string $actorId, string $displayName, string $question, array $options, int $resultMode, int $maxVotes, bool $draft): Poll { - $question = trim($question); - - if ($question === '' || strlen($question) > 32_000) { - throw new PollPropertyException(PollPropertyException::REASON_QUESTION); - } - - try { - json_encode($options, JSON_THROW_ON_ERROR, 1); - } catch (\Exception) { - throw new PollPropertyException(PollPropertyException::REASON_OPTIONS); - } - - $validOptions = []; - foreach ($options as $option) { - if (!is_string($option)) { - throw new PollPropertyException(PollPropertyException::REASON_OPTIONS); - } - - $option = trim($option); - if ($option !== '') { - $validOptions[] = $option; - } - } - - if (count($validOptions) < 2) { - throw new PollPropertyException(PollPropertyException::REASON_OPTIONS); - } - - try { - $jsonOptions = json_encode($validOptions, JSON_THROW_ON_ERROR, 1); - } catch (\Exception) { - throw new PollPropertyException(PollPropertyException::REASON_OPTIONS); - } - - if (strlen($jsonOptions) > 60_000) { - throw new PollPropertyException(PollPropertyException::REASON_OPTIONS); - } - $poll = new Poll(); $poll->setRoomId($roomId); $poll->setActorType($actorType); $poll->setActorId($actorId); $poll->setDisplayName($displayName); $poll->setQuestion($question); - $poll->setOptions($jsonOptions); + $poll->setOptions($options); $poll->setVotes(json_encode([])); $poll->setResultMode($resultMode); $poll->setMaxVotes($maxVotes); @@ -105,28 +67,39 @@ public function getDraftsForRoom(int $roomId): array { * @throws DoesNotExistException */ public function getPoll(int $roomId, int $pollId): Poll { - $poll = $this->pollMapper->getByPollId($pollId); + return $this->pollMapper->getPollByRoomIdAndPollId($roomId, $pollId); + } - if ($poll->getRoomId() !== $roomId) { - throw new DoesNotExistException('Room id mismatch'); + /** + * @param Participant $participant + * @param Poll $poll + * @throws WrongPermissionsException + */ + public function updatePoll(Participant $participant, Poll $poll): void { + if (!$participant->hasModeratorPermissions() + && ($poll->getActorType() !== $participant->getAttendee()->getActorType() + || $poll->getActorId() !== $participant->getAttendee()->getActorId())) { + // Only moderators and the author of the poll can update it + throw new WrongPermissionsException(); } - - return $poll; + $this->pollMapper->update($poll); } /** * @param Participant $participant * @param Poll $poll + * @return void * @throws WrongPermissionsException */ - public function updatePoll(Participant $participant, Poll $poll): void { + public function closePoll(Participant $participant, Poll $poll): void { if (!$participant->hasModeratorPermissions() - && ($poll->getActorType() !== $participant->getAttendee()->getActorType() - || $poll->getActorId() !== $participant->getAttendee()->getActorId())) { + && ($poll->getActorType() !== $participant->getAttendee()->getActorType() + || $poll->getActorId() !== $participant->getAttendee()->getActorId())) { // Only moderators and the author of the poll can update it throw new WrongPermissionsException(); } + $poll->setStatus(Poll::STATUS_CLOSED); $this->pollMapper->update($poll); } diff --git a/openapi-full.json b/openapi-full.json index eccc60c132b..fcc1f62d43f 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -9513,6 +9513,7 @@ "enum": [ "draft", "options", + "poll", "question", "room" ] @@ -9529,6 +9530,269 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/draft/{pollId}": { + "post": { + "operationId": "poll-update-draft-poll", + "summary": "Modify a draft poll", + "description": "Required capability: `edit-draft-poll`", + "tags": [ + "poll" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "question", + "options", + "resultMode", + "maxVotes" + ], + "properties": { + "question": { + "type": "string", + "description": "Question of the poll" + }, + "options": { + "type": "array", + "description": "Options of the poll", + "items": { + "type": "string" + } + }, + "resultMode": { + "type": "integer", + "format": "int64", + "enum": [ + 0, + 1 + ], + "description": "Mode how the results will be shown" + }, + "maxVotes": { + "type": "integer", + "format": "int64", + "description": "Number of maximum votes per voter" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "pollId", + "in": "path", + "description": "The poll id", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Draft modified successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/PollDraft" + } + } + } + } + } + } + } + }, + "400": { + "description": "Modifying poll is not possible", + "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": [ + "draft", + "options", + "question", + "room" + ] + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "No permission to modify this poll", + "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": [ + "draft", + "options", + "question", + "room" + ] + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "No draft poll exists", + "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" + } + } + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/drafts": { "get": { "operationId": "poll-get-all-draft-polls", @@ -10133,15 +10397,33 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "string" + "anyOf": [ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "poll" + ] + } + } + }, + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } } - } + ] } } } @@ -10171,15 +10453,33 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "string" + "anyOf": [ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "poll" + ] + } + } + }, + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } } - } + ] } } } @@ -10209,15 +10509,33 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "string" + "anyOf": [ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "poll" + ] + } + } + }, + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } } - } + ] } } } diff --git a/openapi.json b/openapi.json index e00793473bf..3a56f184da1 100644 --- a/openapi.json +++ b/openapi.json @@ -9400,6 +9400,7 @@ "enum": [ "draft", "options", + "poll", "question", "room" ] @@ -9416,6 +9417,269 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/draft/{pollId}": { + "post": { + "operationId": "poll-update-draft-poll", + "summary": "Modify a draft poll", + "description": "Required capability: `edit-draft-poll`", + "tags": [ + "poll" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "question", + "options", + "resultMode", + "maxVotes" + ], + "properties": { + "question": { + "type": "string", + "description": "Question of the poll" + }, + "options": { + "type": "array", + "description": "Options of the poll", + "items": { + "type": "string" + } + }, + "resultMode": { + "type": "integer", + "format": "int64", + "enum": [ + 0, + 1 + ], + "description": "Mode how the results will be shown" + }, + "maxVotes": { + "type": "integer", + "format": "int64", + "description": "Number of maximum votes per voter" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "pollId", + "in": "path", + "description": "The poll id", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Draft modified successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/PollDraft" + } + } + } + } + } + } + } + }, + "400": { + "description": "Modifying poll is not possible", + "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": [ + "draft", + "options", + "question", + "room" + ] + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "No permission to modify this poll", + "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": [ + "draft", + "options", + "question", + "room" + ] + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "No draft poll exists", + "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" + } + } + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/drafts": { "get": { "operationId": "poll-get-all-draft-polls", @@ -10020,15 +10284,33 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "string" + "anyOf": [ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "poll" + ] + } + } + }, + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } } - } + ] } } } @@ -10058,15 +10340,33 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "string" + "anyOf": [ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "poll" + ] + } + } + }, + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } } - } + ] } } } @@ -10096,15 +10396,33 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "string" + "anyOf": [ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "poll" + ] + } + } + }, + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } } - } + ] } } } diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index eafe8a13943..2dcb4a88f83 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -643,6 +643,26 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/draft/{pollId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Modify a draft poll + * @description Required capability: `edit-draft-poll` + */ + post: operations["poll-update-draft-poll"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/drafts": { parameters: { query?: never; @@ -5488,6 +5508,76 @@ export interface operations { }; }; /** @description Creating poll is not possible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "draft" | "options" | "poll" | "question" | "room"; + }; + }; + }; + }; + }; + }; + }; + "poll-update-draft-poll": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v1"; + token: string; + /** @description The poll id */ + pollId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Question of the poll */ + question: string; + /** @description Options of the poll */ + options: string[]; + /** + * Format: int64 + * @description Mode how the results will be shown + * @enum {integer} + */ + resultMode: 0 | 1; + /** + * Format: int64 + * @description Number of maximum votes per voter + */ + maxVotes: number; + }; + }; + }; + responses: { + /** @description Draft modified successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["PollDraft"]; + }; + }; + }; + }; + /** @description Modifying poll is not possible */ 400: { headers: { [name: string]: unknown; @@ -5504,6 +5594,39 @@ export interface operations { }; }; }; + /** @description No permission to modify this poll */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "draft" | "options" | "question" | "room"; + }; + }; + }; + }; + }; + /** @description No draft poll exists */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + error: string; + }; + }; + }; + }; + }; }; }; "poll-get-all-draft-polls": { @@ -5744,6 +5867,9 @@ export interface operations { ocs: { meta: components["schemas"]["OCSMeta"]; data: { + /** @enum {string} */ + error: "poll"; + } | { error: string; }; }; @@ -5760,6 +5886,9 @@ export interface operations { ocs: { meta: components["schemas"]["OCSMeta"]; data: { + /** @enum {string} */ + error: "poll"; + } | { error: string; }; }; @@ -5776,6 +5905,9 @@ export interface operations { ocs: { meta: components["schemas"]["OCSMeta"]; data: { + /** @enum {string} */ + error: "poll"; + } | { error: string; }; }; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 6a208fd0f90..9725ae87325 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -643,6 +643,26 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/draft/{pollId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Modify a draft poll + * @description Required capability: `edit-draft-poll` + */ + post: operations["poll-update-draft-poll"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/drafts": { parameters: { query?: never; @@ -4969,6 +4989,76 @@ export interface operations { }; }; /** @description Creating poll is not possible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "draft" | "options" | "poll" | "question" | "room"; + }; + }; + }; + }; + }; + }; + }; + "poll-update-draft-poll": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v1"; + token: string; + /** @description The poll id */ + pollId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Question of the poll */ + question: string; + /** @description Options of the poll */ + options: string[]; + /** + * Format: int64 + * @description Mode how the results will be shown + * @enum {integer} + */ + resultMode: 0 | 1; + /** + * Format: int64 + * @description Number of maximum votes per voter + */ + maxVotes: number; + }; + }; + }; + responses: { + /** @description Draft modified successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["PollDraft"]; + }; + }; + }; + }; + /** @description Modifying poll is not possible */ 400: { headers: { [name: string]: unknown; @@ -4985,6 +5075,39 @@ export interface operations { }; }; }; + /** @description No permission to modify this poll */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "draft" | "options" | "question" | "room"; + }; + }; + }; + }; + }; + /** @description No draft poll exists */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + error: string; + }; + }; + }; + }; + }; }; }; "poll-get-all-draft-polls": { @@ -5225,6 +5348,9 @@ export interface operations { ocs: { meta: components["schemas"]["OCSMeta"]; data: { + /** @enum {string} */ + error: "poll"; + } | { error: string; }; }; @@ -5241,6 +5367,9 @@ export interface operations { ocs: { meta: components["schemas"]["OCSMeta"]; data: { + /** @enum {string} */ + error: "poll"; + } | { error: string; }; }; @@ -5257,6 +5386,9 @@ export interface operations { ocs: { meta: components["schemas"]["OCSMeta"]; data: { + /** @enum {string} */ + error: "poll"; + } | { error: string; }; }; diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 82107eadc16..43e2d192fd2 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -2525,6 +2525,43 @@ public function createPoll(string $user, string $identifier, string $statusCode, } } + /** + * @Then /^user "([^"]*)" updates a draft poll in room "([^"]*)" with (\d+)(?: \((v1)\))?$/ + * + * @param string $user + * @param string $identifier + * @param string $statusCode + * @param string $apiVersion + */ + public function updateDraftPoll(string $user, string $identifier, string $statusCode, string $apiVersion = 'v1', ?TableNode $formData = null): void { + $data = $formData->getRowsHash(); + $data['options'] = json_decode($data['options'], true); + if ($data['resultMode'] === 'public') { + $data['resultMode'] = 0; + } elseif ($data['resultMode'] === 'hidden') { + $data['resultMode'] = 1; + } else { + throw new \Exception('Invalid result mode'); + } + if ($data['maxVotes'] === 'unlimited') { + $data['maxVotes'] = 0; + } + + $this->setCurrentUser($user); + $this->sendRequest( + 'POST', '/apps/spreed/api/' . $apiVersion . '/poll/' . self::$identifierToToken[$identifier] . '/draft/' . self::$questionToPollId[$data['question']], + $data + ); + $this->assertStatusCode($this->response, $statusCode); + + if ($statusCode !== '200') { + return; + } + + $response = $this->getDataFromResponse($this->response); + $this->assertPollEquals($data, $response); + } + /** * @Then /^user "([^"]*)" gets poll drafts for room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * diff --git a/tests/integration/features/chat-3/poll.feature b/tests/integration/features/chat-3/poll.feature index ab274d0193d..6ac06ec58b6 100644 --- a/tests/integration/features/chat-3/poll.feature +++ b/tests/integration/features/chat-3/poll.feature @@ -863,3 +863,57 @@ Feature: chat-2/poll | room | actorType | actorId | systemMessage | message | silent | messageParameters | | room | users | participant1 | user_added | {actor} added you | !ISSET | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"user":{"type":"user","id":"participant2","name":"participant2-displayname"}} | | room | users | participant1 | conversation_created | {actor} created the conversation | !ISSET | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | + + Scenario: Update a Draft Poll + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + When user "participant1" adds user "participant2" to room "room" with 200 (v4) + When user "participant1" creates a poll in room "room" with 200 + | question | What is the question? | + | options | ["You","me"] | + | resultMode | public | + | maxVotes | unlimited | + | draft | 1 | + When user "participant1" gets poll drafts for room "room" with 200 + | id | question | options | actorType | actorId | actorDisplayName | status | resultMode | maxVotes | + | POLL_ID(What is the question?) | What is the question? | ["You","me"] | users | participant1 | participant1-displayname | draft | public | 0 | + Then user "participant1" sees the following messages in room "room" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | + | room | users | participant1 | participant1-displayname | {object} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"object":{"type":"talk-poll","id":POLL_ID(This is not a draft!),"name":"This is not a draft!"}} | + Then user "participant1" updates a draft poll in room "room" with 200 + | id | POLL_ID(What is the question?) | + | question | What is the question again? | + | options | ["You","her"] | + | resultMode | public | + | maxVotes | unlimited | + When user "participant1" gets poll drafts for room "room" with 200 + | id | question | options | actorType | actorId | actorDisplayName | status | resultMode | maxVotes | + | POLL_ID(What is the question?) | What is the question again? | ["You","her"] | users | participant1 | participant1-displayname | draft | public | 0 | + + Scenario: Update a Draft Poll fails + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + When user "participant1" adds user "participant2" to room "room" with 200 (v4) + When user "participant1" creates a poll in room "room" with 200 + | question | What is the question? | + | options | ["You","me"] | + | resultMode | public | + | maxVotes | unlimited | + | draft | 1 | + When user "participant1" gets poll drafts for room "room" with 200 + | id | question | options | actorType | actorId | actorDisplayName | status | resultMode | maxVotes | + | POLL_ID(What is the question?) | What is the question? | ["You","me"] | users | participant1 | participant1-displayname | draft | public | 0 | + Then user "participant1" sees the following messages in room "room" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | + | room | users | participant1 | participant1-displayname | {object} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"object":{"type":"talk-poll","id":POLL_ID(This is not a draft!),"name":"This is not a draft!"}} | + Then user "participant1" updates a draft poll in room "room" with 400 + | id | POLL_ID(What is the question?) | + | question | What is the question again? | + | options | [] | + | resultMode | public | + | maxVotes | unlimited | + When user "participant1" gets poll drafts for room "room" with 200 + | id | question | options | actorType | actorId | actorDisplayName | status | resultMode | maxVotes | + | POLL_ID(What is the question?) | What is the question? | ["You","me"] | users | participant1 | participant1-displayname | draft | public | 0 |