From 70b06aa98cfc95c306c316cfbf540c6658008036 Mon Sep 17 00:00:00 2001 From: sualko Date: Thu, 4 Jun 2020 18:56:55 +0200 Subject: [PATCH] feat: add access policy public, require password for guests, moderator approval for guests, only Nextcloud users fix #10 fix #24 --- appinfo/app.php | 14 ++++- lib/BigBlueButton/API.php | 5 ++ lib/Controller/JoinController.php | 27 ++++++++-- lib/Controller/RoomApiController.php | 7 +-- lib/Controller/RoomController.php | 7 +-- lib/Db/Room.php | 10 ++++ .../Version000000Date20200604130935.php | 51 +++++++++++++++++++ lib/Service/RoomService.php | 12 ++++- templates/join.php | 38 ++++++++++++++ templates/publicdisplayname.php | 25 --------- ts/Manager/Api.ts | 10 ++++ ts/Manager/App.scss | 8 ++- ts/Manager/EditRoomDialog.tsx | 44 +++++++++++++--- ts/join.scss | 36 +++++++++++++ ts/join.ts | 1 + webpack.common.js | 3 ++ 16 files changed, 253 insertions(+), 45 deletions(-) create mode 100644 lib/Migration/Version000000Date20200604130935.php create mode 100644 templates/join.php delete mode 100644 templates/publicdisplayname.php create mode 100644 ts/join.scss create mode 100644 ts/join.ts diff --git a/appinfo/app.php b/appinfo/app.php index dfaac5a..aca4835 100644 --- a/appinfo/app.php +++ b/appinfo/app.php @@ -1,3 +1,15 @@ getConfig()->getAppValue('bbb', 'api.url'); +$parsedApiUrl = @parse_url($apiUrl); + +if ($parsedApiUrl !== false) { + $manager = \OC::$server->getContentSecurityPolicyManager(); + $policy = new \OCP\AppFramework\Http\EmptyContentSecurityPolicy(); + + $policy->addAllowedFormActionDomain(($parsedApiUrl['scheme'] ?: 'https') . '://' . $parsedApiUrl['host']); + + $manager->addDefaultPolicy($policy); +} \ No newline at end of file diff --git a/lib/BigBlueButton/API.php b/lib/BigBlueButton/API.php index f56acf7..5ffaf0a 100644 --- a/lib/BigBlueButton/API.php +++ b/lib/BigBlueButton/API.php @@ -58,6 +58,7 @@ class API $joinMeetingParams->setCreationTime($creationTime); $joinMeetingParams->setJoinViaHtml5(true); $joinMeetingParams->setRedirect(true); + $joinMeetingParams->setGuest($uid === null); if ($uid) { $joinMeetingParams->setUserId($uid); @@ -114,6 +115,10 @@ class API $createMeetingParams->addPresentation($presentation->getUrl(), null, $presentation->getFilename()); } + if ($room->access === Room::ACCESS_WAITING_ROOM) { + $createMeetingParams->setGuestPolicyAskModerator(); + } + return $createMeetingParams; } diff --git a/lib/Controller/JoinController.php b/lib/Controller/JoinController.php index 0853acb..33e2e9f 100644 --- a/lib/Controller/JoinController.php +++ b/lib/Controller/JoinController.php @@ -8,6 +8,7 @@ use OCA\BigBlueButton\NotFoundException; use OCP\AppFramework\Http\RedirectResponse; use OCP\IRequest; use OCP\ISession; +use OCP\IURLGenerator; use OCP\IUserSession; use OCP\IConfig; use OCA\BigBlueButton\Service\RoomService; @@ -25,6 +26,9 @@ class JoinController extends Controller /** @var RoomService */ private $service; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var IUserSession */ private $userSession; @@ -39,6 +43,7 @@ class JoinController extends Controller IRequest $request, ISession $session, RoomService $service, + IURLGenerator $urlGenerator, IUserSession $userSession, IConfig $config, API $api @@ -46,6 +51,7 @@ class JoinController extends Controller parent::__construct($appName, $request, $session); $this->service = $service; + $this->urlGenerator = $urlGenerator; $this->userSession = $userSession; $this->config = $config; $this->api = $api; @@ -68,7 +74,7 @@ class JoinController extends Controller * @PublicPage * @NoCSRFRequired */ - public function index($displayname, $u = '', $filename = '') + public function index($displayname, $u = '', $filename = '', $password = '') { $room = $this->getRoom(); @@ -87,10 +93,21 @@ class JoinController extends Controller if ($userId === $room->userId) { $presentation = new Presentation($u, $filename); } - } elseif (empty($displayname) || strlen($displayname) < 3) { - $response = new TemplateResponse($this->appName, 'publicdisplayname', [ + } elseif ($room->access === Room::ACCESS_INTERNAL) { + return new RedirectResponse( + $this->urlGenerator->linkToRoute('core.login.showLoginForm', [ + 'redirect_url' => $this->urlGenerator->linkToRoute( + 'bbb.join.index', + ['token' => $this->token] + ), + ]) + ); + } elseif (empty($displayname) || strlen($displayname) < 3 || ($room->access === Room::ACCESS_PASSWORD && $password !== $room->password)) { + $response = new TemplateResponse($this->appName, 'join', [ 'room' => $room->name, - 'wrongdisplayname' => !empty($displayname) && strlen($displayname) < 3 + 'wrongdisplayname' => !empty($displayname) && strlen($displayname) < 3, + 'passwordRequired' => $room->access === Room::ACCESS_PASSWORD, + 'wrongPassword' => $password !== $room->password && $password !== '', ], 'guest'); $this->addFormActionDomain($response); @@ -116,7 +133,7 @@ class JoinController extends Controller $response->getContentSecurityPolicy()->addAllowedFormActionDomain(($parsedApiUrl['scheme'] ?: 'https') . '://' . $parsedApiUrl['host']); } - private function getRoom(): Room + private function getRoom(): ?Room { if ($this->room === null) { $this->room = $this->service->findByUid($this->token); diff --git a/lib/Controller/RoomApiController.php b/lib/Controller/RoomApiController.php index 8482114..0228259 100644 --- a/lib/Controller/RoomApiController.php +++ b/lib/Controller/RoomApiController.php @@ -81,10 +81,11 @@ class RoomApiController extends ApiController string $name, string $welcome, int $maxParticipants, - bool $record + bool $record, + string $access ): DataResponse { - return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record) { - return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $this->userId); + return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $access) { + return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $this->userId); }); } diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index d271680..edd2b3b 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -73,10 +73,11 @@ class RoomController extends Controller string $name, string $welcome, int $maxParticipants, - bool $record + bool $record, + string $access ): DataResponse { - return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record) { - return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $this->userId); + return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $access) { + return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $this->userId); }); } diff --git a/lib/Db/Room.php b/lib/Db/Room.php index b98b828..25eddb5 100644 --- a/lib/Db/Room.php +++ b/lib/Db/Room.php @@ -7,6 +7,12 @@ use OCP\AppFramework\Db\Entity; class Room extends Entity implements JsonSerializable { + const ACCESS_PUBLIC = 'public'; + const ACCESS_PASSWORD = 'password'; + const ACCESS_WAITING_ROOM = 'waiting_room'; + const ACCESS_INTERNAL = 'internal'; + const ACCESS_INTERNAL_RESTRICTED = 'internal_restricted'; + public $uid; public $name; public $attendeePassword; @@ -15,6 +21,8 @@ class Room extends Entity implements JsonSerializable public $maxParticipants; public $record; public $userId; + public $access; + public $password; public function __construct() { @@ -31,6 +39,8 @@ class Room extends Entity implements JsonSerializable 'welcome' => $this->welcome, 'maxParticipants' => (int) $this->maxParticipants, 'record' => boolval($this->record), + 'access' => $this->access, + 'password' => $this->password, ]; } } diff --git a/lib/Migration/Version000000Date20200604130935.php b/lib/Migration/Version000000Date20200604130935.php new file mode 100644 index 0000000..ec9f342 --- /dev/null +++ b/lib/Migration/Version000000Date20200604130935.php @@ -0,0 +1,51 @@ +hasTable('bbb_rooms')) { + $table = $schema->getTable('bbb_rooms'); + + if (!$table->hasColumn('access')) { + $table->addColumn('access', 'string', [ + 'notnull' => true, + 'default' => Room::ACCESS_PUBLIC, + ]); + } + + if (!$table->hasColumn('password')) { + $table->addColumn('password', 'string', [ + 'length' => 64, + 'notnull' => false, + ]); + } + + return $schema; + } + + return null; + } +} diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index f0ce7cc..ff3fe74 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -75,15 +75,20 @@ class RoomService return $this->mapper->insert($room); } - public function update($id, $name, $welcome, $maxParticipants, $record, $userId) + public function update($id, $name, $welcome, $maxParticipants, $record, $access, $userId) { try { $room = $this->mapper->find($id, $userId); + if ($room->access !== $access) { + $room->setPassword($access === Room::ACCESS_PASSWORD ? $this->humanReadableRandom(8) : null); + } + $room->setName($name); $room->setWelcome($welcome); $room->setMaxParticipants($maxParticipants); $room->setRecord($record); + $room->setAccess($access); $room->setUserId($userId); return $this->mapper->update($room); @@ -102,4 +107,9 @@ class RoomService $this->handleException($e); } } + + private function humanReadableRandom($length) + { + return \OC::$server->getSecureRandom()->generate($length, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE); + } } diff --git a/templates/join.php b/templates/join.php new file mode 100644 index 0000000..0317e68 --- /dev/null +++ b/templates/join.php @@ -0,0 +1,38 @@ + +
+
+

+ +

t('Please enter your name!')); ?>

+ + +
t('The name must be at least 3 characters long.')); ?>
+ + +
t('You have to provide the correct password to join the meeting.')); ?>
+ +
+ + + + + + + + + + +
+
+
diff --git a/templates/publicdisplayname.php b/templates/publicdisplayname.php deleted file mode 100644 index 363d465..0000000 --- a/templates/publicdisplayname.php +++ /dev/null @@ -1,25 +0,0 @@ - -
-
-

- -

t('Please enter your name!')); ?>

- - -
t('The name must be at least 3 characters long.')); ?>
- -

- - - -

-
-
diff --git a/ts/Manager/Api.ts b/ts/Manager/Api.ts index 6f85076..ce2cf34 100644 --- a/ts/Manager/Api.ts +++ b/ts/Manager/Api.ts @@ -1,5 +1,13 @@ import axios from '@nextcloud/axios'; +export enum Access { + Public = 'public', + Password = 'password', + WaitingRoom = 'waiting_room', + Internal = 'internal', + InternalRestricted = 'internal_restricted', +} + export interface Room { id: number; uid: string; @@ -7,6 +15,8 @@ export interface Room { welcome: string; maxParticipants: number; record: boolean; + access: Access; + password?: string; } export type Recording = { diff --git a/ts/Manager/App.scss b/ts/Manager/App.scss index 0d93b9f..715d0eb 100644 --- a/ts/Manager/App.scss +++ b/ts/Manager/App.scss @@ -168,8 +168,14 @@ min-width: 300px; margin: 2em 0; - input:not([type="checkbox"]) { + input:not([type="checkbox"]), + select { width: 100%; + display: block; + } + + [readonly] { + background-color: #f1f1f1; } em { diff --git a/ts/Manager/EditRoomDialog.tsx b/ts/Manager/EditRoomDialog.tsx index c73ddf1..48460dd 100644 --- a/ts/Manager/EditRoomDialog.tsx +++ b/ts/Manager/EditRoomDialog.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import Dialog from './Dialog'; -import { Room } from './Api'; +import { Room, Access } from './Api'; import { SubmitInput } from './SubmitInput'; const descriptions: { [key: string]: string } = { @@ -8,6 +8,7 @@ const descriptions: { [key: string]: string } = { welcome: t('bbb', 'This message is shown to all users in the chat area after they joined.'), maxParticipants: t('bbb', 'Sets a limit on the number of participants for this room. Zero means there is no limit.'), recording: t('bbb', 'If enabled, the moderator is able to start the recording.'), + 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.'), }; type Props = { @@ -18,11 +19,11 @@ type Props = { const EditRoomDialog: React.FC = ({ room, updateProperty }) => { const [open, setOpen] = useState(false); - function formElement(label: string, field: string, type: 'text' | 'number' = 'text') { + function inputElement(label: string, field: string, type: 'text' | 'number' = 'text') { return (
updateProperty(field, value)} /> @@ -31,6 +32,26 @@ const EditRoomDialog: React.FC = ({ room, updateProperty }) => { ); } + function selectElement(label: string, field: string, value: string, options: {[key: string]: string}, onChange: (value: string) => void) { + return ( +
+ + + + {(value === Access.Password && room.password) && } + {descriptions[field] && {descriptions[field]}} +
+ ); + } + return ( <> = ({ room, updateProperty }) => { title={t('bbb', 'Edit')} /> setOpen(false)} title={t('bbb', 'Edit "{room}"', { room: room.name })}> - {formElement('Name', 'name')} - {formElement('Welcome', 'welcome')} - {formElement('Participant limit', 'maxParticipants', 'number')} + {inputElement(t('bbb', 'Name'), 'name')} + {inputElement(t('bbb', 'Welcome'), 'welcome')} + {inputElement(t('bbb', 'Participant limit'), 'maxParticipants', 'number')} + + {selectElement(t('bbb', 'Access'), 'access', room.access, { + [Access.Public]: t('bbb', 'Public'), + [Access.Password]: t('bbb', 'Internal + Password protection for guests'), + [Access.WaitingRoom]: t('bbb', 'Internal + Waiting room for guests'), + [Access.Internal]: t('bbb', 'Internal'), + // [Access.InternalRestricted]: t('bbb', 'Restricted'), + }, (value) => { + console.log('access', value); + updateProperty('access', value); + })}
diff --git a/ts/join.scss b/ts/join.scss new file mode 100644 index 0000000..873a052 --- /dev/null +++ b/ts/join.scss @@ -0,0 +1,36 @@ +.bbb { + + #displayname, + #password { + margin: 5px 0; + padding-right: 45px; + height: 45px; + box-sizing: border-box; + flex: 1 1 auto; + width: 100% !important; + min-width: 0; + } + + button { + width: 100%; + margin-top: 1em; + } + + .submit-icon { + float: right; + } + + .bbb-container { + position: relative; + margin-top: 1em; + } + + input[type='submit'].icon-confirm { + position: absolute; + top: 0px; + right: -5px; + width: 45px !important; + height: 45px; + background-color: transparent !important; + } +} \ No newline at end of file diff --git a/ts/join.ts b/ts/join.ts new file mode 100644 index 0000000..15f35f5 --- /dev/null +++ b/ts/join.ts @@ -0,0 +1 @@ +import './join.scss'; \ No newline at end of file diff --git a/webpack.common.js b/webpack.common.js index 2666deb..d5a9a8d 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -11,6 +11,9 @@ module.exports = { manager: [ path.join(__dirname, 'ts', 'Manager', 'index.tsx'), ], + join: [ + path.join(__dirname, 'ts', 'join.ts'), + ] }, output: { path: path.resolve(__dirname, './js'),