Page MenuHomePhorge

D2197.1775242727.diff
No OneTemporary

Authored By
Unknown
Size
40 KB
Referenced Files
None
Subscribers
None

D2197.1775242727.diff

diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php
--- a/src/app/Http/Controllers/API/V4/OpenViduController.php
+++ b/src/app/Http/Controllers/API/V4/OpenViduController.php
@@ -11,6 +11,8 @@
class OpenViduController extends Controller
{
+ public const AUTH_HEADER = 'X-Meet-Auth-Token';
+
/**
* Accept the room join request.
*
@@ -102,6 +104,37 @@
]);
}
+ /**
+ * Create a connection for screen sharing.
+ *
+ * @param string $id Room identifier (name)
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function createConnection($id)
+ {
+ $room = Room::where('name', $id)->first();
+
+ // This isn't a room, bye bye
+ if (!$room) {
+ return $this->errorResponse(404, \trans('meet.room-not-found'));
+ }
+
+ $connection = $this->getConnectionFromRequest();
+
+ if (
+ !$connection
+ || $connection->session_id != $room->session_id
+ || ($connection->role & Room::ROLE_PUBLISHER) == 0
+ ) {
+ return $this->errorResponse(403);
+ }
+
+ $response = $room->getSessionToken(Room::ROLE_SCREEN);
+
+ return response()->json(['status' => 'success', 'token' => $response['token']]);
+ }
+
/**
* Dismiss the participant/connection from the session.
*
@@ -132,7 +165,7 @@
}
/**
- * Listing of rooms that belong to the current user.
+ * Listing of rooms that belong to the authenticated user.
*
* @return \Illuminate\Http\JsonResponse
*/
@@ -289,12 +322,8 @@
return $this->errorResponse(500, \trans('meet.session-join-error'));
}
- // Create session token for screen sharing connection
- if (($role & Room::ROLE_PUBLISHER) && !empty(request()->input('screenShare'))) {
- $add_token = $room->getSessionToken(Room::ROLE_SCREEN);
-
- $response['shareToken'] = $add_token['token'];
- }
+ // Get up-to-date connections metadata
+ $response['connections'] = $room->getSessionConnections();
// Get up-to-date connections metadata
$response['connections'] = $room->getSessionConnections();
@@ -394,6 +423,18 @@
foreach (request()->input() as $key => $value) {
switch ($key) {
case 'role':
+ // The 'owner' role is not assignable
+ if ($value & Room::ROLE_OWNER && !($connection->role & Room::ROLE_OWNER)) {
+ return $this->errorResponse(403);
+ } elseif (!($value & Room::ROLE_OWNER) && ($connection->role & Room::ROLE_OWNER)) {
+ return $this->errorResponse(403);
+ }
+
+ // The room owner has always a 'moderator' role
+ if (!($value & Room::ROLE_MODERATOR) && $connection->role & Room::ROLE_OWNER) {
+ $value |= Room::ROLE_MODERATOR;
+ }
+
$connection->{$key} = $value;
break;
}
@@ -456,8 +497,38 @@
return true;
}
- // TODO: Moderators authentication
+ // Moderator's authentication via the extra request header
+ if (
+ ($connection = $this->getConnectionFromRequest())
+ && $connection->session_id === $room->session_id
+ && $connection->role & Room::ROLE_MODERATOR
+ ) {
+ return true;
+ }
return false;
}
+
+ /**
+ * Get the connection object for the token in current request headers.
+ * It will also validate the token.
+ *
+ * @return \App\OpenVidu\Connection|null Connection (if exists and the token is valid)
+ */
+ protected function getConnectionFromRequest()
+ {
+ // Authenticate the user via the extra request header
+ if ($token = request()->header(self::AUTH_HEADER)) {
+ list($connId, ) = explode(':', base64_decode($token), 2);
+
+ if (
+ ($connection = Connection::find($connId))
+ && $connection->metadata['authToken'] === $token
+ ) {
+ return $connection;
+ }
+ }
+
+ return null;
+ }
}
diff --git a/src/app/OpenVidu/Room.php b/src/app/OpenVidu/Room.php
--- a/src/app/OpenVidu/Room.php
+++ b/src/app/OpenVidu/Room.php
@@ -230,6 +230,8 @@
if ($response->getStatusCode() == 200) {
$json = json_decode($response->getBody(), true);
+ $authToken = base64_encode($json['id'] . ':' . \random_bytes(16));
+
// Extract the 'token' part of the token, it will be used to authenticate the connection.
// It will be needed in next iterations e.g. to authenticate moderators that aren't
// Kolab4 users (or are just not logged in to Kolab4).
@@ -242,12 +244,13 @@
$conn->session_id = $this->session_id;
$conn->room_id = $this->id;
$conn->role = $role;
- $conn->metadata = ['token' => $url['token']];
+ $conn->metadata = ['token' => $url['token'], 'authToken' => $authToken];
$conn->save();
return [
'session' => $this->session_id,
'token' => $json['token'],
+ 'authToken' => $authToken,
'connectionId' => $json['id'],
'role' => $role,
];
diff --git a/src/resources/js/meet/app.js b/src/resources/js/meet/app.js
--- a/src/resources/js/meet/app.js
+++ b/src/resources/js/meet/app.js
@@ -44,7 +44,6 @@
let subscribersContainer
OV = new OpenVidu()
- screenOV = new OpenVidu()
// If there's anything to do, do it here.
//OV.setAdvancedConfiguration(config)
@@ -131,11 +130,14 @@
})
session.on('connectionDestroyed', event => {
- let conn = connections[event.connection.connectionId]
+ let connectionId = event.connection.connectionId
+ let conn = connections[connectionId]
+
if (conn) {
$(conn.element).remove()
- delete connections[event.connection.connectionId]
+ delete connections[connectionId]
}
+
resize()
})
@@ -689,13 +691,16 @@
*/
function switchScreen(callback) {
if (screenPublisher) {
+ // Note: This is what the original openvidu-call app does.
+ // It is probably better for performance reasons to close the connection,
+ // than to use unpublish() and keep the connection open.
screenSession.disconnect()
screenSession = null
screenPublisher = null
if (callback) {
- // Note: Disconnecting invalidates the token. The callback should request
- // a new token for the next screen sharing session.
+ // Note: Disconnecting invalidates the token, we have to inform the vue component
+ // to update UI state (and be prepared to request a new token).
callback(false)
}
@@ -721,7 +726,9 @@
// It's me
if (session.connection.connectionId == data.connectionId) {
const rolePublisher = data.role && data.role & Roles.PUBLISHER
+ const roleModerator = data.role && data.role & Roles.MODERATOR
const isPublisher = sessionData.role & Roles.PUBLISHER
+ const isModerator = sessionData.role & Roles.MODERATOR
// Inform the vue component, so it can update some UI controls
let update = () => {
@@ -746,6 +753,17 @@
// update the participant element
sessionData.element = participantUpdate(sessionData.element, sessionData)
+ // promoted/demoted to/from a moderator
+ if ('role' in data) {
+ if ((!isModerator && roleModerator) || (isModerator && !roleModerator)) {
+ // Update all participants, to enable/disable the popup menu
+ Object.keys(connections).forEach(key => {
+ const conn = connections[key]
+ participantUpdate(conn.element, conn)
+ })
+ }
+ }
+
// promoted to a publisher
if ('role' in data && !isPublisher && rolePublisher) {
publisher.createVideoElement(sessionData.element, 'PREPEND')
@@ -894,18 +912,19 @@
const element = $(wrapper)
const isModerator = sessionData.role & Roles.MODERATOR
const isSelf = session.connection.connectionId == params.connectionId
+ const rolePublisher = params.role & Roles.PUBLISHER
+ const roleModerator = params.role & Roles.MODERATOR
+ const roleScreen = params.role & Roles.SCREEN
+ const roleOwner = params.role & Roles.OWNER
// Handle publisher-to-subscriber and subscriber-to-publisher change
- if ('role' in params && !(params.role & Roles.SCREEN)) {
- const rolePublisher = params.role & Roles.PUBLISHER
+ if (!roleScreen) {
const isPublisher = element.is('.meet-video')
if ((rolePublisher && !isPublisher) || (!rolePublisher && isPublisher)) {
element.remove()
return participantCreate(params)
}
-
- element.find('.action-role-publisher input').prop('checked', params.role & Roles.PUBLISHER)
}
if ('audioActive' in params) {
@@ -928,13 +947,24 @@
element.addClass('moderated')
}
- element.find('.dropdown-menu')[isSelf || isModerator ? 'removeClass' : 'addClass']('hidden')
- element.find('.permissions')[isModerator ? 'removeClass' : 'addClass']('hidden')
+ const withPerm = isModerator && !roleScreen && !(roleOwner && !isSelf);
+ const withMenu = isSelf || (isModerator && !roleOwner)
- if ('role' in params && params.role & Roles.SCREEN) {
- element.find('.permissions').addClass('hidden')
+ let elements = {
+ '.dropdown-menu': withMenu,
+ '.permissions': withPerm,
+ 'svg.moderator': roleModerator,
+ 'svg.user': !roleModerator
}
+ Object.keys(elements).forEach(key => {
+ element.find(key)[elements[key] ? 'removeClass' : 'addClass']('hidden')
+ })
+
+ element.find('.action-role-publisher input').prop('checked', rolePublisher)
+ element.find('.action-role-moderator input').prop('checked', roleModerator)
+ .prop('disabled', roleOwner)
+
return wrapper
}
@@ -965,7 +995,10 @@
'<div class="dropdown">'
+ '<a href="#" class="meet-nickname btn" aria-haspopup="true" aria-expanded="false" role="button">'
+ '<span class="content"></span>'
- + '<span class="icon">' + svgIcon('user') + '</span>'
+ + '<span class="icon">'
+ + svgIcon('user', null, 'user')
+ + svgIcon('crown', null, 'moderator hidden')
+ + '</span>'
+ '</a>'
+ '<div class="dropdown-menu">'
+ '<a class="dropdown-item action-nickname" href="#">Nickname</a>'
@@ -977,10 +1010,10 @@
+ '<input type="checkbox" class="custom-control-input">'
+ ' <span class="custom-control-label">Audio &amp; Video publishing</span>'
+ '</label>'
- //+ '<label class="dropdown-item action-role-moderator custom-control custom-switch">'
- // + '<input type="checkbox" class="custom-control-input">'
- // + ' <span class="custom-control-label">Moderation</span>'
- //+ '</label>'
+ + '<label class="dropdown-item action-role-moderator custom-control custom-switch">'
+ + '<input type="checkbox" class="custom-control-input">'
+ + ' <span class="custom-control-label">Moderation</span>'
+ + '</label>'
+ '</div>'
+ '</div>'
+ '</div>'
@@ -1060,6 +1093,50 @@
})
}
+ let connectionRole = () => {
+ if (params.isSelf) {
+ return sessionData.role
+ }
+ if (params.connectionId in connections) {
+ return connections[params.connectionId].role
+ }
+ return 0
+ }
+
+ // Don't close the menu on permission change
+ element.find('.dropdown-menu > label').on('click', e => { e.stopPropagation() })
+
+ if (sessionData.onConnectionChange) {
+ element.find('.action-role-publisher input').on('change', e => {
+ const enabled = e.target.checked
+ let role = connectionRole()
+
+ if (enabled) {
+ role |= Roles.PUBLISHER
+ } else {
+ role |= Roles.SUBSCRIBER
+ if (role & Roles.PUBLISHER) {
+ role ^= Roles.PUBLISHER
+ }
+ }
+
+ sessionData.onConnectionChange(params.connectionId, { role })
+ })
+
+ element.find('.action-role-moderator input').on('change', e => {
+ const enabled = e.target.checked
+ let role = connectionRole()
+
+ if (enabled) {
+ role |= Roles.MODERATOR
+ } else if (role & Roles.MODERATOR) {
+ role ^= Roles.MODERATOR
+ }
+
+ sessionData.onConnectionChange(params.connectionId, { role })
+ })
+ }
+
return element.get(0)
}
@@ -1163,6 +1240,10 @@
let gotSession = !!screenSession
+ if (!screenOV) {
+ screenOV = new OpenVidu()
+ }
+
// Init screen sharing session
if (!gotSession) {
screenSession = screenOV.initSession();
@@ -1170,6 +1251,13 @@
let successFunc = function() {
screenSession.publish(screenPublisher)
+
+ screenSession.on('sessionDisconnected', event => {
+ callback(false)
+ screenSession = null
+ screenPublisher = null
+ })
+
if (callback) {
callback(true)
}
@@ -1178,7 +1266,7 @@
let errorFunc = function() {
screenPublisher = null
if (callback) {
- callback(false)
+ callback(false, true)
}
}
diff --git a/src/resources/themes/app.scss b/src/resources/themes/app.scss
--- a/src/resources/themes/app.scss
+++ b/src/resources/themes/app.scss
@@ -293,6 +293,10 @@
}
}
+#logon-form {
+ flex-basis: auto; // Bootstrap issue? See logon page with width < 992
+}
+
#logon-form-footer {
a:not(:first-child) {
margin-left: 2em;
diff --git a/src/resources/vue/Meet/Room.vue b/src/resources/vue/Meet/Room.vue
--- a/src/resources/vue/Meet/Room.vue
+++ b/src/resources/vue/Meet/Room.vue
@@ -174,6 +174,7 @@
faAlignLeft,
faCog,
faCompress,
+ faCrown,
faDesktop,
faExpand,
faMicrophone,
@@ -189,6 +190,7 @@
faAlignLeft,
faCog,
faCompress,
+ faCrown,
faDesktop,
faExpand,
faMicrophone,
@@ -200,6 +202,7 @@
)
let roomRequest
+ const authHeader = 'X-Meet-Auth-Token'
export default {
components: {
@@ -253,6 +256,8 @@
if (this.meet) {
this.meet.leaveRoom()
}
+
+ delete axios.defaults.headers.common[authHeader]
},
methods: {
authSuccess() {
@@ -298,6 +303,10 @@
if (init) {
this.joinSession()
}
+
+ if (this.session.authToken) {
+ axios.defaults.headers.common[authHeader] = this.session.authToken
+ }
})
.catch(error => {
if (!error.response) {
@@ -616,23 +625,26 @@
this.setMenuItem('video', enabled)
},
switchScreen() {
- this.meet.switchScreen(enabled => {
- this.setMenuItem('screen', enabled)
-
- // After one screen sharing session ended request a new token
- // for the next screen sharing session
- if (!enabled) {
- // TODO: This might need to be a different route. E.g. the room password might have
- // changed since user joined the session
- // Also because it creates a redundant connection (token)
- axios.post('/api/v4/openvidu/rooms/' + this.room, this.post, { ignoreErrors: true })
- .then(response => {
- // Response data contains: session, token and shareToken
- this.session.shareToken = response.data.shareToken
- this.meet.updateSession(this.session)
- })
- }
- })
+ const switchScreenAction = () => {
+ this.meet.switchScreen((enabled, error) => {
+ this.setMenuItem('screen', enabled)
+ if (!enabled && !error) {
+ // Closing a screen sharing connection invalidates the token
+ delete this.session.shareToken
+ }
+ })
+ }
+
+ if (this.session.shareToken || !$('#meet-session-menu').find('.link-screen').is('.text-danger')) {
+ switchScreenAction()
+ } else {
+ axios.post('/api/v4/openvidu/rooms/' + this.room + '/connections')
+ .then(response => {
+ this.session.shareToken = response.data.token
+ this.meet.updateSession(this.session)
+ switchScreenAction()
+ })
+ }
},
updateParticipant(connId, params) {
if (this.isModerator()) {
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -84,6 +84,7 @@
Route::get('openvidu/rooms', 'API\V4\OpenViduController@index');
Route::post('openvidu/rooms/{id}/close', 'API\V4\OpenViduController@closeRoom');
Route::post('openvidu/rooms/{id}/config', 'API\V4\OpenViduController@setRoomConfig');
+
// FIXME: I'm not sure about this one, should we use DELETE request maybe?
Route::post('openvidu/rooms/{id}/connections/{conn}/dismiss', 'API\V4\OpenViduController@dismissConnection');
Route::put('openvidu/rooms/{id}/connections/{conn}', 'API\V4\OpenViduController@updateConnection');
@@ -100,6 +101,12 @@
],
function () {
Route::post('openvidu/rooms/{id}', 'API\V4\OpenViduController@joinRoom');
+ Route::post('openvidu/rooms/{id}/connections', 'API\V4\OpenViduController@createConnection');
+ // FIXME: I'm not sure about this one, should we use DELETE request maybe?
+ Route::post('openvidu/rooms/{id}/connections/{conn}/dismiss', 'API\V4\OpenViduController@dismissConnection');
+ Route::put('openvidu/rooms/{id}/connections/{conn}', 'API\V4\OpenViduController@updateConnection');
+ Route::post('openvidu/rooms/{id}/request/{reqid}/accept', 'API\V4\OpenViduController@acceptJoinRequest');
+ Route::post('openvidu/rooms/{id}/request/{reqid}/deny', 'API\V4\OpenViduController@denyJoinRequest');
}
);
diff --git a/src/tests/Browser/Meet/RoomControlsTest.php b/src/tests/Browser/Meet/RoomControlsTest.php
--- a/src/tests/Browser/Meet/RoomControlsTest.php
+++ b/src/tests/Browser/Meet/RoomControlsTest.php
@@ -273,6 +273,7 @@
->assertElementsCount('@chat-list .message', 0)
->keys('@chat-input', 'test1', '{enter}')
->assertValue('@chat-input', '')
+ ->waitFor('@chat-list .message')
->assertElementsCount('@chat-list .message', 1)
->assertSeeIn('@chat-list .message .nickname', 'john')
->assertSeeIn('@chat-list .message div:last-child', 'test1');
diff --git a/src/tests/Browser/Meet/RoomModeratorTest.php b/src/tests/Browser/Meet/RoomModeratorTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Meet/RoomModeratorTest.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace Tests\Browser\Meet;
+
+use App\OpenVidu\Room;
+use Tests\Browser;
+use Tests\Browser\Components\Dialog;
+use Tests\Browser\Components\Menu;
+use Tests\Browser\Pages\Meet\Room as RoomPage;
+use Tests\TestCaseDusk;
+
+class RoomModeratorTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->clearMeetEntitlements();
+ $this->assignMeetEntitlement('john@kolab.org');
+
+ $room = Room::where('name', 'john')->first();
+ $room->setSettings(['password' => null, 'locked' => null]);
+ if ($room->session_id) {
+ $room->session_id = null;
+ $room->save();
+ }
+ }
+
+ public function tearDown(): void
+ {
+ $this->clearMeetEntitlements();
+
+ $room = Room::where('name', 'john')->first();
+ $room->setSettings(['password' => null, 'locked' => null]);
+ if ($room->session_id) {
+ $room->session_id = null;
+ $room->save();
+ }
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test three users in a room, one will be promoted/demoted to/from a moderator
+ *
+ * @group openvidu
+ */
+ public function testModeratorPromotion(): void
+ {
+ $this->browse(function (Browser $browser, Browser $guest1, Browser $guest2) {
+ // In one browser window join as a room owner
+ $browser->visit(new RoomPage('john'))
+ ->click('@setup-button')
+ ->submitLogon('john@kolab.org', 'simple123')
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->type('@setup-nickname-input', 'John')
+ ->select('@setup-mic-select', '')
+ ->select('@setup-cam-select', '')
+ ->clickWhenEnabled('@setup-button')
+ ->waitFor('@session');
+
+ // In one browser window join as a guest (to be promoted)
+ $guest1->visit(new RoomPage('john'))
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->assertMissing('@setup-status-message')
+ ->assertSeeIn('@setup-button', "JOIN")
+ ->type('@setup-nickname-input', 'Guest1')
+ // Join the room, disable cam/mic
+ ->select('@setup-mic-select', '')
+ ->select('@setup-cam-select', '')
+ ->clickWhenEnabled('@setup-button')
+ ->waitFor('@session');
+
+ // In one browser window join as a guest
+ $guest2->visit(new RoomPage('john'))
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->assertMissing('@setup-status-message')
+ ->assertSeeIn('@setup-button', "JOIN")
+ // Join the room, disable mic
+ ->select('@setup-mic-select', '')
+ ->clickWhenEnabled('@setup-button')
+ ->waitFor('@session');
+
+ // Assert that only the owner is a moderator right now
+ $guest1->waitFor('@session video')
+ ->assertMissing('@session div.meet-video .meet-nickname') // guest2
+ ->assertVisible('@session div.meet-subscriber.self svg.user') // self
+ ->assertMissing('@session div.meet-subscriber.self svg.moderator') // self
+ ->assertMissing('@session div.meet-subscriber:not(.self) svg.user') // owner
+ ->assertVisible('@session div.meet-subscriber:not(.self) svg.moderator') // owner
+ ->click('@session div.meet-subscriber.self .meet-nickname')
+ ->whenAvailable('@session .dropdown-menu', function (Browser $browser) {
+ $browser->assertMissing('.permissions');
+ })
+ ->click('@session div.meet-subscriber:not(.self) .meet-nickname')
+ ->assertMissing('.dropdown-menu');
+
+ $guest2->waitFor('@session video')
+ ->assertVisible('@session div.meet-video svg.user') // self
+ ->assertMissing('@session div.meet-video svg.moderator'); // self
+ /*
+ it does not work because the order is different all the time
+
+ ->assertMissing('@session div.meet-subscriber:nth-child(1) svg.user') // owner
+ ->assertVisible('@session div.meet-subscriber:nth-child(1) svg.moderator') // owner
+ ->assertVisible('@session div.meet-subscriber:nth-child(2) svg.user') // guest1
+ ->assertMissing('@session div.meet-subscriber:nth-child(2) svg.moderator'); // guest1
+ */
+
+ // Promote guest1 to a moderator
+ $browser->waitFor('@session video')
+ ->assertMissing('@session div.meet-subscriber.self svg.user') // self
+ ->assertVisible('@session div.meet-subscriber.self svg.moderator') // self
+ ->click('@session div.meet-subscriber.self .meet-nickname')
+ ->whenAvailable('@session .dropdown-menu', function (Browser $browser) {
+ $browser->assertChecked('.action-role-moderator input')
+ ->assertDisabled('.action-role-moderator input');
+ })
+ ->click('@session div.meet-subscriber:not(.self) .meet-nickname')
+ ->whenAvailable('@session div.meet-subscriber:not(.self) .dropdown-menu', function (Browser $browser) {
+ $browser->assertNotChecked('.action-role-moderator input')
+ ->click('.action-role-moderator input');
+ });
+
+ // Assert that we have two moderators now
+ $guest2->waitFor('@session div.meet-subscriber:nth-child(2) svg.moderator')
+ ->assertMissing('@session div.meet-subscriber:nth-child(2) svg.user'); // guest1
+
+ $guest1->waitFor('@session div.meet-subscriber.self svg.moderator')
+ ->assertMissing('@session div.meet-subscriber.self svg.user') // self
+ ->assertVisible('@session div.meet-video svg.user') // guest2
+ ->assertMissing('@session div.meet-video svg.moderator') // guest2
+ ->assertMissing('@session div.meet-subscriber:not(.self) svg.user') // owner
+ ->assertVisible('@session div.meet-subscriber:not(.self) svg.moderator') // owner
+ ->click('@session div.meet-subscriber:not(.self) .meet-nickname') // owner
+ ->assertMissing('@session div.meet-subscriber:not(.self) .dropdown-menu')
+ ->click('@session div.meet-subscriber.self .meet-nickname')
+ ->whenAvailable('@session div.meet-subscriber.self .dropdown-menu', function (Browser $browser) {
+ $browser->assertChecked('.action-role-moderator input')
+ ->assertEnabled('.action-role-moderator input')
+ ->assertNotChecked('.action-role-publisher input')
+ ->assertEnabled('.action-role-publisher input');
+ });
+
+ $browser->waitFor('@session div.meet-subscriber:not(.self) svg.moderator')
+ ->assertMissing('@session div.meet-subscriber:not(.self) svg.user');
+
+ // Check if a moderator can unpublish another user
+ $guest1->click('@session div.meet-video .meet-nickname')
+ ->whenAvailable('@session div.meet-video .dropdown-menu', function (Browser $browser) {
+ $browser->assertNotChecked('.action-role-moderator input')
+ ->assertEnabled('.action-role-moderator input')
+ ->assertChecked('.action-role-publisher input')
+ ->assertEnabled('.action-role-publisher input')
+ ->click('.action-role-publisher input');
+ })
+ ->waitUntilMissing('@session div.meet-video');
+
+ $guest2->waitUntilMissing('@session div.meet-video');
+
+ // Demote guest1 back to a normal user
+ $browser->waitFor('@session div.meet-subscriber:nth-child(3)')
+ ->click('@session') // somehow needed to make the next line invoke the menu
+ ->click('@session div.meet-subscriber:nth-child(2) .meet-nickname')
+ ->whenAvailable('@session div.meet-subscriber:nth-child(2) .dropdown-menu', function ($browser) {
+ $browser->assertChecked('.action-role-moderator input')
+ ->click('.action-role-moderator input');
+ })
+ ->waitFor('@session div.meet-subscriber:nth-child(2) svg.user')
+ ->assertMissing('@session div.meet-subscriber:nth-child(2) svg.moderator');
+
+ $guest1->waitFor('@session div.meet-subscriber.self svg.user')
+ ->assertMissing('@session div.meet-subscriber.self svg.moderator')
+ ->click('@session div.meet-subscriber.self .meet-nickname')
+ ->whenAvailable('@session .dropdown-menu', function (Browser $browser) {
+ $browser->assertMissing('.permissions');
+ });
+ });
+ }
+}
diff --git a/src/tests/Browser/Meet/RoomSetupTest.php b/src/tests/Browser/Meet/RoomSetupTest.php
--- a/src/tests/Browser/Meet/RoomSetupTest.php
+++ b/src/tests/Browser/Meet/RoomSetupTest.php
@@ -74,7 +74,7 @@
$room->save();
}
- $this->assignMeetEntitlement('john@kolab.org', 'meet');
+ $this->assignMeetEntitlement('john@kolab.org');
$this->browse(function (Browser $browser) {
$browser->visit(new RoomPage('john'))
@@ -130,7 +130,7 @@
*/
public function testTwoUsersInARoom(): void
{
- $this->assignMeetEntitlement('john@kolab.org', 'meet');
+ $this->assignMeetEntitlement('john@kolab.org');
$this->browse(function (Browser $browser, Browser $guest) {
// In one browser window act as a guest
@@ -293,7 +293,7 @@
*/
public function testSubscribers(): void
{
- $this->assignMeetEntitlement('john@kolab.org', 'meet');
+ $this->assignMeetEntitlement('john@kolab.org');
$this->browse(function (Browser $browser, Browser $guest) {
// Join the room as the owner
diff --git a/src/tests/Feature/Controller/OpenViduTest.php b/src/tests/Feature/Controller/OpenViduTest.php
--- a/src/tests/Feature/Controller/OpenViduTest.php
+++ b/src/tests/Feature/Controller/OpenViduTest.php
@@ -119,7 +119,6 @@
$this->assertSame($session_id, $json['session']);
$this->assertTrue(is_string($session_id) && !empty($session_id));
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
- $this->assertTrue(!array_key_exists('shareToken', $json));
$john_token = $json['token'];
@@ -131,7 +130,6 @@
$this->assertSame(322, $json['code']);
$this->assertTrue(empty($json['token']));
- $this->assertTrue(empty($json['shareToken']));
// Non-owner, now the session exists, with 'init', but no 'canPublish' argument
$response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]);
@@ -143,7 +141,6 @@
$this->assertSame($session_id, $json['session']);
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
$this->assertTrue($json['token'] != $john_token);
- $this->assertTrue(empty($json['shareToken']));
// Non-owner, now the session exists, with 'init', and with 'role=PUBLISHER'
$post = ['canPublish' => true, 'init' => 1];
@@ -156,7 +153,6 @@
$this->assertSame($session_id, $json['session']);
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
$this->assertTrue($json['token'] != $john_token);
- $this->assertTrue(!array_key_exists('shareToken', $json));
$this->assertEmpty($json['config']['password']);
$this->assertEmpty($json['config']['requires_password']);
@@ -332,6 +328,7 @@
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
// TODO: Test a scenario where both password and lock are enabled
+ // TODO: Test accepting/denying as a non-owner moderator
}
/**
@@ -358,8 +355,6 @@
$this->assertSame(Room::ROLE_PUBLISHER, $json['role']);
$this->assertSame($room->session_id, $json['session']);
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
- $this->assertTrue(strpos($json['shareToken'], 'wss://') === 0);
- $this->assertTrue($json['shareToken'] != $json['token']);
}
/**
@@ -415,6 +410,76 @@
$this->assertCount(2, $json);
}
+ /**
+ * Test creating an extra connection for screen sharing
+ *
+ * @group openvidu
+ */
+ public function testCreateConnection(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $room = Room::where('name', 'john')->first();
+ $room->session_id = null;
+ $room->save();
+
+ $this->assignMeetEntitlement($john);
+
+ // First we create the session
+ $post = ['init' => 1, 'canPublish' => 1];
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+ $owner_auth_token = $json['authToken'];
+
+ // And the other user connection
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $conn_id = $json['connectionId'];
+ $auth_token = $json['authToken'];
+
+ // Non-existing room name
+ $response = $this->post("api/v4/openvidu/rooms/non-existing/connections", []);
+ $response->assertStatus(404);
+
+ // No connection token provided
+ $response = $this->post("api/v4/openvidu/rooms/{$room->name}/connections", []);
+ $response->assertStatus(403);
+
+ // Invalid token
+ $response = $this->actingAs($jack)
+ ->withHeaders([OpenViduController::AUTH_HEADER => '123'])
+ ->post("api/v4/openvidu/rooms/{$room->name}/connections", []);
+
+ $response->assertStatus(403);
+
+ // Subscriber can't get the screen-sharing connection
+ // Note: We're acting as Jack because there's no easy way to unset the 'actingAs' user
+ // throughout the test
+ $response = $this->actingAs($jack)
+ ->withHeaders([OpenViduController::AUTH_HEADER => $auth_token])
+ ->post("api/v4/openvidu/rooms/{$room->name}/connections", []);
+
+ $response->assertStatus(403);
+
+ // Publisher can get the connection
+ $response = $this->actingAs($jack)
+ ->withHeaders([OpenViduController::AUTH_HEADER => $owner_auth_token])
+ ->post("api/v4/openvidu/rooms/{$room->name}/connections", []);
+
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertTrue(strpos($json['token'], 'wss://') === 0);
+ $this->assertTrue(strpos($json['token'], 'role=PUBLISHER') !== false);
+ }
+
/**
* Test dismissing a participant (closing a connection)
*
@@ -476,6 +541,20 @@
$this->assertSame('success', $json['status']);
$this->assertNull($room->getOVConnection($conn_id));
+
+ // Test acting as a moderator
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]);
+ $response->assertStatus(200);
+ $json = $response->json();
+ $conn_id = $json['connectionId'];
+
+ // Note: We're acting as Jack because there's no easy way to unset a 'actingAs' user
+ // throughout the test
+ $response = $this->actingAs($jack)
+ ->withHeaders([OpenViduController::AUTH_HEADER => $this->getModeratorToken($room)])
+ ->post("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}/dismiss");
+
+ $response->assertStatus(200);
}
/**
@@ -576,6 +655,7 @@
$response->assertStatus(200);
$json = $response->json();
+ $owner_conn_id = $json['connectionId'];
// And the other user connection
$response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]);
@@ -584,6 +664,7 @@
$json = $response->json();
$conn_id = $json['connectionId'];
+ $auth_token = $json['authToken'];
$room->refresh();
$conn_data = $room->getOVConnection($conn_id);
@@ -616,5 +697,60 @@
$this->assertSame('success', $json['status']);
$this->assertSame($post['role'], Connection::find($conn_id)->role);
+
+ // Access as moderator
+ // Note: We're acting as Jack because there's no easy way to unset a 'actingAs' user
+ // throughout the test
+ $token = $this->getModeratorToken($room);
+ $post = ['role' => Room::ROLE_PUBLISHER];
+ $response = $this->actingAs($jack)->withHeaders([OpenViduController::AUTH_HEADER => $token])
+ ->put("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame($post['role'], Connection::find($conn_id)->role);
+
+ // Assert that it's not possible to add/remove the 'owner' role
+ $post = ['role' => Room::ROLE_PUBLISHER | Room::ROLE_OWNER];
+ $response = $this->actingAs($jack)->withHeaders([OpenViduController::AUTH_HEADER => $token])
+ ->put("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}", $post);
+
+ $response->assertStatus(403);
+
+ $post = ['role' => Room::ROLE_PUBLISHER];
+ $response = $this->actingAs($jack)->withHeaders([OpenViduController::AUTH_HEADER => $token])
+ ->put("api/v4/openvidu/rooms/{$room->name}/connections/{$owner_conn_id}", $post);
+
+ $response->assertStatus(403);
+
+ // Assert that removing a 'moderator' role from the owner is not possible
+ $post = ['role' => Room::ROLE_PUBLISHER | Room::ROLE_OWNER];
+ $response = $this->actingAs($jack)->withHeaders([OpenViduController::AUTH_HEADER => $token])
+ ->put("api/v4/openvidu/rooms/{$room->name}/connections/{$owner_conn_id}", $post);
+
+ $response->assertStatus(200);
+
+ $this->assertSame($post['role'] | Room::ROLE_MODERATOR, Connection::find($owner_conn_id)->role);
+
+ // Assert that non-moderator token does not allow access
+ $post = ['role' => Room::ROLE_SUBSCRIBER];
+ $response = $this->actingAs($jack)->withHeaders([OpenViduController::AUTH_HEADER => $auth_token])
+ ->put("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}", $post);
+
+ $response->assertStatus(403);
+ }
+
+ /**
+ * Create a moderator connection to the room session.
+ *
+ * @param \App\OpenVidu\Room $room The room
+ *
+ * @return string The connection authentication token
+ */
+ private function getModeratorToken(Room $room): string
+ {
+ $result = $room->getSessionToken(Room::ROLE_MODERATOR);
+
+ return $result['authToken'];
}
}

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 6:58 PM (4 h, 43 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18821722
Default Alt Text
D2197.1775242727.diff (40 KB)

Event Timeline