diff --git a/lib/Authenticator.php b/lib/Authenticator.php
new file mode 100644
index 00000000000..15c15273c91
--- /dev/null
+++ b/lib/Authenticator.php
@@ -0,0 +1,129 @@
+resolved) {
+ return;
+ }
+ $this->resolved = true;
+
+ if ((bool)$this->request->getHeader('x-nextcloud-federation')) {
+ $authUser = urldecode($this->request->server['PHP_AUTH_USER'] ?? '');
+ try {
+ $cloudId = $this->cloudIdManager->resolveCloudId($authUser);
+ $this->isFederationRequest = true;
+ $this->actorType = Attendee::ACTOR_FEDERATED_USERS;
+ $this->actorId = $cloudId->getId();
+ $this->accessToken = $this->request->server['PHP_AUTH_PW'] ?? '';
+ return;
+ } catch (\InvalidArgumentException) {
+ // Fall through to other authentication shapes
+ }
+ }
+
+ $token = $this->request->getParam('token');
+ if (is_string($token) && $token !== '') {
+ $emailActorId = $this->talkSession->getAuthedEmailActorIdForRoom($token);
+ if ($emailActorId !== null) {
+ $this->isAuthenticatedEmailGuest = true;
+ $this->actorType = Attendee::ACTOR_EMAILS;
+ $this->actorId = $emailActorId;
+ }
+ }
+ }
+
+ public function isFederationRequest(): bool {
+ $this->resolve();
+ return $this->isFederationRequest;
+ }
+
+ public function isAuthenticatedEmailGuest(): bool {
+ $this->resolve();
+ return $this->isAuthenticatedEmailGuest;
+ }
+
+ public function isAuthenticatedRequest(): bool {
+ $this->resolve();
+ return $this->isFederationRequest || $this->isAuthenticatedEmailGuest;
+ }
+
+ public function getActorType(): string {
+ $this->resolve();
+ return $this->actorType;
+ }
+
+ public function getActorId(): string {
+ $this->resolve();
+ return $this->actorId;
+ }
+
+ /**
+ * Federation-only alias for {@see getActorId()}. Returns an empty string
+ * when the current request is not a federation request, even if another
+ * authenticated actor (e.g. email guest) is present.
+ */
+ public function getCloudId(): string {
+ return $this->isFederationRequest() ? $this->getActorId() : '';
+ }
+
+ public function getAccessToken(): string {
+ $this->resolve();
+ return $this->accessToken;
+ }
+
+ public function authenticated(Room $room, Participant $participant): void {
+ $this->room = $room;
+ $this->participant = $participant;
+ }
+
+ /**
+ * @throws RoomNotFoundException
+ */
+ public function getRoom(): Room {
+ if ($this->room === null) {
+ throw new RoomNotFoundException();
+ }
+ return $this->room;
+ }
+
+ /**
+ * @throws ParticipantNotFoundException
+ */
+ public function getParticipant(): Participant {
+ if ($this->participant === null) {
+ throw new ParticipantNotFoundException();
+ }
+ return $this->participant;
+ }
+}
diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php
index 8cd63326f5d..7f22c821094 100644
--- a/lib/Chat/Parser/SystemMessage.php
+++ b/lib/Chat/Parser/SystemMessage.php
@@ -10,11 +10,11 @@
use OCA\Circles\CirclesManager;
use OCA\DAV\CardDAV\PhotoCache;
+use OCA\Talk\Authenticator;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Events\MessageParseEvent;
use OCA\Talk\Events\OverwritePublicSharePropertiesEvent;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
-use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\Message;
use OCA\Talk\Participant;
diff --git a/lib/Config.php b/lib/Config.php
index 30df5dc9432..e6c3b78a58c 100644
--- a/lib/Config.php
+++ b/lib/Config.php
@@ -10,7 +10,6 @@
use OCA\Talk\AppInfo\Application;
use OCA\Talk\Events\BeforeTurnServersGetEvent;
-use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Service\RecordingService;
use OCA\Talk\Settings\UserPreference;
diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php
index fa75a086df1..8a81cc7ad48 100644
--- a/lib/Controller/CallController.php
+++ b/lib/Controller/CallController.php
@@ -8,10 +8,10 @@
namespace OCA\Talk\Controller;
+use OCA\Talk\Authenticator;
use OCA\Talk\Config;
use OCA\Talk\Exceptions\DialOutFailedException;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
-use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Manager;
use OCA\Talk\Middleware\Attribute\FederationSupported;
use OCA\Talk\Middleware\Attribute\RequireCallEnabled;
diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php
index 4db08153185..332780748e3 100644
--- a/lib/Controller/ChatController.php
+++ b/lib/Controller/ChatController.php
@@ -9,6 +9,7 @@
namespace OCA\Talk\Controller;
use OCA\Talk\AppInfo\Application;
+use OCA\Talk\Authenticator;
use OCA\Talk\Chat\AutoComplete\SearchPlugin;
use OCA\Talk\Chat\AutoComplete\Sorter;
use OCA\Talk\Chat\ChatManager;
@@ -19,7 +20,6 @@
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Exceptions\ChatSummaryException;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
-use OCA\Talk\Federation\Authenticator;
use OCA\Talk\GuestManager;
use OCA\Talk\Manager;
use OCA\Talk\MatterbridgeManager;
@@ -251,8 +251,8 @@ private function resolveReplyTo(int $replyTo, string $replyToToken, string $acto
* @return list{0: Attendee::ACTOR_*, 1: string}
*/
protected function getActorInfo(string $actorDisplayName = ''): array {
- $remoteCloudId = $this->federationAuthenticator->getCloudId();
- if ($remoteCloudId !== '') {
+ if ($this->federationAuthenticator->getActorType() === Attendee::ACTOR_FEDERATED_USERS) {
+ $remoteCloudId = $this->federationAuthenticator->getActorId();
if ($actorDisplayName) {
$this->participantService->updateDisplayNameForActor(Attendee::ACTOR_FEDERATED_USERS, $remoteCloudId, $actorDisplayName);
}
@@ -1280,7 +1280,7 @@ public function getMessageContext(
}
}
- $currentUser = $this->userManager->get($this->userId);
+ $currentUser = $this->userId !== null ? $this->userManager->getExistingUser($this->userId) : null;
if ($messageId === 0) {
// Guest in a fully expired chat, no history, just loading the chat from beginning for now
$commentsHistory = $commentsFuture = [];
diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php
index 7370b207987..a8eba4efd07 100644
--- a/lib/Controller/RoomController.php
+++ b/lib/Controller/RoomController.php
@@ -9,6 +9,7 @@
namespace OCA\Talk\Controller;
use OCA\DAV\CalDAV\TimezoneService;
+use OCA\Talk\Authenticator;
use OCA\Talk\Capabilities;
use OCA\Talk\Config;
use OCA\Talk\Events\AAttendeeRemovedEvent;
@@ -36,7 +37,6 @@
use OCA\Talk\Exceptions\RoomProperty\SipConfigurationException;
use OCA\Talk\Exceptions\RoomProperty\TypeException;
use OCA\Talk\Exceptions\UnauthorizedException;
-use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Federation\FederationManager;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\GuestManager;
@@ -456,15 +456,32 @@ public function getSingleRoom(string $token): DataResponse {
$isTalkFederation = $this->request->getHeader('x-nextcloud-federation');
if (!$isTalkFederation) {
- $sessionId = $this->session->getSessionForRoom($token);
- $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId, $includeLastMessage, $isSIPBridgeRequest);
+ if ($this->userId === null && $this->federationAuthenticator->isAuthenticatedEmailGuest()) {
+ // Authenticated email guest: the user/public-room check in
+ // getRoomForUserByToken() would reject non-public rooms, so
+ // trust the session-stored actor id and require the email
+ // attendee to still exist.
+ $room = $this->manager->getRoomByToken($token);
+ try {
+ $participant = $this->participantService->getParticipantByActor(
+ $room,
+ Attendee::ACTOR_EMAILS,
+ $this->federationAuthenticator->getActorId(),
+ );
+ } catch (ParticipantNotFoundException) {
+ throw new RoomNotFoundException();
+ }
+ } else {
+ $sessionId = $this->session->getSessionForRoom($token);
+ $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId, $includeLastMessage, $isSIPBridgeRequest);
- try {
- $participant = $this->participantService->getParticipant($room, $this->userId, $sessionId);
- } catch (ParticipantNotFoundException) {
try {
- $participant = $this->participantService->getParticipantBySession($room, $sessionId);
+ $participant = $this->participantService->getParticipant($room, $this->userId, $sessionId);
} catch (ParticipantNotFoundException) {
+ try {
+ $participant = $this->participantService->getParticipantBySession($room, $sessionId);
+ } catch (ParticipantNotFoundException) {
+ }
}
}
} else {
@@ -1426,13 +1443,6 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u
$this->participantService->addCircle($this->room, $circle, $participants);
} elseif ($source === 'emails') {
- $data = [];
- try {
- $this->roomService->setType($this->room, Room::TYPE_PUBLIC);
- $data = ['type' => $this->room->getType()];
- } catch (TypeException) {
- }
-
$email = strtolower($newParticipant);
$actorId = hash('sha256', $email);
try {
@@ -1442,7 +1452,7 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u
$this->guestManager->sendEmailInvitation($this->room, $participant);
}
- return new DataResponse($data);
+ return new DataResponse([]);
} elseif ($source === 'federated_users') {
if (!$this->talkConfig->isFederationEnabled()) {
return new DataResponse(['error' => 'federation'], Http::STATUS_NOT_IMPLEMENTED);
@@ -2020,9 +2030,20 @@ public function markConversationAsInsensitive(): DataResponse {
])]
public function joinRoom(string $token, string $password = '', bool $force = true): DataResponse {
$sessionId = $this->session->getSessionForRoom($token);
+ $authenticatedEmailGuest = null;
+ if ($this->userId === null && $this->federationAuthenticator->isAuthenticatedEmailGuest()) {
+ $authenticatedEmailGuest = $this->federationAuthenticator->getActorId();
+ }
+
try {
- // The participant is just joining, so enforce to not load any session
- $room = $this->manager->getRoomForUserByToken($token, $this->userId, null);
+ // The participant is just joining, so enforce to not load any session.
+ // Authenticated email guests bypass the user/public-room access check;
+ // the email attendee lookup below is the trust anchor instead.
+ if ($authenticatedEmailGuest !== null) {
+ $room = $this->manager->getRoomByToken($token);
+ } else {
+ $room = $this->manager->getRoomForUserByToken($token, $this->userId, null);
+ }
} catch (RoomNotFoundException $e) {
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => 'talkRoomToken']);
@@ -2072,8 +2093,6 @@ public function joinRoom(string $token, string $password = '', bool $force = tru
}
}
- $authenticatedEmailGuest = $this->session->getAuthedEmailActorIdForRoom($token);
-
$headers = [];
if ($authenticatedEmailGuest !== null || $room->isFederatedConversation()
|| ($previousParticipant instanceof Participant && $previousParticipant->isGuest())) {
@@ -2095,6 +2114,12 @@ public function joinRoom(string $token, string $password = '', bool $force = tru
try {
$previousParticipant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_EMAILS, $authenticatedEmailGuest);
} catch (ParticipantNotFoundException) {
+ // Stale spreed-authed-email session: the email attendee was
+ // removed (e.g. by a moderator) since the invite link was opened.
+ $this->session->removeAuthedEmailActorIdForRoom($token);
+ $response = new DataResponse(null, Http::STATUS_NOT_FOUND);
+ $response->throttle(['token' => $token, 'action' => 'talkRoomToken']);
+ return $response;
}
}
$participant = $this->participantService->joinRoomAsNewGuest($this->roomService, $room, $password, $result['result'], $previousParticipant);
@@ -2544,7 +2569,11 @@ public function leaveRoom(string $token): DataResponse {
$this->session->removeSessionForRoom($token);
try {
- $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId);
+ if ($this->userId === null && $this->federationAuthenticator->isAuthenticatedEmailGuest()) {
+ $room = $this->manager->getRoomByToken($token);
+ } else {
+ $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId);
+ }
$participant = $this->participantService->getParticipantBySession($room, $sessionId);
if ($room->isFederatedConversation()) {
diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php
index afc77a0d879..cb2d0a1d16e 100644
--- a/lib/Controller/SignalingController.php
+++ b/lib/Controller/SignalingController.php
@@ -8,13 +8,13 @@
namespace OCA\Talk\Controller;
+use OCA\Talk\Authenticator;
use OCA\Talk\Config;
use OCA\Talk\Events\BeforeSignalingResponseSentEvent;
use OCA\Talk\Exceptions\ForbiddenException;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Exceptions\RoomNotFoundException;
use OCA\Talk\Exceptions\UnauthorizedException;
-use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Manager;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\Session;
@@ -161,6 +161,14 @@ public function getSettings(string $token = ''): DataResponse {
$this->federationAuthenticator->getCloudId()
);
$this->federationAuthenticator->authenticated($room, $participant);
+ } elseif ($token !== '' && $this->userId === null && $this->federationAuthenticator->isAuthenticatedEmailGuest()) {
+ $room = $this->manager->getRoomByToken($token);
+ $participant = $this->participantService->getParticipantByActor(
+ $room,
+ Attendee::ACTOR_EMAILS,
+ $this->federationAuthenticator->getActorId(),
+ );
+ $this->federationAuthenticator->authenticated($room, $participant);
} elseif ($token !== '') {
$room = $this->manager->getRoomForUserByToken($token, $this->userId);
} elseif ($this->userId !== null || $isRecordingRequest) {
@@ -600,11 +608,21 @@ public function pullMessages(string $token): DataResponse {
// Was the session killed or the complete conversation?
try {
- $room = $this->manager->getRoomForUserByToken($token, $this->userId);
- if ($this->userId) {
- // For logged in users we check if they are still part of the public conversation,
- // if not they were removed instead of having a conflict.
- $this->participantService->getParticipant($room, $this->userId, false);
+ if ($this->userId === null && $this->federationAuthenticator->isAuthenticatedEmailGuest()) {
+ $room = $this->manager->getRoomByToken($token);
+ // Verify the email attendee still exists; otherwise treat as removed.
+ $this->participantService->getParticipantByActor(
+ $room,
+ Attendee::ACTOR_EMAILS,
+ $this->federationAuthenticator->getActorId(),
+ );
+ } else {
+ $room = $this->manager->getRoomForUserByToken($token, $this->userId);
+ if ($this->userId) {
+ // For logged in users we check if they are still part of the public conversation,
+ // if not they were removed instead of having a conflict.
+ $this->participantService->getParticipant($room, $this->userId, false);
+ }
}
// Session was killed, make the UI redirect to an error
diff --git a/lib/Federation/Authenticator.php b/lib/Federation/Authenticator.php
deleted file mode 100644
index 4631e1b6392..00000000000
--- a/lib/Federation/Authenticator.php
+++ /dev/null
@@ -1,115 +0,0 @@
-isFederationRequest = (bool)$this->request->getHeader('x-nextcloud-federation');
- if (!$this->isFederationRequest) {
- $this->federationCloudId = '';
- $this->accessToken = '';
- return;
- }
-
- $authUser = $this->request->server['PHP_AUTH_USER'] ?? '';
- $authUser = urldecode($authUser);
-
- try {
- $cloudId = $this->cloudIdManager->resolveCloudId($authUser);
- $this->federationCloudId = $cloudId->getId();
- $this->accessToken = $this->request->server['PHP_AUTH_PW'] ?? '';
- } catch (\InvalidArgumentException) {
- $this->isFederationRequest = false;
- $this->federationCloudId = '';
- $this->accessToken = '';
- }
- }
-
- public function isFederationRequest(): bool {
- if ($this->isFederationRequest === null) {
- $this->readHeaders();
-
- if ($this->isFederationRequest === null) {
- return false;
- }
- }
-
- return $this->isFederationRequest;
- }
-
- public function getCloudId(): string {
- if ($this->federationCloudId === null) {
- $this->readHeaders();
-
- if ($this->federationCloudId === null) {
- return '';
- }
- }
-
- return $this->federationCloudId;
- }
-
- public function getAccessToken(): string {
- if ($this->accessToken === null) {
- $this->readHeaders();
-
- if ($this->accessToken === null) {
- return '';
- }
- }
-
- return $this->accessToken;
- }
-
- public function authenticated(Room $room, Participant $participant): void {
- $this->room = $room;
- $this->participant = $participant;
- }
-
- /**
- * @throws RoomNotFoundException
- */
- public function getRoom(): Room {
- if ($this->room === null) {
- throw new RoomNotFoundException();
- }
- return $this->room;
- }
-
- /**
- * @throws ParticipantNotFoundException
- */
- public function getParticipant(): Participant {
- if ($this->participant === null) {
- throw new ParticipantNotFoundException();
- }
- return $this->participant;
-
- }
-}
diff --git a/lib/GuestManager.php b/lib/GuestManager.php
index 4c4757df761..07ccfa80532 100644
--- a/lib/GuestManager.php
+++ b/lib/GuestManager.php
@@ -222,27 +222,28 @@ public function sendEmailInvitation(Room $room, Participant $participant): void
$user = $this->userSession->getUser();
$invitee = $user instanceof IUser ? $user->getDisplayName() : '';
+ $roomName = $room->getDisplayName('', true);
$template = $this->mailer->createEMailTemplate('Talk.InviteByEmail', [
'invitee' => $invitee,
- 'roomName' => $room->getDisplayName(''),
+ 'roomName' => $roomName,
'roomLink' => $link,
'email' => $email,
'pin' => $pin,
]);
if ($user instanceof IUser) {
- $subject = $this->l->t('%1$s invited you to conversation "%2$s".', [$user->getDisplayName(), $room->getDisplayName('')]);
+ $subject = $this->l->t('%1$s invited you to conversation "%2$s".', [$user->getDisplayName(), $roomName]);
$message->setFrom([Util::getDefaultEmailAddress('no-reply') => $user->getDisplayName()]);
} else {
- $subject = $this->l->t('You were invited to conversation "%s".', $room->getDisplayName(''));
+ $subject = $this->l->t('You were invited to conversation "%s".', $roomName);
$message->setFrom([Util::getDefaultEmailAddress('no-reply') => $this->defaults->getName()]);
}
$template->setSubject($subject);
$template->addHeader();
$template->addHeading(
- htmlspecialchars($room->getDisplayName('')),
+ htmlspecialchars($roomName),
$this->l->t('Conversation invitation')
);
$template->addBodyText(
@@ -298,7 +299,7 @@ public function sendEmailInvitation(Room $room, Participant $participant): void
$this->l->t('Click the link below to join the lobby now.')
);
$template->addBodyButton(
- $this->l->t('Join lobby for "%s"', [$room->getDisplayName('')]),
+ $this->l->t('Join lobby for "%s"', [$roomName]),
$link
);
} else {
@@ -308,7 +309,7 @@ public function sendEmailInvitation(Room $room, Participant $participant): void
$this->l->t('Click the link below to join the conversation now.')
);
$template->addBodyButton(
- $this->l->t('Join "%s"', [$room->getDisplayName('')]),
+ $this->l->t('Join "%s"', [$roomName]),
$link
);
}
diff --git a/lib/Manager.php b/lib/Manager.php
index 9b33f9f3e27..85eea1fa6f9 100644
--- a/lib/Manager.php
+++ b/lib/Manager.php
@@ -13,7 +13,6 @@
use OCA\Talk\Events\RoomCreatedEvent;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Exceptions\RoomNotFoundException;
-use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Model\Attachment;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\AttendeeMapper;
@@ -1110,7 +1109,11 @@ public function getRoomForSession(?string $userId, ?string $sessionId): Room {
$this->participantService->cacheParticipant($room, $participant);
$room->setParticipant($row['actor_id'], $participant);
- if ($room->getType() === Room::TYPE_PUBLIC || !in_array($participant->getAttendee()->getParticipantType(), [Participant::GUEST, Participant::GUEST_MODERATOR, Participant::USER_SELF_JOINED], true)) {
+ if ($room->getType() === Room::TYPE_PUBLIC
+ || $row['actor_type'] === Attendee::ACTOR_EMAILS
+ || !in_array($participant->getAttendee()->getParticipantType(), [Participant::GUEST, Participant::GUEST_MODERATOR, Participant::USER_SELF_JOINED], true)) {
+ // Email attendees were directly invited via their email address;
+ // the attendee row itself is the authorization to access the room.
return $room;
}
diff --git a/lib/Middleware/InjectionMiddleware.php b/lib/Middleware/InjectionMiddleware.php
index 9ae74444ef1..ae95d5a255c 100644
--- a/lib/Middleware/InjectionMiddleware.php
+++ b/lib/Middleware/InjectionMiddleware.php
@@ -8,13 +8,13 @@
namespace OCA\Talk\Middleware;
+use OCA\Talk\Authenticator;
use OCA\Talk\Controller\AEnvironmentAwareOCSController;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Exceptions\ForbiddenException;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Exceptions\PermissionsException;
use OCA\Talk\Exceptions\RoomNotFoundException;
-use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Manager;
use OCA\Talk\Middleware\Attribute\AllowWithoutParticipantWhenPendingInvitation;
use OCA\Talk\Middleware\Attribute\FederationSupported;
@@ -220,7 +220,7 @@ protected function getLoggedInOrGuest(
bool $requireFederationWhenNotLoggedIn = false,
?string $sessionIdParameter = null,
): void {
- if ($requireFederationWhenNotLoggedIn && $this->userId === null && !$this->federationAuthenticator->isFederationRequest()) {
+ if ($requireFederationWhenNotLoggedIn && $this->userId === null && !$this->federationAuthenticator->isAuthenticatedRequest()) {
throw new ParticipantNotFoundException();
}
@@ -232,7 +232,35 @@ protected function getLoggedInOrGuest(
$sessionId = null;
if (!$room instanceof Room) {
$token = $this->request->getParam('token');
- if (!$this->federationAuthenticator->isFederationRequest()) {
+ if ($this->userId === null && $this->federationAuthenticator->isAuthenticatedEmailGuest()) {
+ $room = $this->manager->getRoomByToken($token);
+
+ // After joinRoom() the email guest owns a regular spreed-session for the room.
+ // Prefer the session lookup so the resolved Participant carries its Session
+ // (joinCall/chat/etc. require it). Sanity-check that the session actually
+ // belongs to the authenticated email attendee.
+ $sessionId = $this->talkSession->getSessionForRoom($token);
+ if ($sessionId !== null) {
+ try {
+ $participant = $this->participantService->getParticipantBySession($room, $sessionId);
+ $attendee = $participant->getAttendee();
+ if ($attendee->getActorType() !== Attendee::ACTOR_EMAILS
+ || $attendee->getActorId() !== $this->federationAuthenticator->getActorId()) {
+ throw new ParticipantNotFoundException('3');
+ }
+ } catch (ParticipantNotFoundException) {
+ // Guest might be reloading so the session has the old Talk session while the join happens in parallel
+ $participant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_EMAILS, $this->federationAuthenticator->getActorId());
+ }
+ } else {
+ // No session yet (e.g. between landing on the page and joining); trust the
+ // session-stored actor id only as long as the email attendee still exists.
+ $participant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_EMAILS, $this->federationAuthenticator->getActorId());
+ }
+
+ $this->federationAuthenticator->authenticated($room, $participant);
+ $controller->setParticipant($participant);
+ } elseif (!$this->federationAuthenticator->isFederationRequest()) {
$sessionId = $this->talkSession->getSessionForRoom($token);
$room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId);
} else {
diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php
index a53a5979310..c1ce4253c03 100644
--- a/lib/Service/RoomService.php
+++ b/lib/Service/RoomService.php
@@ -686,11 +686,13 @@ public function setType(Room $room, int $newType, bool $allowSwitchingOneToOne =
$room->setType($newType);
if ($oldType === Room::TYPE_PUBLIC) {
- // Kick all guests and users that were not invited
+ // Kick all guests that are not email invited
+ // and all users that joined the public link
$delete = $this->db->getQueryBuilder();
$delete->delete('talk_attendees')
->where($delete->expr()->eq('room_id', $delete->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
- ->andWhere($delete->expr()->in('participant_type', $delete->createNamedParameter([Participant::GUEST, Participant::GUEST_MODERATOR, Participant::USER_SELF_JOINED], IQueryBuilder::PARAM_INT_ARRAY)));
+ ->andWhere($delete->expr()->in('participant_type', $delete->createNamedParameter([Participant::GUEST, Participant::GUEST_MODERATOR, Participant::USER_SELF_JOINED], IQueryBuilder::PARAM_INT_ARRAY)))
+ ->andWhere($delete->expr()->neq('actor_type', $delete->createNamedParameter(Attendee::ACTOR_EMAILS, IQueryBuilder::PARAM_INT)));
$delete->executeStatement();
}
diff --git a/lib/TalkSession.php b/lib/TalkSession.php
index 5dfb87d4feb..d41803885fe 100644
--- a/lib/TalkSession.php
+++ b/lib/TalkSession.php
@@ -63,11 +63,15 @@ public function removeGuestActorIdForRoom(string $token): void {
}
public function getAuthedEmailActorIdForRoom(string $token): ?string {
- return $this->getValue('spreed-authed-email', $token);
+ return $this->getValue('spreed-authed-email', $token, false);
}
public function setAuthedEmailActorIdForRoom(string $token, string $actorId): void {
- $this->setValue('spreed-authed-email', $token, $actorId);
+ $this->setValue('spreed-authed-email', $token, $actorId, false);
+ }
+
+ public function removeAuthedEmailActorIdForRoom(string $token): void {
+ $this->removeValue('spreed-authed-email', $token, false);
}
public function getFileShareTokenForRoom(string $roomToken): ?string {
@@ -108,14 +112,14 @@ protected function getValues(string $key): array {
return $values;
}
- protected function getValue(string $key, string $token): ?string {
- $token .= $this->getTabId();
+ protected function getValue(string $key, string $token, bool $useTabId = true): ?string {
+ $token .= $useTabId ? $this->getTabId() : '';
$values = $this->getValues($key);
return $values[$token] ?? null;
}
- protected function setValue(string $key, string $token, string $value): void {
- $token .= $this->getTabId();
+ protected function setValue(string $key, string $token, string $value, bool $useTabId = true): void {
+ $token .= $useTabId ? $this->getTabId() : '';
$reopened = $this->session->reopen();
$values = $this->getValues($key);
@@ -128,11 +132,11 @@ protected function setValue(string $key, string $token, string $value): void {
}
}
- protected function removeValue(string $key, string $token): void {
+ protected function removeValue(string $key, string $token, bool $useTabId = true): void {
$reopened = $this->session->reopen();
$values = $this->getValues($key);
- $tabId = $this->getTabId();
+ $tabId = $useTabId ? $this->getTabId() : '';
if ($tabId !== '') {
unset($values[$token . $tabId]);
} else {
diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php
index 395e6adad6d..7810183b21d 100644
--- a/tests/integration/features/bootstrap/FeatureContext.php
+++ b/tests/integration/features/bootstrap/FeatureContext.php
@@ -173,6 +173,10 @@ public static function getSessionIdForUser(string $user): string {
}
public function getAttendeeId(string $type, string $id, string $room, ?string $user = null): int {
+ if ($type === 'emails' && str_contains($id, '@')) {
+ $id = hash('sha256', $id);
+ }
+
if ($type === 'federated_users') {
if (!str_contains($id, '@')) {
$id .= '@' . $this->remoteServerUrl;
@@ -1363,17 +1367,45 @@ public function userSessionState(string $user, int $state, string $identifier, i
#[Then('/^user "([^"]*)" views call-URL of room "([^"]*)" with (\d+)$/')]
public function userViewsCallURL(string $user, string $identifier, int $statusCode, ?TableNode $formData = null): void {
+ $queryParams = [];
+ $assertions = [];
+ if ($formData instanceof TableNode) {
+ foreach ($formData->getRows() as $row) {
+ if (count($row) === 2) {
+ $queryParams[$row[0]] = $row[1];
+ } else {
+ $assertions[] = $row[0];
+ }
+ }
+ }
+
+ $token = self::$identifierToToken[$identifier] ?? $identifier;
+
+ if (isset($queryParams['email']) && !isset($queryParams['access'])) {
+ $previousUser = $this->setCurrentUser('admin');
+ $this->sendRequest('GET', '/apps/spreedcheats/email/access?' . http_build_query([
+ 'token' => $token,
+ 'email' => $queryParams['email'],
+ ]));
+ $this->assertStatusCode($this->response, 200);
+ $data = $this->getDataFromResponse($this->response);
+ $queryParams['access'] = $data['access_token'];
+ $this->setCurrentUser($previousUser);
+ }
+
$this->setCurrentUser($user);
- $this->sendFrontpageRequest(
- 'GET', '/call/' . (self::$identifierToToken[$identifier] ?? $identifier)
- );
+ $url = '/call/' . $token;
+ if (!empty($queryParams)) {
+ $url .= '?' . http_build_query($queryParams);
+ }
+ $this->sendFrontpageRequest('GET', $url);
$this->assertStatusCode($this->response, $statusCode);
- if ($formData instanceof TableNode) {
+ if (!empty($assertions)) {
$content = $this->response->getBody()->getContents();
- foreach ($formData->getRows() as $line) {
- Assert::assertStringContainsString($line[0], $content);
+ foreach ($assertions as $assertion) {
+ Assert::assertStringContainsString($assertion, $content);
}
}
}
diff --git a/tests/integration/features/conversation-3/invite-email.feature b/tests/integration/features/conversation-3/invite-email.feature
index 6c024b2f5c1..ec9726dae6a 100644
--- a/tests/integration/features/conversation-3/invite-email.feature
+++ b/tests/integration/features/conversation-3/invite-email.feature
@@ -22,3 +22,39 @@ Feature: conversation-3/invite-email
# Reinvite failure
When user "participant1" resends invite for room "room" with 404 (v4)
| attendeeId | not-found@example.tld |
+
+ Scenario: Email guest joins a private conversation through their invite link
+ Given user "participant1" creates room "room" (v4)
+ | roomType | 2 |
+ | roomName | room |
+ And user "participant1" adds email "test@example.tld" to room "room" with 200 (v4)
+ And user "participant1" is participant of room "room" (v4)
+ | type |
+ | 2 |
+ And user "participant1" sees the following attendees in room "room" with 200 (v4)
+ | participantType | actorType | actorId | invitedActorId |
+ | 4 | emails | SHA256(test@example.tld) | test@example.tld |
+ | 1 | users | participant1 | ABSENT |
+ When user "guest" views call-URL of room "room" with 200
+ | email | test@example.tld |
+ Then user "guest" joins room "room" with 200 (v4)
+ Then user "guest" joins call "room" with 200 (v4)
+ | flags | 1 |
+ Then user "participant1" sees 1 peers in call "room" with 200 (v4)
+ Then user "guest" leaves room "room" with 200 (v4)
+
+ Scenario: Email guest cannot rejoin after being removed from a private conversation
+ Given user "participant1" creates room "room" (v4)
+ | roomType | 2 |
+ | roomName | room |
+ And user "participant1" adds email "test@example.tld" to room "room" with 200 (v4)
+ And user "participant1" sees the following attendees in room "room" with 200 (v4)
+ | participantType | actorType | actorId | invitedActorId |
+ | 4 | emails | SHA256(test@example.tld) | test@example.tld |
+ | 1 | users | participant1 | ABSENT |
+ And user "guest" views call-URL of room "room" with 200
+ | email | test@example.tld |
+ And user "guest" joins room "room" with 200 (v4)
+ And user "guest" leaves room "room" with 200 (v4)
+ When user "participant1" removes email "test@example.tld" from room "room" with 200 (v4)
+ Then user "guest" joins room "room" with 404 (v4)
diff --git a/tests/integration/spreedcheats/appinfo/routes.php b/tests/integration/spreedcheats/appinfo/routes.php
index 740d80ade91..0b5b1114f53 100644
--- a/tests/integration/spreedcheats/appinfo/routes.php
+++ b/tests/integration/spreedcheats/appinfo/routes.php
@@ -10,6 +10,7 @@
'ocs' => [
['name' => 'Api#resetSpreed', 'url' => '/', 'verb' => 'DELETE'],
['name' => 'Api#ageChat', 'url' => '/age', 'verb' => 'POST'],
+ ['name' => 'Api#emailAccessToken', 'url' => '/email/access', 'verb' => 'GET'],
['name' => 'Api#forgedFederationLeave', 'url' => '/forged/federation/active', 'verb' => 'DELETE'],
['name' => 'Api#createEventInCalendar', 'url' => '/calendar', 'verb' => 'POST'],
['name' => 'Api#createDashboardEvents', 'url' => '/dashboardEvents', 'verb' => 'POST'],
diff --git a/tests/integration/spreedcheats/lib/Controller/ApiController.php b/tests/integration/spreedcheats/lib/Controller/ApiController.php
index 3d27daa680b..aefcc384740 100644
--- a/tests/integration/spreedcheats/lib/Controller/ApiController.php
+++ b/tests/integration/spreedcheats/lib/Controller/ApiController.php
@@ -215,6 +215,28 @@ public function ageChat(string $token, int $hours): DataResponse {
return new DataResponse();
}
+ public function emailAccessToken(string $token, string $email): DataResponse {
+ $query = $this->db->getQueryBuilder();
+ $query->select('a.access_token')
+ ->from('talk_rooms', 'r')
+ ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX(
+ $query->expr()->eq('a.room_id', 'r.id'),
+ $query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_EMAILS)),
+ $query->expr()->eq('a.actor_id', $query->createNamedParameter(hash('sha256', $email))),
+ ))
+ ->where($query->expr()->eq('r.token', $query->createNamedParameter($token)));
+
+ $result = $query->executeQuery();
+ $accessToken = $result->fetchOne();
+ $result->closeCursor();
+
+ if (!$accessToken) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+
+ return new DataResponse(['access_token' => $accessToken]);
+ }
+
public function forgedFederationLeave(string $token, string $actingUser, string $targetUser): DataResponse {
$query = $this->db->getQueryBuilder();
$query->select('id')
diff --git a/tests/php/Chat/Parser/SystemMessageTest.php b/tests/php/Chat/Parser/SystemMessageTest.php
index dd8a0413831..2d9da4e639b 100644
--- a/tests/php/Chat/Parser/SystemMessageTest.php
+++ b/tests/php/Chat/Parser/SystemMessageTest.php
@@ -9,10 +9,10 @@
namespace OCA\Talk\Tests\php\Chat\Parser;
use OCA\DAV\CardDAV\PhotoCache;
+use OCA\Talk\Authenticator;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\Parser\SystemMessage;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
-use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\Message;
use OCA\Talk\Model\Session;
diff --git a/tests/php/Controller/ChatControllerTest.php b/tests/php/Controller/ChatControllerTest.php
index a5f285b4dd1..c1a3561f2b6 100644
--- a/tests/php/Controller/ChatControllerTest.php
+++ b/tests/php/Controller/ChatControllerTest.php
@@ -8,6 +8,7 @@
namespace OCA\Talk\Tests\php\Controller;
+use OCA\Talk\Authenticator;
use OCA\Talk\Chat\AutoComplete\SearchPlugin;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\MessageParser;
@@ -15,7 +16,6 @@
use OCA\Talk\Chat\ReactionManager;
use OCA\Talk\Config;
use OCA\Talk\Controller\ChatController;
-use OCA\Talk\Federation\Authenticator;
use OCA\Talk\GuestManager;
use OCA\Talk\Manager;
use OCA\Talk\MatterbridgeManager;
diff --git a/tests/php/Controller/SignalingControllerTest.php b/tests/php/Controller/SignalingControllerTest.php
index 971756ac395..bfc0f0890c2 100644
--- a/tests/php/Controller/SignalingControllerTest.php
+++ b/tests/php/Controller/SignalingControllerTest.php
@@ -8,13 +8,13 @@
namespace OCA\Talk\Tests\php\Controller;
+use OCA\Talk\Authenticator;
use OCA\Talk\Chat\CommentsManager;
use OCA\Talk\Config;
use OCA\Talk\Controller\SignalingController;
use OCA\Talk\Events\BeforeSignalingResponseSentEvent;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Exceptions\RoomNotFoundException;
-use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Manager;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\AttendeeMapper;
diff --git a/tests/php/Recording/BackendNotifierTest.php b/tests/php/Recording/BackendNotifierTest.php
index cba83431212..1a9e12cb581 100644
--- a/tests/php/Recording/BackendNotifierTest.php
+++ b/tests/php/Recording/BackendNotifierTest.php
@@ -8,9 +8,9 @@
namespace OCA\Talk\Tests\php\Recording;
+use OCA\Talk\Authenticator;
use OCA\Talk\Chat\CommentsManager;
use OCA\Talk\Config;
-use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Manager;
use OCA\Talk\Model\AttendeeMapper;
use OCA\Talk\Model\SessionMapper;
diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml
index a4d85acfd27..48dd33304db 100644
--- a/tests/psalm-baseline.xml
+++ b/tests/psalm-baseline.xml
@@ -8,6 +8,11 @@
+
+
+ request->server]]>
+
+
@@ -82,11 +87,6 @@
-
-
- request->server]]>
-
-