Skip to content

Commit

Permalink
# This is a combination of 9 commits.
Browse files Browse the repository at this point in the history
# This is the 1st commit message:

feat: add option to force passwords in public conversations

Signed-off-by: Anna Larch <anna@nextcloud.com>

# The commit message #2 will be skipped:

# fixup! feat: add option to force passwords in public conversations

# The commit message #3 will be skipped:

# fixup! feat: add option to force passwords in public conversations

# The commit message #4 will be skipped:

# fixup! feat: add option to force passwords in public conversations

# The commit message #5 will be skipped:

# fixup! fixup! feat: add option to force passwords in public conversations

# The commit message #6 will be skipped:

# fixup! fixup! feat: add option to force passwords in public conversations

# The commit message #7 will be skipped:

# fixup! feat: add option to force passwords in public conversations

# The commit message #8 will be skipped:

# fixup! feat: add option to force passwords in public conversations

# The commit message #9 will be skipped:

# fixup! feat: add option to force passwords in public conversations
  • Loading branch information
miaulalala committed Nov 27, 2024
1 parent 5ca9e8e commit cf54d5d
Show file tree
Hide file tree
Showing 28 changed files with 333 additions and 41 deletions.
4 changes: 4 additions & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +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

## 21
* `config => conversations => force-passwords` - Whether passwords are enforced for public rooms
* `conversation-creation-password` - Whether the endpoints for creating public conversations or making a conversation public support setting a password
3 changes: 2 additions & 1 deletion docs/conversation.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@
## Creating a new conversation

*Note:* Creating a conversation as a child breakout room, will automatically set the lobby when breakout rooms are not started and will always overwrite the room type with the parent room type. Also, moderators of the parent conversation will be automatically added as moderators.

* Method: `POST`
* Endpoint: `/room`
* Data:
Expand All @@ -128,13 +127,15 @@
| `roomName` | string | Conversation name up to 255 characters (Not available for `roomType = 1`) |
| `objectType` | string | Type of an object this room references, currently only allowed value is `room` to indicate the parent of a breakout room (See [Object types](constants.md#object-types)) |
| `objectId` | string | Id of an object this room references, room token is used for the parent of a breakout room |
| `password` | string | Password for the room (only available with `conversation-creation-password` capability) |

* Response:
- Status code:
+ `200 OK` When the "one to one" conversation already exists
+ `201 Created` When the conversation was created
+ `400 Bad Request` When an invalid conversation type was given
+ `400 Bad Request` When the conversation name is empty for `type = 3`
+ `400 Bad Request` When a password is required for a public room or when the password is invalid according to the password policy
+ `401 Unauthorized` When the user is not logged in
+ `404 Not Found` When the target to invite does not exist

Expand Down
1 change: 1 addition & 0 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,6 @@ Legend:
| `conversations_files` | string<br>`1` or `0` | `1` | No | 🖌️ | Whether the files app integration is enabled allowing to start conversations in the right sidebar |
| `conversations_files_public_shares` | string<br>`1` or `0` | `1` | No | 🖌️ | Whether the public share integration is enabled allowing to start conversations in the right sidebar on the public share page (Requires `conversations_files` also to be enabled) |
| `enable_matterbridge` | string<br>`1` or `0` | `0` | No | 🖌️ | Whether the Matterbridge integration is enabled and can be configured |
| `force_passwords` | string<br>`1` or `0` | `0` | No || Whether public chats are forced to use a password |
| `inactivity_lock_after_days` | int | `0` | No | | A duration (in days) after which rooms are locked. Calculated from the last activity in the room. |
| `inactivity_enable_lobby` | string<br>`1` or `0` | `0` | No | | Additionally enable the lobby for inactive rooms so they can only be read by moderators. |
4 changes: 3 additions & 1 deletion lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class Capabilities implements IPublicCapability {
'talk-polls-drafts',
'download-call-participants',
'email-csv-import',
'conversation-creation-password',
];

public const CONDITIONAL_FEATURES = [
Expand Down Expand Up @@ -224,7 +225,8 @@ public function getCapabilities(): array {
'summary-threshold' => 100,
],
'conversations' => [
'can-create' => $user instanceof IUser && !$this->talkConfig->isNotAllowedToCreateConversations($user)
'can-create' => $user instanceof IUser && !$this->talkConfig->isNotAllowedToCreateConversations($user),
'force-passwords' => $this->talkConfig->isPasswordEnforced(),
],
'federation' => [
'enabled' => false,
Expand Down
4 changes: 4 additions & 0 deletions lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -705,4 +705,8 @@ public function getInactiveLockTime(): int {
public function enableLobbyOnLockedRooms(): bool {
return $this->appConfig->getAppValueBool('inactivity_enable_lobby');
}

public function isPasswordEnforced(): bool {
return $this->appConfig->getAppValueBool('force_passwords');
}
}
45 changes: 35 additions & 10 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -502,16 +502,25 @@ protected function formatRoom(Room $room, ?Participant $currentParticipant, ?arr
* @param 'groups'|'circles'|'' $source Source of the invite ID ('circles' to create a room with a circle, etc.)
* @param string $objectType Type of the object
* @param string $objectId ID of the object
* @return DataResponse<Http::STATUS_OK|Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error?: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>
* @param string $password The room password (only available with `conversation-creation-password` capability)
* @return DataResponse<Http::STATUS_OK|Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error?: string, hint?: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>
*
* 200: Room already existed
* 201: Room created successfully
* 400: Room type invalid
* 400: Room type invalid or missing or invalid password
* 403: Missing permissions to create room
* 404: User, group or other target to invite was not found
*/
#[NoAdminRequired]
public function createRoom(int $roomType, string $invite = '', string $roomName = '', string $source = '', string $objectType = '', string $objectId = ''): DataResponse {
public function createRoom(
int $roomType,
string $invite = '',
string $roomName = '',
string $source = '',
string $objectType = '',
string $objectId = '',
string $password = '',
): DataResponse {
if ($roomType !== Room::TYPE_ONE_TO_ONE) {
/** @var IUser $user */
$user = $this->userManager->get($this->userId);
Expand All @@ -533,7 +542,7 @@ public function createRoom(int $roomType, string $invite = '', string $roomName
}
return $this->createGroupRoom($invite);
case Room::TYPE_PUBLIC:
return $this->createEmptyRoom($roomName, true, $objectType, $objectId);
return $this->createEmptyRoom($roomName, true, $objectType, $objectId, $password);
}

return new DataResponse([], Http::STATUS_BAD_REQUEST);
Expand Down Expand Up @@ -645,10 +654,10 @@ protected function createCircleRoom(string $targetCircleId): DataResponse {
}

/**
* @return DataResponse<Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error?: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
* @return DataResponse<Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error?: string, hint?: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
*/
#[NoAdminRequired]
protected function createEmptyRoom(string $roomName, bool $public = true, string $objectType = '', string $objectId = ''): DataResponse {
protected function createEmptyRoom(string $roomName, bool $public = true, string $objectType = '', string $objectId = '', string $password = ''): DataResponse {
$currentUser = $this->userManager->get($this->userId);
if (!$currentUser instanceof IUser) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
Expand Down Expand Up @@ -686,7 +695,9 @@ protected function createEmptyRoom(string $roomName, bool $public = true, string

// Create the room
try {
$room = $this->roomService->createConversation($roomType, $roomName, $currentUser, $objectType, $objectId);
$room = $this->roomService->createConversation($roomType, $roomName, $currentUser, $objectType, $objectId, $password);
} catch (PasswordException $e) {
return new DataResponse(['error' => 'password', 'hint' => $e->getHint()], Http::STATUS_BAD_REQUEST);
} catch (\InvalidArgumentException $e) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
Expand Down Expand Up @@ -1420,21 +1431,35 @@ public function removeAttendeeFromRoom(int $attendeeId): DataResponse {
/**
* Allowed guests to join conversation
*
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'type'|'value'}, array{}>
* Required capability: `conversation-creation-password` for `string $password` parameter
*
* @param string $password New password (only available with `conversation-creation-password` capability)
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'type'|'value'|'password', hint?: null|string}, array{}>
*
* 200: Allowed guests successfully
* 400: Allowing guests is not possible
*/
#[NoAdminRequired]
#[RequireLoggedInModeratorParticipant]
public function makePublic(): DataResponse {
public function makePublic(string $password = ''): DataResponse {
if ($this->talkConfig->isPasswordEnforced() && $password === '') {
return new DataResponse(['error' => 'password', 'hint' => $this->l10n->t('Password needs to be set')], Http::STATUS_BAD_REQUEST);

Check failure on line 1446 in lib/Controller/RoomController.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

UndefinedThisPropertyFetch

lib/Controller/RoomController.php:1446:62: UndefinedThisPropertyFetch: Instance property OCA\Talk\Controller\RoomController::$l10n is not defined (see https://psalm.dev/041)
}

try {
$this->roomService->setType($this->room, Room::TYPE_PUBLIC);
if ($password !== '') {
$this->roomService->makePublicWithPassword($this->room, $password);
} else {
$this->roomService->setType($this->room, Room::TYPE_PUBLIC);
}
} catch (PasswordException $e) {
return new DataResponse(['error' => 'password', 'hint' => $e->getHint()], Http::STATUS_BAD_REQUEST);
} catch (TypeException $e) {
return new DataResponse(['error' => $e->getReason()], Http::STATUS_BAD_REQUEST);
}

return new DataResponse();

}

/**
Expand Down
19 changes: 18 additions & 1 deletion lib/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\Comments\NotFoundException;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\ICache;
Expand Down Expand Up @@ -1108,7 +1109,7 @@ public function getChangelogRoom(string $userId): Room {
* @param string $objectId
* @return Room
*/
public function createRoom(int $type, string $name = '', string $objectType = '', string $objectId = ''): Room {
public function createRoom(int $type, string $name = '', string $objectType = '', string $objectId = '', string $password = ''): Room {
$token = $this->getNewToken();

$insert = $this->db->getQueryBuilder();
Expand All @@ -1118,6 +1119,7 @@ public function createRoom(int $type, string $name = '', string $objectType = ''
'name' => $insert->createNamedParameter($name),
'type' => $insert->createNamedParameter($type, IQueryBuilder::PARAM_INT),
'token' => $insert->createNamedParameter($token),
'password' => $insert->createNamedParameter($password),
]
);

Expand All @@ -1135,6 +1137,7 @@ public function createRoom(int $type, string $name = '', string $objectType = ''
'token' => $token,
'object_type' => $objectType,
'object_id' => $objectId,
'password' => $password
]);

$event = new RoomCreatedEvent($room);
Expand Down Expand Up @@ -1409,4 +1412,18 @@ protected function loadLastMessageInfo(IQueryBuilder $query): void {
$query->selectAlias('c.expire_date', 'comment_expire_date');
$query->selectAlias('c.meta_data', 'comment_meta_data');
}

/**
* @param int $roomId
* @param string $password
* @throws Exception
*/
public function setPublic(int $roomId, string $password = ''): void {
$update = $this->db->getQueryBuilder();
$update->update('talk_rooms')
->set('type', $update->createNamedParameter(Room::TYPE_PUBLIC, IQueryBuilder::PARAM_INT))
->set('password', $update->createNamedParameter($password, IQueryBuilder::PARAM_STR))
->where($update->expr()->eq('id', $update->createNamedParameter($roomId, IQueryBuilder::PARAM_INT)));
$update->executeStatement();
}
}
1 change: 1 addition & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@
* },
* conversations: array{
* can-create: bool,
* force-passwords: bool,
* },
* federation: array{
* enabled: bool,
Expand Down
103 changes: 95 additions & 8 deletions lib/Service/RoomService.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
use OCP\EventDispatcher\IEventDispatcher;
use OCP\HintException;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IUser;
use OCP\Log\Audit\CriticalActionPerformedEvent;
use OCP\Security\Events\ValidatePasswordPolicyEvent;
Expand All @@ -80,6 +81,7 @@ public function __construct(
protected IEventDispatcher $dispatcher,
protected IJobList $jobList,
protected LoggerInterface $logger,
protected IL10N $l10n,
) {
}

Expand Down Expand Up @@ -127,17 +129,13 @@ public function createOneToOneConversation(IUser $actor, IUser $targetUser): Roo
}

/**
* @param int $type
* @param string $name
* @param IUser|null $owner
* @param string $objectType
* @param string $objectId
* @return Room
* @throws InvalidArgumentException on too long or empty names
* @throws InvalidArgumentException unsupported type
* @throws InvalidArgumentException invalid object data
* @throws PasswordException empty or invalid password
*/
public function createConversation(int $type, string $name, ?IUser $owner = null, string $objectType = '', string $objectId = ''): Room {
public function createConversation(int $type, string $name, ?IUser $owner = null, string $objectType = '', string $objectId = '', string $password = ''): Room {
$name = trim($name);
if ($name === '' || mb_strlen($name) > 255) {
throw new InvalidArgumentException('name');
Expand Down Expand Up @@ -167,7 +165,20 @@ public function createConversation(int $type, string $name, ?IUser $owner = null
throw new InvalidArgumentException('object');
}

$room = $this->manager->createRoom($type, $name, $objectType, $objectId);
if ($type !== Room::TYPE_PUBLIC || !$this->config->isPasswordEnforced()) {
$room = $this->manager->createRoom($type, $name, $objectType, $objectId);
} elseif ($password === '') {
throw new PasswordException(PasswordException::REASON_VALUE, $this->l10n->t('Password needs to be set'));
} else {
$event = new ValidatePasswordPolicyEvent($password);
try {
$this->dispatcher->dispatchTyped($event);
} catch (HintException $e) {
throw new PasswordException(PasswordException::REASON_VALUE, $e->getHint());
}
$passwordHash = $this->hasher->hash($password);
$room = $this->manager->createRoom($type, $name, $objectType, $objectId, $passwordHash);
}

if ($owner instanceof IUser) {
$this->participantService->addUsers($room, [[
Expand All @@ -177,8 +188,8 @@ public function createConversation(int $type, string $name, ?IUser $owner = null
'participantType' => Participant::OWNER,
]], null);
}

return $room;

}

public function prepareConversationName(string $objectName): string {
Expand Down Expand Up @@ -545,6 +556,44 @@ public function setType(Room $room, int $newType, bool $allowSwitchingOneToOne =
$this->dispatcher->dispatchTyped($event);
}

/**
* @throws PasswordException|TypeException
*/
public function makePublicWithPassword(Room $room, string $password): void {
if ($room->getType() === Room::TYPE_PUBLIC) {
return;
}

if ($room->getType() !== Room::TYPE_GROUP) {
throw new TypeException(TypeException::REASON_TYPE);
}

if ($password === '') {
throw new PasswordException(PasswordException::REASON_VALUE, $this->l10n->t('Password needs to be set'));
}

$event = new ValidatePasswordPolicyEvent($password);
try {
$this->dispatcher->dispatchTyped($event);
} catch (HintException $e) {
throw new PasswordException(PasswordException::REASON_VALUE, $e->getHint());
}

$event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_TYPE, Room::TYPE_PUBLIC, $room->getType());
$this->dispatcher->dispatchTyped($event);
$event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_PASSWORD, $password);
$this->dispatcher->dispatchTyped($event);

$passwordHash = $this->hasher->hash($password);
$this->manager->setPublic($room->getId(), $passwordHash);
$room->setType(Room::TYPE_PUBLIC);

$event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_TYPE, Room::TYPE_PUBLIC, $room->getType());
$this->dispatcher->dispatchTyped($event);
$event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_PASSWORD, $password);
$this->dispatcher->dispatchTyped($event);
}

/**
* @param Room $room
* @param int $newState Currently it is only allowed to change between
Expand Down Expand Up @@ -1221,4 +1270,42 @@ public function deleteRoom(Room $room): void {
public function getInactiveRooms(\DateTime $inactiveSince): array {
return $this->manager->getInactiveRooms($inactiveSince);
}

/**
* @param Room $room
* @param int $oldType
* @param int $newType
* @param bool $allowSwitchingOneToOne
* @return void
*/
public function validateRoomTypeSwitch(Room $room, int $oldType, int $newType, bool $allowSwitchingOneToOne): void {
if (!$allowSwitchingOneToOne && $oldType === Room::TYPE_ONE_TO_ONE) {
throw new TypeException(TypeException::REASON_TYPE);
}

if ($oldType === Room::TYPE_ONE_TO_ONE_FORMER) {
throw new TypeException(TypeException::REASON_TYPE);
}

if ($oldType === Room::TYPE_NOTE_TO_SELF) {
throw new TypeException(TypeException::REASON_TYPE);
}

if (!in_array($newType, [Room::TYPE_GROUP, Room::TYPE_PUBLIC, Room::TYPE_ONE_TO_ONE_FORMER], true)) {
throw new TypeException(TypeException::REASON_VALUE);
}

if ($newType === Room::TYPE_ONE_TO_ONE_FORMER && $oldType !== Room::TYPE_ONE_TO_ONE) {
throw new TypeException(TypeException::REASON_VALUE);
}

if ($room->getBreakoutRoomMode() !== BreakoutRoom::MODE_NOT_CONFIGURED) {
throw new TypeException(TypeException::REASON_BREAKOUT_ROOM);
}

if ($room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
throw new TypeException(TypeException::REASON_BREAKOUT_ROOM);
}
}

}
Loading

0 comments on commit cf54d5d

Please sign in to comment.