Browse Source

feat(federation): Allow editing and deleting for federated users

Signed-off-by: Joas Schilling <coding@schilljs.com>
pull/11587/head
Joas Schilling 2 years ago
parent
commit
5662c982c4
No known key found for this signature in database GPG Key ID: 74434EFE0D2E2205
  1. 1
      lib/Chat/ChatManager.php
  2. 37
      lib/Controller/ChatController.php
  3. 115
      lib/Federation/Proxy/TalkV1/Controller/ChatController.php
  4. 82
      lib/Federation/Proxy/TalkV1/ProxyRequest.php
  5. 37
      lib/Middleware/Attribute/RequireAuthenticatedParticipant.php
  6. 11
      lib/Middleware/InjectionMiddleware.php

1
lib/Chat/ChatManager.php

@ -495,6 +495,7 @@ class ChatManager {
* @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 {

37
lib/Controller/ChatController.php

@ -35,6 +35,7 @@ use OCA\Talk\Federation\Authenticator;
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;
@ -721,12 +722,23 @@ class ChatController extends AEnvironmentAwareController {
* 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) {
@ -792,7 +804,7 @@ class ChatController extends AEnvironmentAwareController {
* @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
@ -800,13 +812,26 @@ class ChatController extends AEnvironmentAwareController {
* 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) {
@ -847,6 +872,8 @@ class ChatController extends AEnvironmentAwareController {
$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);

115
lib/Federation/Proxy/TalkV1/Controller/ChatController.php

@ -216,6 +216,121 @@ class ChatController {
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()
*

82
lib/Federation/Proxy/TalkV1/ProxyRequest.php

@ -118,6 +118,88 @@ class ProxyRequest {
}
}
/**
* @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

37
lib/Middleware/Attribute/RequireAuthenticatedParticipant.php

@ -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
lib/Middleware/InjectionMiddleware.php

@ -31,6 +31,7 @@ use OCA\Talk\Exceptions\RoomNotFoundException;
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;
@ -97,6 +98,10 @@ class InjectionMiddleware extends Middleware {
$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);
}
@ -179,7 +184,11 @@ class InjectionMiddleware extends Middleware {
* @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');

Loading…
Cancel
Save