diff --git a/appinfo/routes.php b/appinfo/routes.php index 8ddc9dd..8a9312a 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -12,7 +12,7 @@ return [ ['name' => 'server#check', 'url' => '/server/check', 'verb' => 'POST'], ['name' => 'server#version', 'url' => '/server/version', 'verb' => 'GET'], ['name' => 'server#delete_record', 'url' => '/server/record/{recordId}', 'verb' => 'DELETE'], - ['name' => 'join#index', 'url' => '/b/{token}', 'verb' => 'GET'], + ['name' => 'join#index', 'url' => '/b/{token}/{moderatorToken}', 'verb' => 'GET', 'defaults' => ['moderatorToken' => '']], ['name' => 'restriction#user', 'url' => '/restrictions/user', 'verb' => 'GET'], ['name' => 'hook#meetingEnded', 'url' => '/hook/ended/{token}/{mac}', 'verb' => 'GET'], ['name' => 'hook#recordingReady', 'url' => '/hook/recording/{token}/{mac}', 'verb' => 'GET'], diff --git a/lib/BigBlueButton/API.php b/lib/BigBlueButton/API.php index 5ac14ad..e69914e 100644 --- a/lib/BigBlueButton/API.php +++ b/lib/BigBlueButton/API.php @@ -12,7 +12,6 @@ use BigBlueButton\Parameters\JoinMeetingParameters; use OCA\BigBlueButton\Crypto; use OCA\BigBlueButton\Db\Room; use OCA\BigBlueButton\Event\MeetingStartedEvent; -use OCA\BigBlueButton\Permission; use OCA\BigBlueButton\UrlHelper; use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; @@ -26,9 +25,6 @@ class API { /** @var IURLGenerator */ private $urlGenerator; - /** @var Permission */ - private $permission; - /** @var BigBlueButton|null */ private $server; @@ -47,7 +43,6 @@ class API { public function __construct( IConfig $config, IURLGenerator $urlGenerator, - Permission $permission, Crypto $crypto, IEventDispatcher $eventDispatcher, IL10N $l10n, @@ -55,7 +50,6 @@ class API { ) { $this->config = $config; $this->urlGenerator = $urlGenerator; - $this->permission = $permission; $this->crypto = $crypto; $this->eventDispatcher = $eventDispatcher; $this->l10n = $l10n; @@ -78,8 +72,8 @@ class API { * * @return string join url */ - public function createJoinUrl(Room $room, float $creationTime, string $displayname, ?string $uid = null) { - $password = $this->permission->isModerator($room, $uid) ? $room->moderatorPassword : $room->attendeePassword; + public function createJoinUrl(Room $room, float $creationTime, string $displayname, bool $isModerator, ?string $uid = null) { + $password = $isModerator ? $room->moderatorPassword : $room->attendeePassword; $joinMeetingParams = new JoinMeetingParameters($room->uid, $displayname, $password); diff --git a/lib/Controller/JoinController.php b/lib/Controller/JoinController.php index e721a80..53a2430 100644 --- a/lib/Controller/JoinController.php +++ b/lib/Controller/JoinController.php @@ -78,6 +78,12 @@ class JoinController extends Controller { throw new NotFoundException(); } + $moderatorToken = $this->request->getParam('moderatorToken'); + + if (!empty($moderatorToken) && $moderatorToken !== $room->moderatorToken) { + throw new NoPermissionException(); + } + $displayname = trim($displayname); $userId = null; $presentation = null; @@ -108,7 +114,9 @@ class JoinController extends Controller { return $response; } - if ($room->requireModerator && ($userId === null || !$this->permission->isModerator($room, $userId)) && !$this->api->isRunning($room)) { + $isModerator = (!empty($moderatorToken) && $moderatorToken === $room->moderatorToken) || $this->permission->isModerator($room, $userId); + + if ($room->requireModerator && !$isModerator && !$this->api->isRunning($room)) { return new TemplateResponse($this->appName, 'waiting', [ 'room' => $room->name, 'name' => $displayname, @@ -116,7 +124,7 @@ class JoinController extends Controller { } $creationDate = $this->api->createMeeting($room, $presentation); - $joinUrl = $this->api->createJoinUrl($room, $creationDate, $displayname, $userId); + $joinUrl = $this->api->createJoinUrl($room, $creationDate, $displayname, $isModerator, $userId); \OCP\Util::addHeader('meta', ['http-equiv' => 'refresh', 'content' => '3;url='.$joinUrl]); diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 4e80ba8..ade6772 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -114,7 +114,8 @@ class RoomController extends Controller { bool $record, string $access, bool $everyoneIsModerator, - bool $requireModerator + bool $requireModerator, + ?string $moderatorToken ): DataResponse { $room = $this->service->find($id); @@ -137,8 +138,8 @@ class RoomController extends Controller { return new DataResponse('Access type not allowed.', Http::STATUS_BAD_REQUEST); } - return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator) { - return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator); + return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken) { + return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken); }); } diff --git a/lib/Db/Room.php b/lib/Db/Room.php index 01141fe..068f96f 100644 --- a/lib/Db/Room.php +++ b/lib/Db/Room.php @@ -20,6 +20,7 @@ use OCP\AppFramework\Db\Entity; * @method bool getEveryoneIsModerator() * @method bool getRequireModerator() * @method bool getEveryoneIsModerator() + * @method string getModeratorToken() * @method void setUid(string $uid) * @method void setName(string $name) * @method void setAttendeePassword(string $pw) @@ -32,6 +33,7 @@ use OCP\AppFramework\Db\Entity; * @method void setPassword(string $pw) * @method void setEveryoneIsModerator(bool $everyone) * @method void setRequireModerator(bool $require) + * @method void setModeratorToken(string $moderatorToken) */ class Room extends Entity implements JsonSerializable { public const ACCESS_PUBLIC = 'public'; @@ -55,6 +57,7 @@ class Room extends Entity implements JsonSerializable { public $everyoneIsModerator; public $requireModerator = false; public $shared = false; + public $moderatorToken; public function __construct() { $this->addType('maxParticipants', 'integer'); @@ -78,6 +81,7 @@ class Room extends Entity implements JsonSerializable { 'everyoneIsModerator' => boolval($this->everyoneIsModerator), 'requireModerator' => boolval($this->requireModerator), 'shared' => boolval($this->shared), + 'moderatorToken' => $this->moderatorToken, ]; } } diff --git a/lib/Migration/Version000000Date20210122164501.php b/lib/Migration/Version000000Date20210122164501.php new file mode 100644 index 0000000..a2cb558 --- /dev/null +++ b/lib/Migration/Version000000Date20210122164501.php @@ -0,0 +1,42 @@ +hasTable('bbb_rooms')) { + $table = $schema->getTable('bbb_rooms'); + + if (!$table->hasColumn('moderator_token')) { + $table->addColumn('moderator_token', 'string', [ + 'notnull' => false, + 'unique' => true, + 'length' => 64 + ]); + } + + return $schema; + } + + return null; + } +} diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index d66076f..aa338c6 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -90,7 +90,7 @@ class RoomService { return $createdRoom; } - public function update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator) { + public function update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken) { try { $room = $this->mapper->find($id); @@ -98,6 +98,10 @@ class RoomService { $room->setPassword($access === Room::ACCESS_PASSWORD ? $this->humanReadableRandom(8) : null); } + if ($room->moderatorToken !== $moderatorToken) { + $room->setModeratorToken(empty($moderatorToken) ? null : $this->humanReadableRandom(16)); + } + $room->setName($name); $room->setWelcome($welcome); $room->setMaxParticipants(\max($maxParticipants, 0)); diff --git a/tests/Unit/Controller/JoinControllerTest.php b/tests/Unit/Controller/JoinControllerTest.php index 701f721..54fad9a 100644 --- a/tests/Unit/Controller/JoinControllerTest.php +++ b/tests/Unit/Controller/JoinControllerTest.php @@ -95,7 +95,7 @@ class JoinControllerTest extends TestCase { $this->api ->expects($this->once()) ->method('createJoinUrl') - ->with($this->room, 12345, 'User Bar', 'user_bar') + ->with($this->room, 12345, 'User Bar', false, 'user_bar') ->willReturn($url); $result = $this->controller->index(null); @@ -177,7 +177,7 @@ class JoinControllerTest extends TestCase { $this->api ->expects($this->once()) ->method('createJoinUrl') - ->with($this->room, 12345, 'Foo Bar', null) + ->with($this->room, 12345, 'Foo Bar', false, null) ->willReturn($url); $this->invalidDisplayname('a'); diff --git a/ts/Common/Api.ts b/ts/Common/Api.ts index a3b9351..e8deb51 100644 --- a/ts/Common/Api.ts +++ b/ts/Common/Api.ts @@ -38,6 +38,7 @@ export interface Room { everyoneIsModerator: boolean; requireModerator: boolean; shared: boolean; + moderatorToken: string; } export interface RoomShare { @@ -121,14 +122,17 @@ class Api { return response.data; } - public getRoomUrl(room: Room) { + public getRoomUrl(room: Room, forModerator = false) { const shortener = document.getElementById('bbb-root')?.getAttribute('data-shortener') || ''; + const token = (forModerator && room.moderatorToken) ? `${room.uid}/${room.moderatorToken}` : room.uid; if (shortener) { - return shortener.replace(/\{user\}/g, room.userId).replace(/\{token\}/g, room.uid); + return shortener + .replace(/\{user\}/g, room.userId) + .replace(/\{token\}/g, token); } - return window.location.origin + api.getUrl(`b/${room.uid}`); + return window.location.origin + api.getUrl(`b/${token}`); } public async getRooms(): Promise { diff --git a/ts/Manager/EditRoom.tsx b/ts/Manager/EditRoom.tsx index 82e4efc..783224f 100644 --- a/ts/Manager/EditRoom.tsx +++ b/ts/Manager/EditRoom.tsx @@ -5,7 +5,7 @@ import EditRoomDialog from './EditRoomDialog'; type Props = { room: Room; restriction?: Restriction; - updateProperty: (key: string, value: string | boolean | number) => Promise; + updateProperty: (key: string, value: string | boolean | number | null) => Promise; } const EditRoom: React.FC = ({ room, restriction, updateProperty }) => { diff --git a/ts/Manager/EditRoomDialog.tsx b/ts/Manager/EditRoomDialog.tsx index 15b1a72..27695fd 100644 --- a/ts/Manager/EditRoomDialog.tsx +++ b/ts/Manager/EditRoomDialog.tsx @@ -13,12 +13,13 @@ const descriptions: { [key: string]: string } = { access: t('bbb', 'Public: Everyone knowing the link is able to join. Password: Guests have to provide a password. Waiting room: A moderator has to accept every guest before they can join. Internal: Only Nextcloud users can join.'), moderator: t('bbb', 'A moderator is able to manage all participants in a meeting including kicking, muting or selecting a presenter. Users with the role moderator are also able to close a meeting or change the default settings.'), requireModerator: t('bbb', 'If enabled, normal users have to wait until a moderator is in the room.'), + moderatorToken: t('bbb', 'If enabled, a moderator URL is generated which allows access with moderator permission.'), }; type Props = { room: Room; restriction?: Restriction; - updateProperty: (key: string, value: string | boolean | number) => Promise; + updateProperty: (key: string, value: string | boolean | number | null) => Promise; open: boolean; setOpen: (open: boolean) => void; } @@ -123,6 +124,17 @@ const EditRoomDialog: React.FC = ({ room, restriction, updateProperty, op {descriptions.moderator} + +
+ updateProperty('moderatorToken', event.target.checked ? 'true' : null)} /> + +
+ {!!room.moderatorToken && } + {descriptions.moderatorToken}

{t('bbb', 'Miscellaneous')}