diff --git a/lib/BigBlueButton/API.php b/lib/BigBlueButton/API.php index 6fa24ef..a057895 100644 --- a/lib/BigBlueButton/API.php +++ b/lib/BigBlueButton/API.php @@ -170,7 +170,7 @@ class API { $createMeetingParams->setModeratorPW($room->moderatorPassword); $createMeetingParams->setRecord($room->record); $createMeetingParams->setAllowStartStopRecording($room->record); - $createMeetingParams->setLogoutURL($this->urlGenerator->getBaseUrl()); + $createMeetingParams->setLogoutURL($room->logoutURL); $createMeetingParams->setMuteOnStart($room->getJoinMuted()); $createMeetingParams->addMeta('bbb-origin-version', $this->appManager->getAppVersion(Application::ID)); diff --git a/lib/Controller/RestrictionController.php b/lib/Controller/RestrictionController.php index d5c25b3..5489deb 100644 --- a/lib/Controller/RestrictionController.php +++ b/lib/Controller/RestrictionController.php @@ -83,7 +83,8 @@ class RestrictionController extends Controller { int $maxRooms, array $roomTypes, int $maxParticipants, - bool $allowRecording + bool $allowRecording, + bool $allowLogoutURL ): DataResponse { return $this->handleNotFound(function () use ( $id, @@ -91,14 +92,16 @@ class RestrictionController extends Controller { $maxRooms, $roomTypes, $maxParticipants, - $allowRecording) { + $allowRecording, + $allowLogoutURL) { return $this->service->update( $id, $groupId, $maxRooms, $roomTypes, $maxParticipants, - $allowRecording + $allowRecording, + $allowLogoutURL ); }); } diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index a5252c5..0e4a608 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -119,7 +119,8 @@ class RoomController extends Controller { bool $listenOnly, bool $mediaCheck, bool $cleanLayout, - bool $joinMuted + bool $joinMuted, + string $logoutURL ): DataResponse { $room = $this->service->find($id); @@ -137,13 +138,17 @@ class RoomController extends Controller { return new DataResponse(['message' => 'Not allowed to enable recordings.'], Http::STATUS_BAD_REQUEST); } + if (!$restriction->getallowLogoutURL() && $logoutURL !== $room->getlogoutURL()) { + return new DataResponse(['message' => 'Not allowed to enable custom logout URLs'], Https::STATUS_BAD_REQUEST); + } + $disabledRoomTypes = \json_decode($restriction->getRoomTypes()); if ((in_array($access, $disabledRoomTypes) && $access !== $room->getAccess()) || !in_array($access, Room::ACCESS)) { return new DataResponse(['message' => 'Access type not allowed.'], Http::STATUS_BAD_REQUEST); } - return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken, $listenOnly, $mediaCheck, $cleanLayout, $joinMuted) { - return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken, $listenOnly, $mediaCheck, $cleanLayout, $joinMuted); + return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken, $listenOnly, $mediaCheck, $cleanLayout, $joinMuted, $logoutURL) { + return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $requireModerator, $moderatorToken, $listenOnly, $mediaCheck, $cleanLayout, $joinMuted, $logoutURL); }); } diff --git a/lib/Db/Restriction.php b/lib/Db/Restriction.php index 33db7ce..0a8df9f 100644 --- a/lib/Db/Restriction.php +++ b/lib/Db/Restriction.php @@ -12,10 +12,12 @@ use OCP\AppFramework\Db\Entity; * @method string getRoomTypes() * @method int getMaxParticipants() * @method bool getAllowRecording() + * @method bool getAllowLogoutURL() * @method void setRoomId(string $id) * @method void setMaxRooms(int $number) * @method void setMaxParticipants(int $number) * @method void setAllowRecording(bool $allow) + * @method void setAllowLogoutURL(bool $allow) */ class Restriction extends Entity implements JsonSerializable { public const ALL_ID = ''; @@ -25,11 +27,13 @@ class Restriction extends Entity implements JsonSerializable { protected $roomTypes = '[]'; protected $maxParticipants = -1; protected $allowRecording = true; + protected $allowLogoutURL = true; public function __construct() { $this->addType('maxRooms', 'integer'); $this->addType('maxParticipants', 'integer'); $this->addType('allowRecording', 'boolean'); + $this->addType('allowLogoutURL', 'boolean'); } public function jsonSerialize(): array { @@ -40,6 +44,7 @@ class Restriction extends Entity implements JsonSerializable { 'roomTypes' => \json_decode($this->roomTypes), 'maxParticipants' => (int) $this->maxParticipants, 'allowRecording' => boolval($this->allowRecording), + 'allowLogoutURL' => boolval($this->allowLogoutURL), ]; } } diff --git a/lib/Db/Room.php b/lib/Db/Room.php index e7881b8..d8cc785 100644 --- a/lib/Db/Room.php +++ b/lib/Db/Room.php @@ -25,6 +25,7 @@ use OCP\AppFramework\Db\Entity; * @method bool getMediaCheck() * @method bool getCleanLayout() * @method bool getJoinMuted() + * @method string getLogoutURL() * @method void setUid(string $uid) * @method void setName(string $name) * @method void setAttendeePassword(string $pw) @@ -42,6 +43,7 @@ use OCP\AppFramework\Db\Entity; * @method void setMediaCheck(bool $mediaCheck) * @method void setCleanLayout(bool $cleanLayout) * @method void setJoinMuted(bool $joinMuted) + * @method void setLogoutURL(string $logoutURL) */ class Room extends Entity implements JsonSerializable { public const ACCESS_PUBLIC = 'public'; @@ -71,6 +73,7 @@ class Room extends Entity implements JsonSerializable { public $mediaCheck; public $cleanLayout; public $joinMuted; + public $logoutURL; public function __construct() { $this->addType('maxParticipants', 'integer'); @@ -95,6 +98,7 @@ class Room extends Entity implements JsonSerializable { 'record' => boolval($this->record), 'access' => $this->access, 'password' => $this->password, + 'logoutURL' => $this->logoutURL, 'everyoneIsModerator' => boolval($this->everyoneIsModerator), 'requireModerator' => boolval($this->requireModerator), 'shared' => boolval($this->shared), diff --git a/lib/Migration/Version000000Date20220316125900.php b/lib/Migration/Version000000Date20220316125900.php new file mode 100644 index 0000000..67db570 --- /dev/null +++ b/lib/Migration/Version000000Date20220316125900.php @@ -0,0 +1,42 @@ +hasTable('bbb_rooms')) { + $table = $schema->getTable('bbb_rooms'); + + if (!$table->hasColumn('logout_u_r_l')) { + $table->addColumn('logout_u_r_l', 'string', [ + 'notnull' => false, + 'length' => 200 + ]); + } + + return $schema; + } + + return null; + } +} diff --git a/lib/Migration/Version000000Date20220316165602.php b/lib/Migration/Version000000Date20220316165602.php new file mode 100644 index 0000000..516d6cc --- /dev/null +++ b/lib/Migration/Version000000Date20220316165602.php @@ -0,0 +1,41 @@ +hasTable('bbb_restrictions')) { + $table = $schema->getTable('bbb_restrictions'); + + if (!$table->hasColumn('allow_logout_u_r_l')) { + $table->addColumn('allow_logout_u_r_l', 'boolean', [ + 'notnull' => false, + 'default' => false + ]); + } + + return $schema; + } + + return null; + } +} diff --git a/lib/Service/RestrictionService.php b/lib/Service/RestrictionService.php index 1bbcff7..ed8e310 100644 --- a/lib/Service/RestrictionService.php +++ b/lib/Service/RestrictionService.php @@ -57,6 +57,10 @@ class RestrictionService { if (!$restriction->getAllowRecording() && $r->getAllowRecording()) { $restriction->setAllowRecording($r->getAllowRecording()); } + + if (!$restriction->getallowLogoutURL() && $r->getallowLogoutURL()) { + $restriction->setallowLogoutURL($r->getallowLogoutURL()); + } } $restriction->setId(0); @@ -82,7 +86,7 @@ class RestrictionService { return $this->mapper->insert($restriction); } - public function update(int $id, string $groupId, int $maxRooms, array $roomTypes, int $maxParticipants, bool $allowRecording): Restriction { + public function update(int $id, string $groupId, int $maxRooms, array $roomTypes, int $maxParticipants, bool $allowRecording, bool $allowLogoutURL): Restriction { try { $restriction = $this->mapper->find($id); @@ -91,6 +95,7 @@ class RestrictionService { $restriction->setRoomTypes(\json_encode($roomTypes)); $restriction->setMaxParticipants(\max($maxParticipants, -1)); $restriction->setAllowRecording($allowRecording); + $restriction->setallowLogoutURL($allowLogoutURL); return $this->mapper->update($restriction); } catch (Exception $e) { diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 198ae95..c98f1fc 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -8,6 +8,7 @@ use OCA\BigBlueButton\Db\Room; use OCA\BigBlueButton\Db\RoomMapper; use OCA\BigBlueButton\Event\RoomCreatedEvent; +use OCP\IURLGenerator; use OCA\BigBlueButton\Event\RoomDeletedEvent; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; @@ -17,6 +18,9 @@ use OCP\Security\ISecureRandom; class RoomService { + /** @var IURLGenerator */ + private $urlGenerator; + /** @var RoomMapper */ private $mapper; @@ -30,10 +34,12 @@ class RoomService { private $random; public function __construct( + IURLGenerator $urlGenerator, RoomMapper $mapper, IConfig $config, IEventDispatcher $eventDispatcher, ISecureRandom $random) { + $this->urlGenerator = $urlGenerator; $this->mapper = $mapper; $this->config = $config; $this->eventDispatcher = $eventDispatcher; @@ -103,6 +109,7 @@ class RoomService { $room->setMediaCheck($mediaCheck); $room->setCleanLayout(false); $room->setJoinMuted(false); + $room->setLogoutURL($this->urlGenerator->getBaseUrl()); if ($access === Room::ACCESS_PASSWORD) { $room->setPassword($this->humanReadableRandom(8)); @@ -133,7 +140,8 @@ class RoomService { bool $listenOnly, bool $mediaCheck, bool $cleanLayout, - bool $joinMuted) { + bool $joinMuted, + string $logoutURL) { try { $room = $this->mapper->find($id); @@ -156,6 +164,7 @@ class RoomService { $room->setMediaCheck($mediaCheck); $room->setCleanLayout($cleanLayout); $room->setJoinMuted($joinMuted); + $room->setLogoutURL($logoutURL); return $this->mapper->update($room); } catch (Exception $e) { diff --git a/tests/Integration/Service/RestrictionServiceTest.php b/tests/Integration/Service/RestrictionServiceTest.php index 5adcd3d..885519e 100644 --- a/tests/Integration/Service/RestrictionServiceTest.php +++ b/tests/Integration/Service/RestrictionServiceTest.php @@ -67,6 +67,7 @@ class RestrictionServiceTest extends TestCase { $this->assertEquals(10, $updatedRestriction->getMaxRooms()); $this->assertEquals(15, $updatedRestriction->getMaxParticipants()); $this->assertEquals(false, $updatedRestriction->getAllowRecording()); + $this->assertEqauls(false, $updatedRestriction->getAllowLogoutURL()); $this->service->delete($restriction->getId()); } diff --git a/tests/Unit/Service/RestrictionServiceTest.php b/tests/Unit/Service/RestrictionServiceTest.php index c0584df..2b0ea68 100644 --- a/tests/Unit/Service/RestrictionServiceTest.php +++ b/tests/Unit/Service/RestrictionServiceTest.php @@ -25,12 +25,14 @@ class RestrictionServiceTest extends TestCase { $restriction0->setRoomTypes(\json_encode([Room::ACCESS_INTERNAL])); $restriction0->setMaxParticipants(50); $restriction0->setAllowRecording(false); + $restriction0->setAllowLogoutURL(false); $restriction1 = new Restriction(); $restriction1->setRoomTypes(\json_encode([Room::ACCESS_INTERNAL, Room::ACCESS_INTERNAL_RESTRICTED])); $restriction1->setMaxRooms(10); $restriction1->setMaxParticipants(100); $restriction1->setAllowRecording(true); + $restriction1->setAllowRecording(true); $this->mapper ->expects($this->once()) @@ -48,5 +50,6 @@ class RestrictionServiceTest extends TestCase { $this->assertEquals(-1, $result->getMaxRooms()); $this->assertEquals(100, $result->getMaxParticipants()); $this->assertTrue($result->getAllowRecording()); + $this->assertTrue($result->getAllowLogoutURL()); } } diff --git a/ts/Common/Api.ts b/ts/Common/Api.ts index 61c128a..d3d52e3 100644 --- a/ts/Common/Api.ts +++ b/ts/Common/Api.ts @@ -24,6 +24,7 @@ export interface Restriction { roomTypes: string[]; maxParticipants: number; allowRecording: boolean; + allowLogoutURL: boolean; } export interface Room { @@ -44,6 +45,7 @@ export interface Room { mediaCheck: boolean, cleanLayout: boolean, joinMuted: boolean, + logoutURL: string, } export interface RoomShare { diff --git a/ts/Manager/EditRoomDialog.tsx b/ts/Manager/EditRoomDialog.tsx index ba0ff44..8631254 100644 --- a/ts/Manager/EditRoomDialog.tsx +++ b/ts/Manager/EditRoomDialog.tsx @@ -21,6 +21,7 @@ const descriptions: { [key: string]: string } = { mediaCheck: t('bbb', 'If enabled, the user has not to perform an echo call and webcam preview on the first join (available since BBB server 2.3).'), cleanLayout: t('bbb', 'If enabled, the user list, chat area and presentation are hidden by default.'), joinMuted: t('bbb', 'If enabled, all users will join the meeting muted.'), + logoutURL: t('bbb', 'After the meeting ends, all users will be redirected to this URL'), }; const LOGO_QR = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAGnSURBVFiF7de9TxRBHMbxzxG5BonRBOGsVRJfIvGFPwFKX0tjJGqsrfwvCC0YtbJSQyT4J0hB1BhtjZFCI4FoqTRCsbO43g24e+5Q3ZNsZm9+z8zzvZns7Rw9/a0jeIx1bNZ8rYe5WzuFt7CSILj9WsFwHtooADzA7XD/DG/CgDrUwHlcDZ/ncLfdtBoCn9cUGtN8yPiWd/QVikOhfZcQ4G1oD8cA8u2oa9ljyufe3vq+HYx7ph7Avv8YO4Rx2b4uy35oKqubFWhiBl+wiJf4imn0V52smxWYxc22vn7cwwHcqjJZ1RUYi4QXNYUzKQEm/1FvYCIlwEAJz/6UAB9KeN6nBFjAp13qH2VPRjKADdkr9Uek9h3XgicZwGk8wcFI7VConUoFMIZXOLGL5ySWVHgUywI08RSDJbyDwdusE+AGjpb0wjFcrxPgSoXwXJerAnScVgo63gXAaKSv49RVBFgL7dnIwN9dAMR0LrSreUfxbfgCd3BJdix/7Q/pBn5WDPuF++G+gQu4WMjq0Ii9+WPyWeFU3K4WHsm2o+7gNTwMX7SnbW0BScCZl0uGVe8AAAAASUVORK5CYII='; @@ -75,6 +76,19 @@ const EditRoomDialog: React.FC = ({ room, restriction, updateProperty, op ); } + function inputElementRestricted(label: string, field: string, type: 'text' | 'number' = 'text', restricted: boolean) { + return ( +
+ + + updateProperty(field, value)} disabled={restricted} /> + {descriptions[field] && {descriptions[field]}} +
+ ); + } + function selectElement(label: string, field: string, value: string, options: { [key: string]: string }, onChange: (value: string) => void) { return (
@@ -123,6 +137,8 @@ const EditRoomDialog: React.FC = ({ room, restriction, updateProperty, op updateProperty('access', value); })} + {inputElementRestricted(t('bbb', 'Custom redirect after meeting'), 'logoutURL', 'text', !restriction?.allowLogoutURL)} + {room.access === Access.InternalRestricted &&
{descriptions.internalRestrictedShareWith} diff --git a/ts/Manager/SubmitInput.tsx b/ts/Manager/SubmitInput.tsx index 098dbd1..2dc818c 100644 --- a/ts/Manager/SubmitInput.tsx +++ b/ts/Manager/SubmitInput.tsx @@ -10,6 +10,7 @@ export interface SubmitInputProps extends InputHTMLAttributes name: string; onSubmitValue: (value: string) => void; focus?: boolean; + disabled?: boolean; } export interface SubmitInputState { @@ -42,6 +43,7 @@ export class SubmitInput extends Component { autoFocus={this.props.focus} min={this.props.min} max={this.props.max} + disabled={this.props.disabled} /> ; } diff --git a/ts/Restrictions/App.tsx b/ts/Restrictions/App.tsx index d5cd85c..93b9199 100644 --- a/ts/Restrictions/App.tsx +++ b/ts/Restrictions/App.tsx @@ -71,7 +71,10 @@ const App: React.FC = () => { {t('bbb', 'Recording')} - + + {t('bbb', 'Custom redirect after meeting')} + + diff --git a/ts/Restrictions/RestrictionRow.tsx b/ts/Restrictions/RestrictionRow.tsx index 3c5c899..1357fa0 100644 --- a/ts/Restrictions/RestrictionRow.tsx +++ b/ts/Restrictions/RestrictionRow.tsx @@ -71,6 +71,16 @@ const RestrictionRoom: React.FC = (props) => { + + updateRestriction('allowLogoutURL', event.target.checked)} /> + + +