Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117747825
D2197.1775176602.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
40 KB
Referenced Files
None
Subscribers
None
D2197.1775176602.diff
View Options
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 & 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
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 12:36 AM (10 h, 55 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18821722
Default Alt Text
D2197.1775176602.diff (40 KB)
Attached To
Mode
D2197: Improve handling of screen-sharing connection tokens
Attached
Detach File
Event Timeline