Skip to content

Commit

Permalink
feat(federation): Allow editing and deleting for federated users
Browse files Browse the repository at this point in the history
Signed-off-by: Joas Schilling <coding@schilljs.com>
  • Loading branch information
nickvergessen committed Feb 22, 2024
1 parent c6a028e commit 6b4ae53
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 6 deletions.
1 change: 1 addition & 0 deletions lib/Chat/ChatManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ public function deleteMessage(Room $chat, IComment $comment, Participant $partic
* @param \DateTime $editTime
* @param string $message
* @return IComment
* @throws MessageTooLongException
* @throws \InvalidArgumentException When the message is empty or the shared object is not a file share with caption
*/
public function editMessage(Room $chat, IComment $comment, Participant $participant, \DateTime $editTime, string $message): IComment {
Expand Down
37 changes: 32 additions & 5 deletions lib/Controller/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use OCA\Talk\GuestManager;
use OCA\Talk\MatterbridgeManager;
use OCA\Talk\Middleware\Attribute\FederationSupported;
use OCA\Talk\Middleware\Attribute\RequireAuthenticatedParticipant;
use OCA\Talk\Middleware\Attribute\RequireLoggedInParticipant;
use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby;
use OCA\Talk\Middleware\Attribute\RequireModeratorParticipant;
Expand Down Expand Up @@ -721,12 +722,23 @@ protected function loadSelfReactions(array $messages, array $commentIdToIndex):
* 404: Message not found
* 405: Deleting this message type is not allowed
*/
#[NoAdminRequired]
#[FederationSupported]
#[PublicPage]
#[RequireModeratorOrNoLobby]
#[RequireParticipant]
#[RequireAuthenticatedParticipant]
#[RequirePermission(permission: RequirePermission::CHAT)]
#[RequireReadWriteConversation]
public function deleteMessage(int $messageId): DataResponse {
if ($this->room->getRemoteServer() !== '') {
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController::class);
return $proxy->deleteMessage(
$this->room,
$this->participant,
$messageId,
);
}

try {
$message = $this->chatManager->getComment($this->room, (string) $messageId);
} catch (NotFoundException $e) {
Expand Down Expand Up @@ -792,21 +804,34 @@ public function deleteMessage(int $messageId): DataResponse {
* @param int $messageId ID of the message
* @param string $message the message to send
* @psalm-param non-negative-int $messageId
* @return DataResponse<Http::STATUS_OK|Http::STATUS_ACCEPTED, TalkChatMessageWithParent, array{X-Chat-Last-Common-Read?: numeric-string}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_METHOD_NOT_ALLOWED, array<empty>, array{}>
* @return DataResponse<Http::STATUS_OK|Http::STATUS_ACCEPTED, TalkChatMessageWithParent, array{X-Chat-Last-Common-Read?: numeric-string}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_METHOD_NOT_ALLOWED|Http::STATUS_REQUEST_ENTITY_TOO_LARGE, array<empty>, array{}>
*
* 200: Message edited successfully
* 202: Message edited successfully, but a bot or Matterbridge is configured, so the information can be replicated to other services
* 400: Editing message is not possible, e.g. when the new message is empty or the message is too old
* 403: Missing permissions to edit message
* 404: Message not found
* 405: Editing this message type is not allowed
* 413: Message too long
*/
#[NoAdminRequired]
#[FederationSupported]
#[PublicPage]
#[RequireModeratorOrNoLobby]
#[RequireParticipant]
#[RequireAuthenticatedParticipant]
#[RequirePermission(permission: RequirePermission::CHAT)]
#[RequireReadWriteConversation]
public function editMessage(int $messageId, string $message): DataResponse {
if ($this->room->getRemoteServer() !== '') {
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController::class);
return $proxy->editMessage(
$this->room,
$this->participant,
$messageId,
$message,
);
}

try {
$comment = $this->chatManager->getComment($this->room, (string) $messageId);
} catch (NotFoundException $e) {
Expand Down Expand Up @@ -847,6 +872,8 @@ public function editMessage(int $messageId, string $message): DataResponse {
$this->timeFactory->getDateTime(),
$message
);
} catch (MessageTooLongException) {
return new DataResponse([], Http::STATUS_REQUEST_ENTITY_TOO_LARGE);
} catch (\InvalidArgumentException $e) {
if ($e->getMessage() === 'object_share') {
return new DataResponse([], Http::STATUS_METHOD_NOT_ALLOWED);
Expand Down
115 changes: 115 additions & 0 deletions lib/Federation/Proxy/TalkV1/Controller/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,121 @@ public function getMessageContext(Room $room, Participant $participant, int $mes
return new DataResponse($data, Http::STATUS_OK, $headers);
}

/**
* @return DataResponse<Http::STATUS_OK|Http::STATUS_ACCEPTED, TalkChatMessageWithParent, array{X-Chat-Last-Common-Read?: numeric-string}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_METHOD_NOT_ALLOWED, array<empty>, array{}>
* @throws CannotReachRemoteException
* @throws RemoteClientException
*
* 200: Message edited successfully
* 202: Message edited successfully, but a bot or Matterbridge is configured, so the information can be replicated to other services
* 400: Editing message is not possible, e.g. when the new message is empty or the message is too old
* 403: Missing permissions to edit message
* 404: Message not found
* 405: Editing this message type is not allowed
*
* @see \OCA\Talk\Controller\ChatController::editMessage()
*/
public function editMessage(Room $room, Participant $participant, int $messageId, string $message): DataResponse {
$proxy = $this->proxy->put(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/' . $messageId,
[
'message' => $message,
],
);

/** @var Http::STATUS_OK|Http::STATUS_ACCEPTED|Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_REQUEST_ENTITY_TOO_LARGE $statusCode */
$statusCode = $proxy->getStatusCode();

if ($statusCode !== Http::STATUS_OK && $statusCode !== Http::STATUS_ACCEPTED) {
if (in_array($statusCode, [
Http::STATUS_BAD_REQUEST,
Http::STATUS_FORBIDDEN,
Http::STATUS_NOT_FOUND,
Http::STATUS_REQUEST_ENTITY_TOO_LARGE,
], true)) {
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
}
return new DataResponse([], $statusCode);
}

/** @var ?TalkChatMessageWithParent $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_OK, Http::STATUS_ACCEPTED]);
if (!empty($data)) {
$data = $this->userConverter->convertAttendee($room, $data, 'actorType', 'actorId', 'actorDisplayName');
} else {
$data = null;
}

$headers = [];
if ($proxy->getHeader('X-Chat-Last-Common-Read')) {
$headers['X-Chat-Last-Common-Read'] = (string) (int) $proxy->getHeader('X-Chat-Last-Common-Read');
}

return new DataResponse(
$data,
$statusCode,
$headers
);
}

/**
* @return DataResponse<Http::STATUS_OK|Http::STATUS_ACCEPTED, TalkChatMessageWithParent, array{X-Chat-Last-Common-Read?: numeric-string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_METHOD_NOT_ALLOWED, array<empty>, array{}>
* @throws CannotReachRemoteException
* @throws RemoteClientException
*
* 200: Message deleted successfully
* 202: Message deleted successfully, but a bot or Matterbridge is configured, so the information can be replicated elsewhere
* 400: Deleting message is not possible
* 403: Missing permissions to delete message
* 404: Message not found
* 405: Deleting this message type is not allowed
*
* @see \OCA\Talk\Controller\ChatController::deleteMessage()
*/
public function deleteMessage(Room $room, Participant $participant, int $messageId): DataResponse {
$proxy = $this->proxy->delete(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/' . $messageId,
);

/** @var Http::STATUS_OK|Http::STATUS_ACCEPTED|Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_REQUEST_ENTITY_TOO_LARGE $statusCode */
$statusCode = $proxy->getStatusCode();

if ($statusCode !== Http::STATUS_OK && $statusCode !== Http::STATUS_ACCEPTED) {
if (in_array($statusCode, [
Http::STATUS_BAD_REQUEST,
Http::STATUS_FORBIDDEN,
Http::STATUS_NOT_FOUND,
Http::STATUS_REQUEST_ENTITY_TOO_LARGE,
], true)) {
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
}
return new DataResponse([], $statusCode);
}

/** @var ?TalkChatMessageWithParent $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_OK, Http::STATUS_ACCEPTED]);
if (!empty($data)) {
$data = $this->userConverter->convertAttendee($room, $data, 'actorType', 'actorId', 'actorDisplayName');
} else {
$data = null;
}

$headers = [];
if ($proxy->getHeader('X-Chat-Last-Common-Read')) {
$headers['X-Chat-Last-Common-Read'] = (string) (int) $proxy->getHeader('X-Chat-Last-Common-Read');
}

return new DataResponse(
$data,
$statusCode,
$headers
);
}

/**
* @see \OCA\Talk\Controller\ChatController::mentions()
*
Expand Down
82 changes: 82 additions & 0 deletions lib/Federation/Proxy/TalkV1/ProxyRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,88 @@ public function get(
}
}

/**
* @throws CannotReachRemoteException
* @throws RemoteClientException
*/
public function put(
string $cloudId,
#[SensitiveParameter]
string $accessToken,
string $url,
array $parameters = [],
): IResponse {
$requestOptions = $this->generateDefaultRequestOptions($cloudId, $accessToken);
if (!empty($parameters)) {
$requestOptions['json'] = $parameters;
}

try {
return $this->clientService->newClient()->put($url, $requestOptions);
} catch (ClientException $e) {
$status = $e->getResponse()->getStatusCode();

try {
$body = $e->getResponse()->getBody()->getContents();
$data = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($data)) {
throw new \RuntimeException('JSON response is not an array');
}
} catch (\Throwable $e) {
throw new CannotReachRemoteException('Error parsing JSON response', $e->getCode(), $e);
}

$clientException = new RemoteClientException($e->getMessage(), $status, $e, $data);
$this->logger->error('Client error from remote', ['exception' => $clientException]);
throw $clientException;
} catch (ServerException|\Throwable $e) {
$serverException = new CannotReachRemoteException($e->getMessage(), $e->getCode(), $e);
$this->logger->error('Could not reach remote', ['exception' => $serverException]);
throw $serverException;
}
}

/**
* @throws CannotReachRemoteException
* @throws RemoteClientException
*/
public function delete(
string $cloudId,
#[SensitiveParameter]
string $accessToken,
string $url,
array $parameters = [],
): IResponse {
$requestOptions = $this->generateDefaultRequestOptions($cloudId, $accessToken);
if (!empty($parameters)) {
$requestOptions['json'] = $parameters;
}

try {
return $this->clientService->newClient()->delete($url, $requestOptions);
} catch (ClientException $e) {
$status = $e->getResponse()->getStatusCode();

try {
$body = $e->getResponse()->getBody()->getContents();
$data = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($data)) {
throw new \RuntimeException('JSON response is not an array');
}
} catch (\Throwable $e) {
throw new CannotReachRemoteException('Error parsing JSON response', $e->getCode(), $e);
}

$clientException = new RemoteClientException($e->getMessage(), $status, $e, $data);
$this->logger->error('Client error from remote', ['exception' => $clientException]);
throw $clientException;
} catch (ServerException|\Throwable $e) {
$serverException = new CannotReachRemoteException($e->getMessage(), $e->getCode(), $e);
$this->logger->error('Could not reach remote', ['exception' => $serverException]);
throw $serverException;
}
}

/**
* @throws CannotReachRemoteException
* @throws RemoteClientException
Expand Down
37 changes: 37 additions & 0 deletions lib/Middleware/Attribute/RequireAuthenticatedParticipant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2024 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

namespace OCA\Talk\Middleware\Attribute;

use Attribute;
use OCA\Talk\Middleware\InjectionMiddleware;

/**
* Allows logged-in users and federated participants
* @see InjectionMiddleware::getLoggedIn()
*/
#[Attribute(Attribute::TARGET_METHOD)]
class RequireAuthenticatedParticipant extends RequireParticipant {
}
11 changes: 10 additions & 1 deletion lib/Middleware/InjectionMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Manager;
use OCA\Talk\Middleware\Attribute\FederationSupported;
use OCA\Talk\Middleware\Attribute\RequireAuthenticatedParticipant;
use OCA\Talk\Middleware\Attribute\RequireLoggedInModeratorParticipant;
use OCA\Talk\Middleware\Attribute\RequireLoggedInParticipant;
use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby;
Expand Down Expand Up @@ -97,6 +98,10 @@ public function beforeController(Controller $controller, string $methodName): vo
$apiVersion = $this->request->getParam('apiVersion');
$controller->setAPIVersion((int) substr($apiVersion, 1));

if (!empty($reflectionMethod->getAttributes(RequireAuthenticatedParticipant::class))) {
$this->getLoggedInOrGuest($controller, false, requireFederationWhenNotLoggedIn: true);
}

if (!empty($reflectionMethod->getAttributes(RequireLoggedInParticipant::class))) {
$this->getLoggedIn($controller, false);
}
Expand Down Expand Up @@ -179,7 +184,11 @@ protected function getLoggedIn(AEnvironmentAwareController $controller, bool $mo
* @throws NotAModeratorException
* @throws ParticipantNotFoundException
*/
protected function getLoggedInOrGuest(AEnvironmentAwareController $controller, bool $moderatorRequired, bool $requireListedWhenNoParticipant = false): void {
protected function getLoggedInOrGuest(AEnvironmentAwareController $controller, bool $moderatorRequired, bool $requireListedWhenNoParticipant = false, bool $requireFederationWhenNotLoggedIn = false): void {
if ($requireFederationWhenNotLoggedIn && $this->userId === null && !$this->federationAuthenticator->isFederationRequest()) {
throw new ParticipantNotFoundException();
}

$room = $controller->getRoom();
if (!$room instanceof Room) {
$token = $this->request->getParam('token');
Expand Down

0 comments on commit 6b4ae53

Please sign in to comment.