diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e191074e..142335412 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Rate limiting to prevent Room-ID enumeration attacks ([#2518])
+- Greenlight3 import command ([#2664])
### Changed
diff --git a/app/Console/Commands/ImportGreenlight3Command.php b/app/Console/Commands/ImportGreenlight3Command.php
new file mode 100644
index 000000000..ef776addc
--- /dev/null
+++ b/app/Console/Commands/ImportGreenlight3Command.php
@@ -0,0 +1,395 @@
+ 'pgsql',
+ 'host' => $this->argument('host'),
+ 'database' => $this->argument('database'),
+ 'username' => $this->argument('username'),
+ 'password' => $this->argument('password'),
+ 'port' => $this->argument('port'),
+ 'charset' => 'utf8',
+ 'prefix' => '',
+ 'prefix_indexes' => true,
+ 'schema' => 'public',
+ 'sslmode' => 'prefer',
+ ]);
+
+ $users = DB::connection('greenlight')->table('users')->where('provider', 'greenlight')->get(['id', 'name', 'email', 'external_id', 'password_digest']);
+ $rooms = DB::connection('greenlight')->table('rooms')->get(['id', 'friendly_id', 'user_id', 'name']);
+ $sharedAccesses = DB::connection('greenlight')->table('shared_accesses')->get(['room_id', 'user_id']);
+
+ // ask user what room type the imported rooms should get
+ $roomType = $this->option('room-type') != null
+ ? RoomType::firstWhere('name', $this->option('room-type'))->id
+ : select(
+ label: 'What room type should the rooms be assigned to?',
+ options: RoomType::pluck('name', 'id'),
+ scroll: 10
+ );
+
+ // ask user to add prefix to room names
+ $prefix = $this->option('no-confirm') ? null : text(
+ label: 'Prefix for room names',
+ placeholder: 'E.g. (Imported)',
+ hint: '(Optional).'
+ );
+
+ // ask user what room type the imported rooms should get
+ $defaultRole = $this->option('default-role') != null
+ ? Role::firstWhere('name', $this->option('default-role'))->id
+ : select(
+ 'Please select the default role for new imported non-ldap users',
+ options: Role::pluck('name', 'id'),
+ scroll: 10
+ );
+
+ // ask user for presentation path
+ $presentationPath = $this->option('presentation-path')
+ ? $this->option('presentation-path')
+ : text(
+ label: 'Path to GL3 room presentations',
+ placeholder: 'migration/presentations',
+ );
+
+ // Start transaction to rollback if import fails or user cancels
+ DB::beginTransaction();
+
+ try {
+ $userMap = $this->importUsers($users, $defaultRole);
+ $roomMap = $this->importRooms($rooms, $roomType, $userMap, $prefix);
+ $this->importPresentations($roomMap, $presentationPath);
+ $this->importSharedAccesses($sharedAccesses, $roomMap, $userMap);
+
+ if ($this->option('no-confirm') || confirm('Do you wish to commit the import?')) {
+ DB::commit();
+ $this->info('Import completed');
+ } else {
+ DB::rollBack();
+ $this->warn('Import canceled; nothing was imported');
+ }
+ } catch (\Exception $e) {
+ DB::rollBack();
+ $this->error('Import failed: '.$e->getMessage());
+ }
+ }
+
+ /**
+ * Process greenlight user collection and try to import users
+ *
+ * @param Collection $users Collection with all users found in the greenlight database
+ * @param int $defaultRole IDs of the role that should be assigned to new non-ldap users
+ * @return array Array map of greenlight user ids as key and id of the found/created user as value
+ */
+ protected function importUsers(Collection $users, int $defaultRole): array
+ {
+ $this->line('Importing users');
+ $userMap = [];
+
+ $bar = progress(label: 'Importing users', steps: $users->count());
+ $bar->start();
+
+ // counter of user ids that already exists
+ $existed = 0;
+ // counter of users that are created
+ $created = 0;
+
+ foreach ($users as $user) {
+ // check if user with this email exists
+ $dbUser = User::where('email', $user->email)->first();
+ if ($dbUser != null) {
+ // user found, link greenlight user id to id of found user
+ $existed++;
+ } else {
+ // create new user
+ $dbUser = new User;
+ $dbUser->authenticator = $user->external_id ? 'oidc' : 'local';
+ $dbUser->external_id = $user->external_id;
+ $dbUser->email = $user->email;
+ // as greenlight doesn't split the name in first and lastname,
+ // we have to import it as firstname and ask the users or admins to correct it later if desired
+ $dbUser->firstname = $user->name;
+ $dbUser->lastname = '';
+ $dbUser->password = $user->external_id ? Hash::make(Str::random()) : $user->password_digest;
+ $dbUser->locale = config('app.locale');
+ $dbUser->timezone = app(GeneralSettings::class)->default_timezone;
+ $dbUser->save();
+
+ if (! $user->external_id) {
+ $dbUser->roles()->attach($defaultRole);
+ }
+
+ // user was successfully created, link greenlight user id to id of new user
+ $created++;
+ }
+ $userMap[$user->id] = $dbUser->id;
+ $bar->advance();
+ }
+
+ $bar->finish();
+
+ // show import results
+ $this->line('');
+ $this->info($created.' created, '.$existed.' skipped (already existed)');
+
+ $this->line('');
+
+ return $userMap;
+ }
+
+ /**
+ * Process greenlight room collection and create the rooms if not already existing
+ *
+ * @param Collection $rooms Collection with rooms users found in the greenlight database
+ * @param int $roomType ID of the roomtype the rooms should be assigned to
+ * @param array $userMap Array map of greenlight user ids as key and id of the found/created user as value
+ * @param string|null $prefix Prefix to add to room names
+ * @return array Array map of greenlight room ids as key and id of the created room as value
+ */
+ protected function importRooms(Collection $rooms, int $roomType, array $userMap, ?string $prefix): array
+ {
+ $this->line('Importing rooms');
+
+ $bar = $this->output->createProgressBar($rooms->count());
+ $bar->start();
+
+ // counter of room ids that already exists
+ $existed = 0;
+ // counter of rooms that are created
+ $created = 0;
+ // list of rooms that could not be created, e.g. room owner not found
+ $failed = [];
+ // array with the key being the greenlight id and value the new object id
+ $roomMap = [];
+
+ // walk through all found greenlight rooms
+ foreach ($rooms as $room) {
+ // check if a room with the same id exists
+ $dbRoom = Room::find($room->friendly_id);
+ if ($dbRoom != null) {
+ // if found add counter but not add to room map
+ // this also prevents adding shared access, as we can't know if this id collision belongs to the same room
+ // and a shared access import is desired
+ $existed++;
+ $bar->advance();
+
+ continue;
+ }
+
+ // try to find owner of this room
+ if (! isset($userMap[$room->user_id])) {
+ // if owner was not found, eg. missing in the greenlight db or user import failed, don't import room
+ array_push($failed, [$room->name, $room->friendly_id]);
+ $bar->advance();
+
+ continue;
+ }
+
+ // create room with same id, same name, access code
+ $dbRoom = new Room;
+ $dbRoom->id = $room->friendly_id;
+ $dbRoom->name = Str::limit(($prefix != null ? ($prefix.' ') : '').$room->name, 253); // if prefix given, add prefix separated by a space from the title; truncate after 253 chars to prevent too long room names
+ $roomOptions = DB::connection('greenlight')->table('room_meeting_options')->join('meeting_options', 'meeting_option_id', '=', 'meeting_options.id')->where('room_id', $room->id)->get(['name', 'value']);
+
+ // set room settings
+ foreach ($roomOptions as $option) {
+ switch ($option->name) {
+ case 'glAnyoneCanStart':
+ $dbRoom->everyone_can_start = $option->value === 'true';
+ break;
+ case 'glAnyoneJoinAsModerator':
+ $dbRoom->default_role = $option->value ? RoomUserRole::MODERATOR : RoomUserRole::USER;
+ break;
+ case 'glRequireAuthentication':
+ $dbRoom->allow_guests = ! $option->value === 'true';
+ break;
+ case 'glViewerAccessCode':
+ $dbRoom->access_code = $option->value;
+ break;
+ case 'guestPolicy':
+ $dbRoom->lobby = $option->value == 'ASK_MODERATOR' ? RoomLobby::ENABLED : RoomLobby::DISABLED;
+ break;
+ case 'muteOnStart':
+ $dbRoom->mute_on_start = $option->value === 'true';
+ break;
+ case 'record':
+ $dbRoom->record = $option->value === 'true';
+ break;
+ }
+ }
+
+ // associate room with the imported or found user
+ $dbRoom->owner()->associate($userMap[$room->user_id]);
+ // set room type to given roomType for this import batch
+ $dbRoom->roomType()->associate($roomType);
+ $dbRoom->save();
+
+ // increase counter and add room to room map (key = greenlight db id, value = new db id)
+ $created++;
+ $roomMap[$room->id] = $room->friendly_id;
+ $bar->advance();
+ }
+
+ // show import results
+ $this->line('');
+ $this->info($created.' created, '.$existed.' skipped (already existed)');
+
+ // if any room imports failed, show room name, id and access code
+ if (count($failed) > 0) {
+ $this->line('');
+
+ $this->error('Room import failed for the following '.count($failed).' rooms, because no room owner was found:');
+ $this->table(
+ ['Name', 'Friendly ID'],
+ $failed
+ );
+ }
+ $this->line('');
+
+ return $roomMap;
+ }
+
+ /**
+ * Process room mapping and import the rooms' presentations
+ *
+ * @param array $roomMap Array map of greenlight room ids as key and id of the created room as value
+ * @param array $presentationPath Path to GL3 presentation files in default storage
+ */
+ protected function importPresentations(array $roomMap, string $presentationPath)
+ {
+ $this->line('Importing presentations for rooms');
+
+ $bar = $this->output->createProgressBar(count($roomMap));
+ $bar->start();
+
+ // counter of successfully imported presentations
+ $created = 0;
+ // counter of failed imports
+ $failed = 0;
+
+ foreach ($roomMap as $gl3RoomId => $pilosRoomId) {
+ $attachments = DB::connection('greenlight')->table('active_storage_attachments')->where('record_id', $gl3RoomId)->get('blob_id');
+ foreach ($attachments as $attachment) {
+ // Read file path from GL3 database
+ $blob = DB::connection('greenlight')->table('active_storage_blobs')->find($attachment->blob_id);
+ $path = $presentationPath.'/'.substr($blob->key, 0, 2).'/'.substr($blob->key, 2, 2).'/'.$blob->key;
+
+ try {
+ // Prepare uploaded file
+ $presentation = new UploadedFile(Storage::path($path), $blob->filename);
+
+ // Construct RoomFile
+ $file = new RoomFile;
+ $file->path = $path;
+ $file->filename = $blob->filename;
+ $file->use_in_meeting = true;
+ $room = Room::find($pilosRoomId);
+
+ // Save file and room, delete source file
+ $presentation->store($room->id);
+ $room->files()->save($file);
+ $room->updateDefaultFile();
+ Storage::delete($path);
+
+ $created++;
+ } catch (\Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException $e) {
+ $failed++;
+
+ continue;
+ }
+ }
+ $bar->advance();
+ }
+
+ $this->line('');
+ $this->info($created.' created, '.$failed.' skipped (file not found)');
+ $this->line('');
+ }
+
+ /**
+ * Process greenlight shared room access collection and try to create the room membership for the users and rooms
+ * Each user get the moderator role, as that is the greenlight equivalent
+ *
+ * @param Collection $sharedAccesses Collection of user and room ids for shared room access
+ * @param array $roomMap Array map of greenlight room ids as key and id of the created room as value
+ * @param array $userMap Array map of greenlight user ids as key and id of the found/created user as value
+ */
+ protected function importSharedAccesses(Collection $sharedAccesses, array $roomMap, array $userMap)
+ {
+ $this->line('Importing shared room accesses');
+
+ $bar = $this->output->createProgressBar($sharedAccesses->count());
+ $bar->start();
+
+ // counter of shared accesses that are created
+ $created = 0;
+ // counter of shared accesses that could not be created, eg. room or user not imported
+ $failed = 0;
+
+ // walk through all found greenlight shared accesses
+ foreach ($sharedAccesses as $sharedAccess) {
+ $room = $sharedAccess->room_id;
+ $user = $sharedAccess->user_id;
+
+ // check if user id and room id are found in the imported rooms
+ if (! isset($userMap[$user]) || ! isset($roomMap[$room])) {
+ // one or both are not found
+ $bar->advance();
+ $failed++;
+
+ continue;
+ }
+
+ // find room object and add user as moderator to the room
+ $dbRoom = Room::find($roomMap[$room]);
+ $dbRoom->members()->syncWithoutDetaching([$userMap[$user] => ['role' => RoomUserRole::MODERATOR]]);
+ $bar->advance();
+ $created++;
+ }
+
+ // show import result
+ $this->line('');
+ $this->info($created.' created, '.$failed.' skipped (user or room not found)');
+ }
+}
diff --git a/app/Http/Controllers/api/v1/RoomController.php b/app/Http/Controllers/api/v1/RoomController.php
index 736f822a2..e7a9dc4b1 100644
--- a/app/Http/Controllers/api/v1/RoomController.php
+++ b/app/Http/Controllers/api/v1/RoomController.php
@@ -467,7 +467,7 @@ public function authenticate(Room $room, RoomAuthRequest $request)
$accessCode = $request->access_code;
- if (is_numeric($accessCode) && $room->access_code == $accessCode) {
+ if ($room->access_code == $accessCode) {
// Generate new room auth token or retrieve existing one
$roomAuthToken = RoomAuthToken::firstOrCreate([
'room_id' => $room->id,
diff --git a/app/Http/Requests/UpdateRoomSettings.php b/app/Http/Requests/UpdateRoomSettings.php
index 7ad1bb711..1d9e39284 100644
--- a/app/Http/Requests/UpdateRoomSettings.php
+++ b/app/Http/Requests/UpdateRoomSettings.php
@@ -42,12 +42,17 @@ public function rules()
*/
private function getAccessCodeValidationRule(): array
{
- // Support keeping old 6-digit codes (Greenlight v2)
+ // Support keeping
+ // * old 6-digit numeric codes (Greenlight v2)
+ // * old 6-digit alphanumeric codes (Greenlight v3)
$current = $this->room->access_code ?? '';
$incoming = $this->str('access_code') ?? '';
+ $alphanumeric = $current == $incoming && ! is_numeric($current);
$digits = ($current == $incoming && strlen($current) == 6) ? 6 : 9;
- $rules = ['numeric', 'digits:'.$digits, 'bail'];
+ $rules = $alphanumeric
+ ? ['alpha_num:ascii', 'lowercase', 'size:6', 'bail']
+ : ['numeric', 'digits:'.$digits, 'bail'];
// Make sure that the given room type id is a number
if (is_numeric($this->input('room_type'))) {
diff --git a/app/Models/Room.php b/app/Models/Room.php
index 70591c156..9e20fbffe 100644
--- a/app/Models/Room.php
+++ b/app/Models/Room.php
@@ -338,7 +338,8 @@ public function getModeratorOnlyMessage()
$message = __('rooms.invitation.room', ['roomname' => $this->name, 'platform' => $appName]).'
';
$message .= __('rooms.invitation.link').': '.config('app.url').'/rooms/'.$this->id;
if ($this->access_code != null) {
- $message .= '
'.__('rooms.invitation.code').': '.implode('-', str_split($this->access_code, 3));
+ $message .= '
'.__('rooms.invitation.code').': ';
+ $message .= $this->legacy_code ? $this->access_code : implode('-', str_split($this->access_code, 3));
}
return $message;
diff --git a/docs/docs/administration/08-advanced/06-migrate-greenlight.md b/docs/docs/administration/08-advanced/06-migrate-greenlight.md
index 2f523595b..a9da3a3e1 100644
--- a/docs/docs/administration/08-advanced/06-migrate-greenlight.md
+++ b/docs/docs/administration/08-advanced/06-migrate-greenlight.md
@@ -1,6 +1,6 @@
---
-title: Migrate from Greenlight v2
-description: Step by step guide to migrate from Greenlight v2 to PILOS
+title: Migrate from Greenlight
+description: Step by step guide to migrate from Greenlight to PILOS
---
PILOS provides an easy to use command to import all greenlight users (incl. ldap), rooms and shared accesses.
@@ -26,8 +26,17 @@ ports:
Also make sure the internal firewall of the OS and no external firewall is not blocking access to the port and from the host PILOS is running on.
+If you want to import Room presentations, copy Greenlight's active storage directory to the PILOS app storage at `/storage/app/migration/presentations` and specify `--presentation-path=migration/presentations` at the command line.
+Successfully imported presentation files will be moved to a different location.
+
## Running migration command
+The command will output the process of the import and imforms about failed user, room and shared access import.
+
+**Note** If a room with the same room id already exists in PILOS it will NOT be imported and the shared accesses are ignored.
+
+### Greenlight 2
+
```bash
docker compose exec app pilos-cli import:greenlight-v2 {host : ip or hostname of postgres database server}
{port : port of postgres database server}
@@ -42,9 +51,40 @@ docker compose exec app pilos-cli import:greenlight-v2 {host : ip or hostname
docker compose exec app pilos-cli import:greenlight-v2 localhost 5432 greenlight_production postgres 12345678
```
-The command will output the process of the import and imforms about failed user, room and shared access import.
+### Greenlight 3
-**Note** If a room with the same room id already exists in PILOS it will NOT be imported and the shared accesses are ignored.
+```
+Usage:
+ import:greenlight-v3 [options] [--]
+
+Arguments:
+ host ip or hostname of postgres database server
+ port port of postgres database server
+ database greenlight database name, see greenlight .env variable DB_NAME
+ username greenlight database username, see greenlight .env variable DB_USERNAME
+ password greenlight database password, see greenlight .env variable DB_PASSWORD
+
+Options:
+ --no-confirm do not ask if the import should be committed
+ --default-role[=DEFAULT-ROLE] default role for imported external users
+ --room-type[=ROOM-TYPE] room type for imported rooms
+ --presentation-path[=PRESENTATION-PATH] path to room presentations
+```
+
+**Example**
+
+```bash
+docker compose exec app pilos-cli import:greenlight-v3 \
+ --no-confirm \
+ --default-role=User \
+ --room-type=Meeting \
+ --presentation-path=migration/presentations \
+ pg-cluster.svc.cluster.local
+ 5432
+ greenlight-db
+ greenlight-user
+ d4t4basePa$$Word
+```
## Adjust nginx to redirect to PILOS (other host)
diff --git a/resources/js/components/RoomShareButton.vue b/resources/js/components/RoomShareButton.vue
index 0ab1baff2..0eb7f2038 100644
--- a/resources/js/components/RoomShareButton.vue
+++ b/resources/js/components/RoomShareButton.vue
@@ -147,8 +147,10 @@ const roomUrl = computed(() => {
});
const formattedAccessCode = computed(() => {
- return String(props.room.access_code)
- .match(/.{1,3}/g)
- .join("-");
+ return isNaN(props.room.access_code)
+ ? props.room.access_code
+ : String(props.room.access_code)
+ .match(/.{1,3}/g)
+ .join("-");
});
diff --git a/resources/js/views/RoomsView.vue b/resources/js/views/RoomsView.vue
index 298d633b7..31748946d 100644
--- a/resources/js/views/RoomsView.vue
+++ b/resources/js/views/RoomsView.vue
@@ -120,8 +120,8 @@
id="access-code"
v-model="accessCodeInput"
autofocus
- :mask="room.legacy_code ? '999-999' : '999-999-999'"
- :placeholder="room.legacy_code ? '123-456' : '123-456-789'"
+ :mask="room.legacy_code ? '******' : '999-999-999'"
+ :placeholder="room.legacy_code ? '123abc' : '123-456-789'"
:invalid="
accessCodeInvalid ||
formErrors.fieldInvalid('access_code')
diff --git a/tests/Backend/Feature/api/v1/Room/RoomTest.php b/tests/Backend/Feature/api/v1/Room/RoomTest.php
index 3044b0893..8b2e56867 100644
--- a/tests/Backend/Feature/api/v1/Room/RoomTest.php
+++ b/tests/Backend/Feature/api/v1/Room/RoomTest.php
@@ -675,6 +675,12 @@ public function test_auth_with_access_code_guests()
'session_id' => $currentSession->id,
'type' => RoomAuthTokenType::CODE->value,
]);
+
+ // Try with legacy alphanumeric access code
+ $room->access_code = '012abc';
+ $room->save();
+ $this->postJson(route('api.v1.rooms.authenticate', ['room' => $room]), ['type' => RoomAuthTokenType::CODE->value, 'access_code' => $room->access_code])
+ ->assertStatus(201);
}
/**
diff --git a/tests/Backend/Unit/Console/ImportGreenlight3Test.php b/tests/Backend/Unit/Console/ImportGreenlight3Test.php
new file mode 100644
index 000000000..9236d9b7d
--- /dev/null
+++ b/tests/Backend/Unit/Console/ImportGreenlight3Test.php
@@ -0,0 +1,356 @@
+ 'admin',
+ ]);
+
+ Role::firstOrCreate([
+ 'name' => 'student',
+ ]);
+ }
+
+ /**
+ * Mock DB with fake response of the postgres database
+ *
+ * @param Collection $users Collection of Users
+ * @param Collection $rooms Collection Collection of Rooms
+ * @param Collection $sharedAccesses Collection Collection of SharedAccesses
+ * @param Collection $presentations Collection Collection of presentations
+ */
+ private function fakeDatabase(Collection $users, Collection $rooms, Collection $sharedAccesses, Collection $presentations)
+ {
+ // preserve DB default
+ $connection = DB::connection();
+
+ DB::shouldReceive('connection')
+ ->with(null)
+ ->andReturn($connection);
+
+ // mock connection to greenlight postgres database and queries
+ DB::shouldReceive('connection')
+ ->with('greenlight')
+ ->andReturn(Mockery::mock('Illuminate\Database\Connection', function ($mock) use ($presentations, $sharedAccesses, $rooms, $users) {
+ $mock->shouldReceive('table')
+ ->with('users')
+ ->once()
+ ->andReturn(Mockery::mock('Illuminate\Database\Query\Builder', function ($mock) use ($users) {
+ $mock->shouldReceive('where')
+ ->with('provider', 'greenlight')
+ ->andReturn(Mockery::mock('Illuminate\Database\Query\Builder', function ($mock) use ($users) {
+ $mock->shouldReceive('get')
+ ->with(['id', 'name', 'email', 'external_id', 'password_digest'])
+ ->andReturn($users);
+ }));
+ }));
+
+ $mock->shouldReceive('table')
+ ->with('rooms')
+ ->once()
+ ->andReturn(Mockery::mock('Illuminate\Database\Query\Builder', function ($mock) use ($rooms) {
+ $mock->shouldReceive('get')
+ ->with(['id', 'friendly_id', 'user_id', 'name'])
+ ->andReturn($rooms);
+ }));
+
+ $mock->shouldReceive('table')
+ ->with('shared_accesses')
+ ->once()
+ ->andReturn(Mockery::mock('Illuminate\Database\Query\Builder', function ($mock) use ($sharedAccesses) {
+ $mock->shouldReceive('get')
+ ->with(['room_id', 'user_id'])
+ ->andReturn($sharedAccesses);
+ }));
+
+ $mock->shouldReceive('table')
+ ->with('room_meeting_options')
+ ->andReturn(Mockery::mock('Illuminate\Database\Query\Builder', function ($mock) {
+ $mock->shouldReceive('join')
+ ->with('meeting_options', 'meeting_option_id', '=', 'meeting_options.id')
+ ->andReturn(Mockery::mock('Illuminate\Database\Query\Builder', function ($mock) {
+ $roomOptions = [
+ 1 => [],
+ 2 => [],
+ 3 => [
+ (object) [
+ 'name' => 'record',
+ 'value' => true,
+ ],
+ (object) [
+ 'name' => 'glRequireAuthentication',
+ 'value' => false,
+ ],
+ ],
+ 4 => [],
+ 5 => [
+ (object) [
+ 'name' => 'glViewerAccessCode',
+ 'value' => '012345abcd',
+ ],
+ (object) [
+ 'name' => 'guestPolicy',
+ 'value' => 'ASK_MODERATOR',
+ ],
+ (object) [
+ 'name' => 'glAnyoneJoinAsModerator',
+ 'value' => true,
+ ],
+ ],
+ 6 => [
+ (object) [
+ 'name' => 'muteOnStart',
+ 'value' => true,
+ ],
+ (object) [
+ 'name' => 'glAnyoneCanStart',
+ 'value' => true,
+ ],
+ ],
+ 7 => [],
+ 8 => [],
+ ];
+ foreach ($roomOptions as $i => $options) {
+ $mock->shouldReceive('where')
+ ->with('room_id', $i)
+ ->andReturn(Mockery::mock('Illuminate\Database\Query\Builder', function ($mock) use ($options) {
+ $mock->shouldReceive('get')
+ ->with(['name', 'value'])
+ ->andReturn($options);
+ }));
+ }
+ }));
+ }));
+
+ $mock->shouldReceive('table')
+ ->with('active_storage_attachments')
+ ->andReturn(Mockery::mock('Illuminate\Database\Query\Builder', function ($mock) use ($presentations) {
+ foreach ($presentations as $pres) {
+ $mock->shouldReceive('where')
+ ->with('record_id', $pres->room_id)
+ ->andReturn(Mockery::mock('Illuminate\Database\Query\Builder', function ($mock) use ($pres) {
+ $mock->shouldReceive('get')
+ ->with('blob_id')
+ ->andReturn([$pres]);
+ }));
+ }
+
+ $mock->shouldReceive('where')
+ ->andReturn(Mockery::mock('Illuminate\Database\Query\Builder', function ($mock) {
+ $mock->shouldReceive('get')
+ ->with('blob_id')
+ ->andReturn([]);
+ }));
+ }));
+
+ $mock->shouldReceive('table')
+ ->with('active_storage_blobs')
+ ->andReturn(Mockery::mock('Illuminate\Database\Query\Builder', function ($mock) use ($presentations) {
+ foreach ($presentations as $pres) {
+ $mock->shouldReceive('find')
+ ->with($pres->blob_id)
+ ->andReturn($pres);
+ }
+ }));
+ }));
+
+ DB::shouldReceive('beginTransaction')->once();
+ DB::shouldReceive('rollBack')->never();
+ DB::shouldReceive('commit')->once();
+ }
+
+ protected function test_command(?string $prefix = null)
+ {
+ // password for all users
+ $password = Hash::make('secret');
+
+ // create user that exists before import
+ $existingUser = new User;
+ $existingUser->firstname = 'John';
+ $existingUser->lastname = 'Doe';
+ $existingUser->email = 'john.doe@domain.tld';
+ $existingUser->password = $password;
+ $existingUser->save();
+
+ // create room that exists before import
+ $existingRoom = new Room;
+ $existingRoom->name = 'Existing room 1';
+ $existingRoom->roomType()->associate(RoomType::all()->first());
+ $existingRoom->owner()->associate($existingUser);
+ $existingRoom->save();
+
+ // Create fake users, ldap users and social users
+ $users = [];
+ $users[] = new Greenlight3User(1, 'John Doe', 'john.doe@domain.tld', null, $password);
+ $users[] = new Greenlight3User(2, 'John Doe', 'john@domain.tld', null, $password);
+ $users[] = new Greenlight3User(3, 'John Doe', 'j.doe@domain.tld', '79b3db28-31a9-42bf-ac9a-49bdf13b6cc1', null);
+ $users[] = new Greenlight3User(4, 'John Doe', 'j.doe@domain.tld', null, $password);
+
+ // Create fake rooms
+ $rooms = [];
+ $rooms[] = new Greenlight3Room(1, 'abc-def-xyz-123', $users[0]->id, 'Test Room 1');
+ $rooms[] = new Greenlight3Room(2, 'abc-def-xyz-234', $users[1]->id, 'Test Room 2');
+ $rooms[] = new Greenlight3Room(3, 'abc-def-xyz-345', $users[2]->id, 'Test Room 3');
+ $rooms[] = new Greenlight3Room(4, 'abc-def-xyz-456', $users[3]->id, 'Test Room 4');
+
+ $rooms[] = new Greenlight3Room(5, 'hij-klm-xyz-123', $users[0]->id, 'Test Room 5');
+ $rooms[] = new Greenlight3Room(6, 'hij-klm-xyz-234', $users[0]->id, 'Test Room 6');
+ $rooms[] = new Greenlight3Room(7, 'hij-klm-xyz-456', 99, 'Test Room 9', true);
+ $rooms[] = new Greenlight3Room(8, $existingRoom->id, $users[0]->id, 'Test Room 10');
+
+ $presentations = [];
+ $presentations[] = new Greenlight3Attachment('d8db66f8-0915-4aba-b24d-82974300f359', 'feen6movahheegheeg0ovahche8bu3mo', '1testvongpresiher.pdf', '1');
+ $presentations[] = new Greenlight3Attachment('9a91bf3d-e5c4-45c8-8df5-41c76ad403ec', 'xivei7mi0cohtoecacahyaich8ohzaed', '2testvongpresiher.pdf', '2');
+
+ // Create fake shared accesses
+ $sharedAccesses = [];
+ $sharedAccesses[] = new GreenlightSharedAccess(1, 1, 2);
+ $sharedAccesses[] = new GreenlightSharedAccess(2, 1, 3); // shared access should be applied for existing users
+ $sharedAccesses[] = new GreenlightSharedAccess(3, 1, 4);
+
+ $sharedAccesses[] = new GreenlightSharedAccess(4, 1, 99); // invalid user id
+ $sharedAccesses[] = new GreenlightSharedAccess(5, 7, 1); // room that has an invalid owner
+ $sharedAccesses[] = new GreenlightSharedAccess(6, 8, 1); // room that already exists should not be modified
+
+ // mock database connections with fake data
+ $this->fakeDatabase(new Collection($users), new Collection($rooms), new Collection($sharedAccesses), new Collection($presentations));
+
+ $roomType = RoomType::where('name', 'Lecture')->first();
+ $role = Role::where('name', 'student')->first();
+
+ // run artisan command and text questions and outputs
+ $storageMock = Storage::fake();
+ $storageMock->put('migration/presentations/fe/en/feen6movahheegheeg0ovahche8bu3mo', 'Foobar test 123');
+
+ $this->artisan('import:greenlight-v3 localhost 5432 greenlight_production postgres 12345678')
+ ->expectsQuestion('What room type should the rooms be assigned to?', $roomType->id)
+ ->expectsQuestion('Prefix for room names', $prefix)
+ ->expectsQuestion('Please select the default role for new imported non-ldap users', $role->id)
+ ->expectsQuestion('Path to GL3 room presentations', 'migration/presentations')
+ ->expectsOutput('Importing users')
+ ->expectsOutput('2 created, 2 skipped (already existed)')
+ ->expectsOutput('Importing rooms')
+ ->expectsOutput('6 created, 1 skipped (already existed)')
+ ->expectsOutput('Room import failed for the following 1 rooms, because no room owner was found:')
+ ->expectsTable(['Name', 'Friendly ID'], [['Test Room 9', 'hij-klm-xyz-456']])
+ ->expectsOutput('Importing presentations for rooms')
+ ->expectsOutput('1 created, 1 skipped (file not found)')
+ ->expectsOutput('Importing shared room accesses')
+ ->expectsOutput('3 created, 3 skipped (user or room not found)')
+ ->expectsQuestion('Do you wish to commit the import?', 'yes')
+ ->expectsOutput('Import completed')
+ ->assertSuccessful();
+
+ // check amount of users and rooms
+ $this->assertCount(2, User::where('authenticator', 'local')->get());
+ $this->assertCount(1, User::where('authenticator', 'oidc')->get());
+ $this->assertCount(7, Room::all());
+
+ // Check users
+ $this->assertNotNull(User::where([['authenticator', 'local'], ['firstname', 'John'], ['lastname', 'Doe'], ['email', 'john.doe@domain.tld'], ['external_id', null], ['password', $password]])->first());
+ $this->assertNotNull(User::where([['authenticator', 'local'], ['firstname', 'John Doe'], ['lastname', ''], ['email', 'john@domain.tld'], ['external_id', null], ['password', $password]])->first());
+ $this->assertNotNull(User::where([['authenticator', 'oidc'], ['firstname', 'John Doe'], ['lastname', ''], ['email', 'j.doe@domain.tld'], ['external_id', '79b3db28-31a9-42bf-ac9a-49bdf13b6cc1']])->first());
+
+ // Check user roles
+ $this->assertEquals(['student'], User::where([['authenticator', 'local'], ['firstname', 'John Doe'], ['lastname', ''], ['email', 'john@domain.tld'], ['external_id', null], ['password', $password]])->first()->roles->pluck('name')->toArray());
+
+ // Check OIDC users and existing users don't get a default role assigned
+ $this->assertCount(0, User::where([['authenticator', 'oidc'], ['firstname', 'John Doe'], ['lastname', ''], ['email', 'j.doe@domain.tld'], ['external_id', '79b3db28-31a9-42bf-ac9a-49bdf13b6cc1']])->first()->roles);
+ $this->assertCount(0, User::where([['authenticator', 'local'], ['firstname', 'John'], ['lastname', 'Doe'], ['email', 'john.doe@domain.tld'], ['external_id', null], ['password', $password]])->first()->roles);
+
+ // check if all rooms are created
+ $this->assertEqualsCanonicalizing(
+ [$existingRoom->id, 'abc-def-xyz-123', 'abc-def-xyz-234', 'abc-def-xyz-345', 'abc-def-xyz-456', 'hij-klm-xyz-123', 'hij-klm-xyz-234'],
+ Room::all()->pluck('id')->toArray()
+ );
+
+ // check access code
+ $this->assertNull(Room::find('abc-def-xyz-123')->access_code);
+ $this->assertNull(Room::find('abc-def-xyz-234')->access_code);
+ $this->assertNull(Room::find('abc-def-xyz-345')->access_code);
+ $this->assertNull(Room::find('abc-def-xyz-456')->access_code);
+ $this->assertEquals('012345abcd', Room::find('hij-klm-xyz-123')->access_code);
+ $this->assertNull(Room::find('hij-klm-xyz-234')->access_code);
+
+ // check room settings
+ $this->assertFalse(Room::find('abc-def-xyz-234')->record);
+ $this->assertFalse(Room::find('abc-def-xyz-234')->allow_guests);
+ $this->assertTrue(Room::find('abc-def-xyz-345')->record);
+ $this->assertTrue(Room::find('abc-def-xyz-345')->allow_guests);
+
+ $this->assertFalse(Room::find('hij-klm-xyz-123')->mute_on_start);
+ $this->assertFalse(Room::find('hij-klm-xyz-123')->everyone_can_start);
+ $this->assertEquals(RoomLobby::ENABLED, Room::find('hij-klm-xyz-123')->lobby);
+ $this->assertEquals(RoomUserRole::MODERATOR, Room::find('hij-klm-xyz-123')->default_role);
+
+ $this->assertTrue(Room::find('hij-klm-xyz-234')->mute_on_start);
+ $this->assertTrue(Room::find('hij-klm-xyz-234')->everyone_can_start);
+ $this->assertEquals(RoomLobby::DISABLED, Room::find('hij-klm-xyz-234')->lobby);
+ $this->assertEquals(RoomUserRole::USER, Room::find('hij-klm-xyz-234')->default_role);
+
+ // Test room name prefix
+ if ($prefix != null) {
+ $this->assertEquals($prefix.' Test Room 1', Room::find('abc-def-xyz-123')->name);
+ } else {
+ $this->assertEquals('Test Room 1', Room::find('abc-def-xyz-123')->name);
+ }
+
+ // Test presentations
+ $this->assertEquals('1testvongpresiher.pdf', Room::find('abc-def-xyz-123')->files()->first()->filename);
+ $this->assertEmpty(Room::find('abc-def-xyz-234')->files()->get());
+ $this->assertEmpty(Room::find('abc-def-xyz-345')->files()->get());
+
+ // Testing room ownership
+ $this->assertEquals(User::where('email', 'john.doe@domain.tld')->where('authenticator', 'local')->first(), Room::find('abc-def-xyz-123')->owner);
+ $this->assertEquals(User::where('email', 'john@domain.tld')->where('authenticator', 'local')->first(), Room::find('abc-def-xyz-234')->owner);
+ $this->assertEquals(User::where('email', 'j.doe@domain.tld')->where('authenticator', 'oidc')->where('external_id', '79b3db28-31a9-42bf-ac9a-49bdf13b6cc1')->first(), Room::find('abc-def-xyz-345')->owner);
+
+ // Testing room memberships (should be moderator, as that is the greenlight equivalent)
+ $this->assertCount(2, Room::find('abc-def-xyz-123')->members);
+ foreach (Room::find('abc-def-xyz-123')->members as $member) {
+ $this->assertEquals(RoomUserRole::MODERATOR, $member->pivot->role);
+ }
+ $this->assertTrue(Room::find('abc-def-xyz-123')->members->contains(User::where('email', 'john@domain.tld')->where('authenticator', 'local')->first()));
+ }
+
+ public function test()
+ {
+ $this->test_command();
+ }
+
+ public function test_with_prefix()
+ {
+ $this->test_command('Migration:');
+ }
+}
diff --git a/tests/Backend/Unit/Console/helper/Greenlight3Attachment.php b/tests/Backend/Unit/Console/helper/Greenlight3Attachment.php
new file mode 100644
index 000000000..7015a2c9d
--- /dev/null
+++ b/tests/Backend/Unit/Console/helper/Greenlight3Attachment.php
@@ -0,0 +1,25 @@
+blob_id = $blob_id;
+ $this->key = $key;
+ $this->filename = $filename;
+ $this->room_id = $room_id;
+ }
+}
diff --git a/tests/Backend/Unit/Console/helper/Greenlight3Room.php b/tests/Backend/Unit/Console/helper/Greenlight3Room.php
new file mode 100644
index 000000000..27081c881
--- /dev/null
+++ b/tests/Backend/Unit/Console/helper/Greenlight3Room.php
@@ -0,0 +1,28 @@
+id = $id;
+ $this->friendly_id = $friendly_id;
+ $this->user_id = $user_id;
+ $this->name = $name;
+ $this->deleted = $deleted;
+ }
+}
diff --git a/tests/Backend/Unit/Console/helper/Greenlight3User.php b/tests/Backend/Unit/Console/helper/Greenlight3User.php
new file mode 100644
index 000000000..cbef4a970
--- /dev/null
+++ b/tests/Backend/Unit/Console/helper/Greenlight3User.php
@@ -0,0 +1,28 @@
+id = $id;
+ $this->name = $name;
+ $this->email = $email;
+ $this->external_id = $external_id;
+ $this->password_digest = $password_digest;
+ }
+}
diff --git a/tests/Frontend/e2e/RoomsViewGeneral.cy.js b/tests/Frontend/e2e/RoomsViewGeneral.cy.js
index 618aef221..d07a47528 100644
--- a/tests/Frontend/e2e/RoomsViewGeneral.cy.js
+++ b/tests/Frontend/e2e/RoomsViewGeneral.cy.js
@@ -428,7 +428,7 @@ describe("Room View general", function () {
cy.contains("rooms.require_access_code").should("be.visible");
// Submit valid access code
- cy.get("#access-code").type("012345");
+ cy.get("#access-code").type("012abc");
});
// Intercept room auth request
@@ -460,7 +460,7 @@ describe("Room View general", function () {
cy.wait("@roomAuthRequest").then((interception) => {
expect(interception.request.body).to.eql({
- access_code: "012345",
+ access_code: "012abc",
type: 0,
});
});
diff --git a/tests/Frontend/e2e/RoomsViewSettings.cy.js b/tests/Frontend/e2e/RoomsViewSettings.cy.js
index 87146e72f..fb9eeed22 100644
--- a/tests/Frontend/e2e/RoomsViewSettings.cy.js
+++ b/tests/Frontend/e2e/RoomsViewSettings.cy.js
@@ -1236,6 +1236,73 @@ describe("Rooms view settings", function () {
);
});
+ it("change settings with GL3 access code", function () {
+ cy.fixture("roomSettings.json").then((roomSettings) => {
+ roomSettings.data.access_code = "012abc";
+ cy.intercept("GET", "api/v1/rooms/abc-def-123/settings", {
+ statusCode: 200,
+ body: roomSettings,
+ }).as("roomSettingsRequest");
+ });
+
+ cy.visit("/rooms/abc-def-123#tab=settings");
+ cy.wait("@roomSettingsRequest");
+
+ cy.get("#room-setting-access_code")
+ .should("have.value", "012abc")
+ .and("not.be.disabled");
+
+ // Change settings
+ cy.get("#room-setting-name").clear();
+ cy.get("#room-setting-name").type("Meeting Two");
+
+ // Save settings
+ cy.fixture("roomTypesWithSettings.json").then(() => {
+ cy.fixture("roomSettings.json").then((roomSettings) => {
+ roomSettings.data.name = "Meeting Two";
+ roomSettings.data.access_code = "012abc";
+
+ cy.intercept("PUT", "api/v1/rooms/abc-def-123", {
+ statusCode: 200,
+ body: roomSettings,
+ }).as("roomSettingsSaveRequest");
+
+ cy.get('[data-test="room-settings-save-button"]').click();
+ });
+ });
+
+ cy.wait("@roomSettingsSaveRequest").then((interception) => {
+ expect(interception.request.body).to.eql({
+ name: "Meeting Two",
+ expert_mode: true,
+ welcome: "Welcome message",
+ short_description: "Short description",
+ access_code: "012abc",
+ room_type: 2,
+ mute_on_start: true,
+ lock_settings_disable_cam: false,
+ webcams_only_for_moderator: true,
+ lock_settings_disable_mic: false,
+ lock_settings_disable_private_chat: false,
+ lock_settings_disable_public_chat: true,
+ lock_settings_disable_note: true,
+ lock_settings_hide_user_list: true,
+ everyone_can_start: false,
+ allow_membership: false,
+ allow_guests: true,
+ default_role: 1,
+ lobby: 2,
+ visibility: 1,
+ record_attendance: false,
+ record: false,
+ auto_start_recording: false,
+ });
+ });
+
+ // Check that settings are shown correctly
+ cy.get("#room-setting-name").should("have.value", "Meeting Two");
+ });
+
it("change settings errors", function () {
cy.fixture("roomTypesWithSettings.json").then((roomTypes) => {
cy.fixture("roomSettings.json").then((roomSettings) => {