Passed
Push — master ( 5e92ea...2f45b9 )
by
unknown
17:08 queued 08:12
created

ZoomPlugin::createScheduleMeeting()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 15
c 0
b 0
f 0
nc 2
nop 9
dl 0
loc 28
rs 9.7666

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\Course;
6
use Chamilo\CoreBundle\Entity\Session;
7
use Chamilo\CoreBundle\Enums\ActionIcon;
8
use Chamilo\CoreBundle\Enums\ToolIcon;
9
use Chamilo\CourseBundle\Entity\CGroup;
10
use Chamilo\PluginBundle\Zoom\API\JWTClient;
11
use Chamilo\PluginBundle\Zoom\API\MeetingInfoGet;
12
use Chamilo\PluginBundle\Zoom\API\MeetingRegistrant;
13
use Chamilo\PluginBundle\Zoom\API\MeetingSettings;
14
use Chamilo\PluginBundle\Zoom\API\RecordingFile;
15
use Chamilo\PluginBundle\Zoom\API\RecordingList;
16
use Chamilo\PluginBundle\Zoom\Meeting;
17
use Chamilo\PluginBundle\Zoom\MeetingActivity;
18
use Chamilo\PluginBundle\Zoom\MeetingRepository;
19
use Chamilo\PluginBundle\Zoom\Recording;
20
use Chamilo\PluginBundle\Zoom\RecordingRepository;
21
use Chamilo\PluginBundle\Zoom\Registrant;
22
use Chamilo\PluginBundle\Zoom\RegistrantRepository;
23
use Chamilo\UserBundle\Entity\User;
24
use Doctrine\ORM\EntityRepository;
25
use Doctrine\ORM\OptimisticLockException;
26
use Doctrine\ORM\Tools\SchemaTool;
27
use Doctrine\ORM\Tools\ToolsException;
28
29
/**
30
 * Class ZoomPlugin. Integrates Zoom meetings in courses.
31
 */
32
class ZoomPlugin extends Plugin
33
{
34
    const RECORDING_TYPE_CLOUD = 'cloud';
35
    const RECORDING_TYPE_LOCAL = 'local';
36
    const RECORDING_TYPE_NONE  = 'none';
37
38
    public $isCoursePlugin = true;
39
40
    /** @var JWTClient|null Lazy-initialized JWT client */
41
    private ?JWTClient $jwtClient = null;
42
43
    /** @var string Cached API key (normalized) */
44
    private string $apiKey = '';
45
46
    /** @var string Cached API secret (normalized) */
47
    private string $apiSecret = '';
48
49
    /**
50
     * ZoomPlugin constructor.
51
     * Do NOT sign/generate tokens here. Keep it side-effect free.
52
     */
53
    public function __construct()
54
    {
55
        parent::__construct(
56
            '0.3',
57
            'Sébastien Ducoulombier, Julio Montoya',
58
            [
59
                'apiKey'                       => 'text',
60
                'apiSecret'                    => 'text',
61
                'verificationToken'            => 'text',
62
                'enableParticipantRegistration'=> 'boolean',
63
                'enableCloudRecording'         => [
64
                    'type'    => 'select',
65
                    'options' => [
66
                        self::RECORDING_TYPE_CLOUD => 'Cloud',
67
                        self::RECORDING_TYPE_LOCAL => 'Local',
68
                        self::RECORDING_TYPE_NONE  => get_lang('None'),
69
                    ],
70
                ],
71
                'enableGlobalConference'       => 'boolean',
72
                'globalConferenceAllowRoles'   => [
73
                    'type'      => 'select',
74
                    'options'   => [
75
                        PLATFORM_ADMIN => get_lang('Administrator'),
76
                        COURSEMANAGER  => get_lang('Teacher'),
77
                        STUDENT        => get_lang('Learner'),
78
                        STUDENT_BOSS   => get_lang('Superior (n+1)'),
79
                    ],
80
                    'attributes' => ['multiple' => 'multiple'],
81
                ],
82
            ]
83
        );
84
85
        $this->isAdminPlugin = true;
86
87
        // Cache normalized credentials (empty string if missing). Do NOT instantiate JWT client here.
88
        $this->apiKey    = trim((string) ($this->get('apiKey') ?? ''));
89
        $this->apiSecret = trim((string) ($this->get('apiSecret') ?? ''));
90
    }
91
92
    /**
93
     * Safe factory. Kept for BC.
94
     *
95
     * @return ZoomPlugin
96
     */
97
    public static function create()
98
    {
99
        static $instance = null;
100
        return $instance ? $instance : $instance = new self();
101
    }
102
103
    /**
104
     * Lazily build the JWT client only when config is present.
105
     * Never throws here; return null if not configured.
106
     */
107
    private function getJwtClient(): ?JWTClient
108
    {
109
        if ($this->jwtClient instanceof JWTClient) {
110
            return $this->jwtClient;
111
        }
112
113
        // Ensure fresh copy of settings in case they were edited at runtime
114
        $this->apiKey    = trim((string) ($this->get('apiKey') ?? ''));
115
        $this->apiSecret = trim((string) ($this->get('apiSecret') ?? ''));
116
117
        if ($this->apiKey === '' || $this->apiSecret === '') {
118
            return null;
119
        }
120
121
        // Construct but do not sign here. JWT signing must happen on demand.
122
        $this->jwtClient = new JWTClient($this->apiKey, $this->apiSecret);
123
        return $this->jwtClient;
124
    }
125
126
    /**
127
     * Build a short-lived JWT token on demand, with clear error if not configured.
128
     */
129
    private function getJwtToken(): string
130
    {
131
        $client = $this->getJwtClient();
132
        if (!$client) {
133
            // English in code; Spanish message will be shown via Display::return_message if needed.
134
            throw new \RuntimeException('Zoom plugin is not configured (missing API key/secret).');
135
        }
136
        return $client->makeToken(); // relies on the patched JWTClient with makeToken()
137
    }
138
139
    /**
140
     * @return bool
141
     */
142
    public static function currentUserCanJoinGlobalMeeting()
143
    {
144
        $user = api_get_user_entity(api_get_user_id());
145
        if (null === $user) {
146
            return false;
147
        }
148
149
        return
150
            'true' === api_get_plugin_setting('zoom', 'enableGlobalConference')
151
            && in_array(
152
                (api_is_platform_admin() ? PLATFORM_ADMIN : $user->getStatus()),
153
                (array) api_get_plugin_setting('zoom', 'globalConferenceAllowRoles')
154
            );
155
    }
156
157
    /**
158
     * @return array
159
     */
160
    public function getProfileBlockItems()
161
    {
162
        $elements = $this->meetingsToWhichCurrentUserIsRegisteredComingSoon();
163
        $addMeetingLink = false;
164
        if (self::currentUserCanJoinGlobalMeeting()) {
165
            $addMeetingLink = true;
166
        }
167
168
        if ($addMeetingLink) {
169
            $elements[$this->get_lang('Meetings')] = api_get_path(WEB_PLUGIN_PATH) . 'zoom/meetings.php';
170
        }
171
172
        $items = [];
173
        foreach ($elements as $title => $link) {
174
            $items[] = [
175
                'class' => 'video-conference',
176
                'icon'  => Display::getMdiIcon(ToolIcon::VIDEOCONFERENCE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('VideoConference')),
177
                'link'  => $link,
178
                'title' => $title,
179
            ];
180
        }
181
182
        return $items;
183
    }
184
185
    /**
186
     * @return array [ $title => $link ]
187
     */
188
    public function meetingsToWhichCurrentUserIsRegisteredComingSoon()
189
    {
190
        $linkTemplate = api_get_path(WEB_PLUGIN_PATH) . 'zoom/join_meeting.php?meetingId=%s';
191
        $user         = api_get_user_entity(api_get_user_id());
192
        $meetings     = self::getRegistrantRepository()->meetingsComingSoonRegistrationsForUser($user);
193
        $items        = [];
194
195
        foreach ($meetings as $registrant) {
196
            $meeting = $registrant->getMeeting();
197
            $items[sprintf(
198
                $this->get_lang('DateMeetingTitle'),
199
                $meeting->formattedStartTime,
200
                $meeting->getMeetingInfoGet()->topic
201
            )] = sprintf($linkTemplate, $meeting->getId());
202
        }
203
204
        return $items;
205
    }
206
207
    /**
208
     * @return RegistrantRepository|EntityRepository
209
     */
210
    public static function getRegistrantRepository()
211
    {
212
        return Database::getManager()->getRepository(Registrant::class);
213
    }
214
215
    /**
216
     * Creates this plugin's related tables in the internal database.
217
     * Installs course fields in all courses.
218
     *
219
     * @throws ToolsException
220
     * @throws \Doctrine\DBAL\Exception
221
     */
222
    public function install()
223
    {
224
        $schemaManager = Database::getManager()->getConnection()->createSchemaManager();
225
226
        $tablesExists = $schemaManager->tablesExist(
227
            [
228
                'plugin_zoom_meeting',
229
                'plugin_zoom_meeting_activity',
230
                'plugin_zoom_recording',
231
                'plugin_zoom_registrant',
232
            ]
233
        );
234
235
        if ($tablesExists) {
236
            return;
237
        }
238
239
        (new SchemaTool(Database::getManager()))->createSchema(
240
            [
241
                Database::getManager()->getClassMetadata(Meeting::class),
242
                Database::getManager()->getClassMetadata(MeetingActivity::class),
243
                Database::getManager()->getClassMetadata(Recording::class),
244
                Database::getManager()->getClassMetadata(Registrant::class),
245
            ]
246
        );
247
248
        $this->install_course_fields_in_all_courses();
249
    }
250
251
    /**
252
     * Drops this plugins' related tables from the internal database.
253
     * Uninstalls course fields in all courses().
254
     */
255
    public function uninstall()
256
    {
257
        // Never touch JWT here. Keep uninstall resilient even if config is broken.
258
        (new SchemaTool(Database::getManager()))->dropSchema(
259
            [
260
                Database::getManager()->getClassMetadata(Meeting::class),
261
                Database::getManager()->getClassMetadata(MeetingActivity::class),
262
                Database::getManager()->getClassMetadata(Recording::class),
263
                Database::getManager()->getClassMetadata(Registrant::class),
264
            ]
265
        );
266
        $this->uninstall_course_fields_in_all_courses();
267
    }
268
269
    /**
270
     * Generates the search form to include in the meeting list administration page.
271
     * The form has DatePickers 'start' and 'end' and Checkbox 'reloadRecordingLists'.
272
     *
273
     * @return FormValidator the form
274
     */
275
    public function getAdminSearchForm()
276
    {
277
        $form = new FormValidator('search');
278
        $form->addHeader($this->get_lang('SearchMeeting'));
279
        $form->addDatePicker('start', get_lang('Start date'));
280
        $form->addDatePicker('end', get_lang('End date'));
281
        $form->addButtonSearch(get_lang('Search'));
282
283
        $oneMonth = new DateInterval('P1M');
284
        if ($form->validate()) {
285
            try {
286
                $start = new DateTime($form->getSubmitValue('start'));
287
            } catch (Exception $exception) {
288
                $start = new DateTime();
289
                $start->sub($oneMonth);
290
            }
291
            try {
292
                $end = new DateTime($form->getSubmitValue('end'));
293
            } catch (Exception $exception) {
294
                $end = new DateTime();
295
                $end->add($oneMonth);
296
            }
297
        } else {
298
            $start = new DateTime();
299
            $start->sub($oneMonth);
300
            $end = new DateTime();
301
            $end->add($oneMonth);
302
        }
303
304
        try {
305
            $form->setDefaults(
306
                [
307
                    'start' => $start->format('Y-m-d'),
308
                    'end'   => $end->format('Y-m-d'),
309
                ]
310
            );
311
        } catch (Exception $exception) {
312
            error_log(join(':', [__FILE__, __LINE__, $exception]));
313
        }
314
315
        return $form;
316
    }
317
318
    /**
319
     * Generates a meeting edit form and updates the meeting on validation.
320
     *
321
     * @param Meeting $meeting the meeting
322
     *
323
     * @throws Exception
324
     *
325
     * @return FormValidator
326
     */
327
    public function getEditMeetingForm($meeting)
328
    {
329
        $meetingInfoGet = $meeting->getMeetingInfoGet();
330
        $form           = new FormValidator('edit', 'post', $_SERVER['REQUEST_URI']);
331
        $form->addHeader($this->get_lang('UpdateMeeting'));
332
        $form->addText('topic', $this->get_lang('Topic'));
333
334
        if ($meeting->requiresDateAndDuration()) {
335
            $startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('Start Time'));
336
            $form->setRequired($startTimeDatePicker);
337
            $durationNumeric = $form->addNumeric('duration', get_lang('Duration (minutes)'));
338
            $form->setRequired($durationNumeric);
339
        }
340
341
        $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]);
342
        $form->addButtonUpdate(get_lang('Update'));
343
344
        if ($form->validate()) {
345
            if ($meeting->requiresDateAndDuration()) {
346
                $meetingInfoGet->start_time = (new DateTime($form->getSubmitValue('startTime')))->format(
347
                    DateTimeInterface::ISO8601
348
                );
349
                $meetingInfoGet->timezone = date_default_timezone_get();
350
                $meetingInfoGet->duration = (int) $form->getSubmitValue('duration');
351
            }
352
            $meetingInfoGet->topic  = $form->getSubmitValue('topic');
353
            $meetingInfoGet->agenda = $form->getSubmitValue('agenda');
354
355
            try {
356
                $meetingInfoGet->update();
357
                $meeting->setMeetingInfoGet($meetingInfoGet);
358
                Database::getManager()->persist($meeting);
359
                Database::getManager()->flush();
360
361
                Display::addFlash(
362
                    Display::return_message($this->get_lang('MeetingUpdated'), 'confirm')
363
                );
364
            } catch (Exception $exception) {
365
                Display::addFlash(
366
                    Display::return_message($exception->getMessage(), 'error')
367
                );
368
            }
369
        }
370
371
        $defaults = [
372
            'topic'  => $meetingInfoGet->topic,
373
            'agenda' => $meetingInfoGet->agenda,
374
        ];
375
        if ($meeting->requiresDateAndDuration()) {
376
            $defaults['startTime'] = $meeting->startDateTime->format('Y-m-d H:i');
0 ignored issues
show
Bug introduced by
The method format() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

376
            /** @scrutinizer ignore-call */ 
377
            $defaults['startTime'] = $meeting->startDateTime->format('Y-m-d H:i');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
377
            $defaults['duration']  = $meetingInfoGet->duration;
378
        }
379
        $form->setDefaults($defaults);
380
381
        return $form;
382
    }
383
384
    /**
385
     * Generates a meeting delete form and deletes the meeting on validation.
386
     *
387
     * @param Meeting $meeting
388
     * @param string  $returnURL where to redirect to on successful deletion
389
     *
390
     * @throws Exception
391
     *
392
     * @return FormValidator
393
     */
394
    public function getDeleteMeetingForm($meeting, $returnURL)
395
    {
396
        $id   = $meeting->getMeetingId();
397
        $form = new FormValidator('delete', 'post', api_get_self() . '?meetingId=' . $id);
398
        $form->addButtonDelete($this->get_lang('DeleteMeeting'));
399
400
        if ($form->validate()) {
401
            $this->deleteMeeting($meeting, $returnURL);
402
        }
403
404
        return $form;
405
    }
406
407
    /**
408
     * @param Meeting $meeting
409
     * @param string  $returnURL
410
     *
411
     * @return false
412
     */
413
    public function deleteMeeting($meeting, $returnURL)
414
    {
415
        if (null === $meeting) {
416
            return false;
417
        }
418
419
        $em = Database::getManager();
420
        try {
421
            // No need to delete an instant meeting.
422
            if (\Chamilo\PluginBundle\Zoom\API\Meeting::TYPE_INSTANT != $meeting->getMeetingInfoGet()->type) {
423
                $meeting->getMeetingInfoGet()->delete();
424
            }
425
426
            $em->remove($meeting);
427
            $em->flush();
428
429
            Display::addFlash(
430
                Display::return_message($this->get_lang('MeetingDeleted'), 'confirm')
431
            );
432
            api_location($returnURL);
433
        } catch (Exception $exception) {
434
            $this->handleException($exception);
435
        }
436
    }
437
438
    /**
439
     * @param Exception $exception
440
     */
441
    public function handleException($exception)
442
    {
443
        if ($exception instanceof Exception) {
0 ignored issues
show
introduced by
$exception is always a sub-type of Exception.
Loading history...
444
            $error   = json_decode($exception->getMessage());
445
            $message = $exception->getMessage();
446
            if ($error->message) {
447
                $message = $error->message;
448
            }
449
            Display::addFlash(
450
                Display::return_message($message, 'error')
451
            );
452
        }
453
    }
454
455
    /**
456
     * Generates a registrant list update form listing course and session users.
457
     * Updates the list on validation.
458
     *
459
     * @param Meeting $meeting
460
     *
461
     * @throws Exception
462
     *
463
     * @return FormValidator
464
     */
465
    public function getRegisterParticipantForm($meeting)
466
    {
467
        $form          = new FormValidator('register', 'post', $_SERVER['REQUEST_URI']);
468
        $userIdSelect  = $form->addSelect('userIds', $this->get_lang('RegisteredUsers'));
469
        $userIdSelect->setMultiple(true);
470
        $form->addButtonSend($this->get_lang('UpdateRegisteredUserList'));
471
472
        $users = $meeting->getRegistrableUsers();
473
        foreach ($users as $user) {
474
            $userIdSelect->addOption(
475
                api_get_person_name($user->getFirstname(), $user->getLastname()),
476
                $user->getId()
477
            );
478
        }
479
480
        if ($form->validate()) {
481
            $selectedUserIds = $form->getSubmitValue('userIds');
482
            $selectedUsers   = [];
483
            if (!empty($selectedUserIds)) {
484
                foreach ($users as $user) {
485
                    if (in_array($user->getId(), $selectedUserIds)) {
486
                        $selectedUsers[] = $user;
487
                    }
488
                }
489
            }
490
491
            try {
492
                $this->updateRegistrantList($meeting, $selectedUsers);
493
                Display::addFlash(
494
                    Display::return_message($this->get_lang('RegisteredUserListWasUpdated'), 'confirm')
495
                );
496
            } catch (Exception $exception) {
497
                Display::addFlash(
498
                    Display::return_message($exception->getMessage(), 'error')
499
                );
500
            }
501
        }
502
503
        $registeredUserIds = [];
504
        foreach ($meeting->getRegistrants() as $registrant) {
505
            $registeredUserIds[] = $registrant->getUser()->getId();
506
        }
507
        $userIdSelect->setSelected($registeredUserIds);
508
509
        return $form;
510
    }
511
512
    /**
513
     * Generates a form for recording files actions.
514
     *
515
     * @param Meeting $meeting
516
     * @param string  $returnURL
517
     *
518
     * @throws Exception
519
     *
520
     * @return FormValidator
521
     */
522
    public function getFileForm($meeting, $returnURL)
523
    {
524
        $form = new FormValidator('fileForm', 'post', $_SERVER['REQUEST_URI']);
525
526
        if (!$meeting->getRecordings()->isEmpty()) {
527
            $fileIdSelect = $form->addSelect('fileIds', get_lang('Files'));
528
            $fileIdSelect->setMultiple(true);
529
530
            $recordingList = $meeting->getRecordings();
531
            foreach ($recordingList as &$recording) {
532
                $options    = [];
533
                $recordings = $recording->getRecordingMeeting()->recording_files;
534
535
                foreach ($recordings as $file) {
536
                    $options[] = [
537
                        'text'  => sprintf('%s.%s (%s)', $file->recording_type, $file->file_type, $file->file_size),
538
                        'value' => $file->id,
539
                    ];
540
                }
541
542
                $fileIdSelect->addOptGroup(
543
                    $options,
544
                    sprintf("%s (%s)", $recording->formattedStartTime, $recording->formattedDuration)
545
                );
546
            }
547
548
            $actions = [];
549
            if ($meeting->isCourseMeeting()) {
550
                $actions['CreateLinkInCourse'] = $this->get_lang('CreateLinkInCourse');
551
                $actions['CopyToCourse']       = $this->get_lang('CopyToCourse');
552
            }
553
            $actions['DeleteFile'] = $this->get_lang('DeleteFile');
554
555
            $form->addRadio('action', get_lang('Action'), $actions);
556
            $form->addButtonUpdate($this->get_lang('DoIt'));
557
558
            if ($form->validate()) {
559
                $action = $form->getSubmitValue('action');
560
                $idList = $form->getSubmitValue('fileIds');
561
562
                foreach ($recordingList as $recording) {
563
                    $recordings = $recording->getRecordingMeeting()->recording_files;
564
565
                    foreach ($recordings as $file) {
566
                        if (in_array($file->id, $idList)) {
567
                            $name = sprintf(
568
                                $this->get_lang('XRecordingOfMeetingXFromXDurationXDotX'),
569
                                $file->recording_type,
570
                                $meeting->getId(),
571
                                $recording->formattedStartTime,
572
                                $recording->formattedDuration,
573
                                $file->file_type
574
                            );
575
576
                            if ('CreateLinkInCourse' === $action && $meeting->isCourseMeeting()) {
577
                                try {
578
                                    $this->createLinkToFileInCourse($meeting, $file, $name);
579
                                    Display::addFlash(
580
                                        Display::return_message(
581
                                            $this->get_lang('LinkToFileWasCreatedInCourse'),
582
                                            'success'
583
                                        )
584
                                    );
585
                                } catch (Exception $exception) {
586
                                    Display::addFlash(
587
                                        Display::return_message($exception->getMessage(), 'error')
588
                                    );
589
                                }
590
                            } elseif ('CopyToCourse' === $action && $meeting->isCourseMeeting()) {
591
                                try {
592
                                    $this->copyFileToCourse($meeting, $file, $name);
593
                                    Display::addFlash(
594
                                        Display::return_message($this->get_lang('FileWasCopiedToCourse'), 'confirm')
595
                                    );
596
                                } catch (Exception $exception) {
597
                                    Display::addFlash(
598
                                        Display::return_message($exception->getMessage(), 'error')
599
                                    );
600
                                }
601
                            } elseif ('DeleteFile' === $action) {
602
                                try {
603
                                    $name = $file->recording_type;
604
                                    $file->delete();
605
                                    Display::addFlash(
606
                                        Display::return_message($this->get_lang('FileWasDeleted') . ': ' . $name, 'confirm')
607
                                    );
608
                                } catch (Exception $exception) {
609
                                    Display::addFlash(
610
                                        Display::return_message($exception->getMessage(), 'error')
611
                                    );
612
                                }
613
                            }
614
                        }
615
                    }
616
                }
617
618
                api_location($returnURL);
619
            }
620
        }
621
622
        return $form;
623
    }
624
625
    /**
626
     * Adds to the meeting course documents a link to a meeting instance recording file.
627
     *
628
     * @param Meeting       $meeting
629
     * @param RecordingFile $file
630
     * @param string        $name
631
     *
632
     * @throws Exception
633
     */
634
    public function createLinkToFileInCourse($meeting, $file, $name)
635
    {
636
        $course = $meeting->getCourse();
637
        if (null === $course) {
638
            throw new Exception('This meeting is not linked to a course');
639
        }
640
        $courseInfo = api_get_course_info_by_id($course->getId());
641
        if (empty($courseInfo)) {
642
            throw new Exception('This meeting is not linked to a valid course');
643
        }
644
645
        // Implement your cloud link creation here if needed.
646
        // (Left commented as in original source)
647
    }
648
649
    /**
650
     * Copies a recording file to a meeting's course.
651
     *
652
     * @param Meeting       $meeting
653
     * @param RecordingFile $file
654
     * @param string        $name
655
     *
656
     * @throws Exception
657
     */
658
    public function copyFileToCourse($meeting, $file, $name)
659
    {
660
        $course = $meeting->getCourse();
661
        if (null === $course) {
662
            throw new Exception('This meeting is not linked to a course');
663
        }
664
        $courseInfo = api_get_course_info_by_id($course->getId());
665
        if (empty($courseInfo)) {
666
            throw new Exception('This meeting is not linked to a valid course');
667
        }
668
669
        $tmpFile = tmpfile();
670
        if (false === $tmpFile) {
671
            throw new Exception('tmpfile() returned false');
672
        }
673
674
        // === NEW: get short-lived JWT on demand (no token in constructor) ===
675
        $token = $this->getJwtToken();
676
677
        $curl = curl_init($file->getFullDownloadURL($token));
678
        if (false === $curl) {
679
            throw new Exception('Could not init curl');
680
        }
681
682
        if (!curl_setopt_array(
683
            $curl,
684
            [
685
                CURLOPT_FILE           => $tmpFile,
686
                CURLOPT_FOLLOWLOCATION => true,
687
                CURLOPT_MAXREDIRS      => 10,
688
                CURLOPT_TIMEOUT        => 120,
689
            ]
690
        )) {
691
            throw new Exception('Could not set curl options');
692
        }
693
694
        if (false === curl_exec($curl)) {
695
            $err = curl_error($curl);
696
            throw new Exception("curl_exec failed: " . $err);
697
        }
698
699
        $sessionId = 0;
700
        $session   = $meeting->getSession();
701
        if (null !== $session) {
702
            $sessionId = $session->getId();
703
        }
704
705
        $groupId = 0;
706
        $group   = $meeting->getGroup();
707
        if (null !== $group) {
708
            $groupId = $group->getIid();
709
        }
710
711
        $newPath = null;
712
713
        // (Upload to documents tool is left commented as in original code)
714
715
        fclose($tmpFile);
716
        if (false === $newPath) {
717
            throw new Exception('Could not handle uploaded document');
718
        }
719
    }
720
721
    /**
722
     * Generates a form to fast and easily create and start an instant meeting.
723
     * On validation, create it then redirect to it and exit.
724
     */
725
    public function getCreateInstantMeetingForm(
726
        User $user,
727
        Course $course,
728
        CGroup $group = null,
729
        Session $session = null
730
    ) {
731
        $extraUrl = '';
732
        if (!empty($course)) {
733
            $extraUrl = api_get_cidreq();
734
        }
735
736
        $form = new FormValidator('createInstantMeetingForm', 'post', api_get_self() . '?' . $extraUrl, '_blank');
737
        $form->addButton('startButton', $this->get_lang('StartInstantMeeting'), 'video-camera', 'primary');
738
739
        if ($form->validate()) {
740
            try {
741
                $this->startInstantMeeting($this->get_lang('InstantMeeting'), $user, $course, $group, $session);
742
            } catch (Exception $exception) {
743
                Display::addFlash(
744
                    Display::return_message($exception->getMessage(), 'error')
745
                );
746
            }
747
        }
748
749
        return $form;
750
    }
751
752
    /**
753
     * Generates a form to schedule a meeting.
754
     * On validation, creates it and redirects to its page.
755
     *
756
     * @throws Exception
757
     */
758
    public function getScheduleMeetingForm(User $user, Course $course = null, CGroup $group = null, Session $session = null)
759
    {
760
        $extraUrl = '';
761
        if (!empty($course)) {
762
            $extraUrl = api_get_cidreq();
763
        }
764
765
        $form = new FormValidator('scheduleMeetingForm', 'post', api_get_self() . '?' . $extraUrl);
766
        $form->addHeader($this->get_lang('ScheduleAMeeting'));
767
768
        $startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('Start Time'));
769
        $form->setRequired($startTimeDatePicker);
770
771
        $form->addText('topic', $this->get_lang('Topic'), true);
772
        $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]);
773
774
        $durationNumeric = $form->addNumeric('duration', get_lang('Duration (minutes)'));
775
        $form->setRequired($durationNumeric);
776
777
        if (null === $course && 'true' === $this->get('enableGlobalConference')) {
778
            $options = [];
779
            $options['everyone']          = $this->get_lang('ForEveryone');
780
            $options['registered_users']  = $this->get_lang('SomeUsers');
781
            if (!empty($options)) {
782
                if (1 === count($options)) {
783
                    $form->addHidden('type', key($options));
784
                } else {
785
                    $form->addSelect('type', $this->get_lang('ConferenceType'), $options);
786
                }
787
            }
788
        } else {
789
            $form->addHidden('type', 'course'); // To course
790
        }
791
792
        $form->addButtonCreate(get_lang('Save'));
793
794
        if ($form->validate()) {
795
            $type = $form->getSubmitValue('type');
796
797
            switch ($type) {
798
                case 'everyone':
799
                    $user    = null;
800
                    $group   = null;
801
                    $course  = null;
802
                    $session = null;
803
                    break;
804
805
                case 'registered_users':
806
                    $course  = null;
807
                    $session = null;
808
                    break;
809
810
                case 'course':
811
                    $user = null;
812
                    break;
813
            }
814
815
            try {
816
                $newMeeting = $this->createScheduleMeeting(
817
                    $user,
818
                    $course,
819
                    $group,
820
                    $session,
821
                    new DateTime($form->getSubmitValue('startTime')),
822
                    $form->getSubmitValue('duration'),
823
                    $form->getSubmitValue('topic'),
824
                    $form->getSubmitValue('agenda'),
825
                    substr(uniqid('z', true), 0, 10)
826
                );
827
828
                Display::addFlash(Display::return_message($this->get_lang('NewMeetingCreated')));
829
830
                if ($newMeeting->isCourseMeeting()) {
831
                    if ('RegisterAllCourseUsers' === $form->getSubmitValue('userRegistration')) {
832
                        $this->registerAllCourseUsers($newMeeting);
833
                        Display::addFlash(
834
                            Display::return_message($this->get_lang('AllCourseUsersWereRegistered'))
835
                        );
836
                    } elseif ('RegisterTheseGroupMembers' === $form->getSubmitValue('userRegistration')) {
837
                        $userIds = [];
838
                        foreach ($form->getSubmitValue('groupIds') as $groupId) {
839
                            $userIds = array_unique(array_merge($userIds, GroupManager::get_users($groupId)));
840
                        }
841
                        $users = Database::getManager()->getRepository('ChamiloUserBundle:User')->findBy(['id' => $userIds]);
842
                        $this->registerUsers($newMeeting, $users);
843
                        Display::addFlash(
844
                            Display::return_message($this->get_lang('GroupUsersWereRegistered'))
845
                        );
846
                    }
847
                }
848
849
                api_location('meeting.php?meetingId=' . $newMeeting->getMeetingId() . '&' . $extraUrl);
850
            } catch (Exception $exception) {
851
                Display::addFlash(
852
                    Display::return_message($exception->getMessage(), 'error')
853
                );
854
            }
855
        } else {
856
            $form->setDefaults(
857
                [
858
                    'duration'        => 60,
859
                    'userRegistration'=> 'RegisterAllCourseUsers',
860
                ]
861
            );
862
        }
863
864
        return $form;
865
    }
866
867
    /**
868
     * Return the current global meeting (create it if needed).
869
     *
870
     * @throws Exception
871
     */
872
    public function getGlobalMeeting()
873
    {
874
        foreach ($this->getMeetingRepository()->unfinishedGlobalMeetings() as $meeting) {
875
            return $meeting;
876
        }
877
878
        return $this->createGlobalMeeting();
879
    }
880
881
    /**
882
     * @return MeetingRepository|EntityRepository
883
     */
884
    public static function getMeetingRepository()
885
    {
886
        return Database::getManager()->getRepository(Meeting::class);
887
    }
888
889
    /**
890
     * Returns the URL to enter (start or join) a meeting or null if not possible.
891
     *
892
     * @param Meeting $meeting
893
     *
894
     * @throws OptimisticLockException
895
     * @throws Exception
896
     *
897
     * @return string|null
898
     */
899
    public function getStartOrJoinMeetingURL($meeting)
900
    {
901
        $status     = $meeting->getMeetingInfoGet()->status;
902
        $userId     = api_get_user_id();
903
        $currentUser= api_get_user_entity($userId);
904
        $isGlobal   = 'true' === $this->get('enableGlobalConference') && $meeting->isGlobalMeeting();
905
906
        switch ($status) {
907
            case 'ended':
908
                if ($this->userIsConferenceManager($meeting)) {
909
                    return $meeting->getMeetingInfoGet()->start_url;
910
                }
911
                break;
912
913
            case 'waiting':
914
                if ($this->userIsConferenceManager($meeting)) {
915
                    return $meeting->getMeetingInfoGet()->start_url;
916
                }
917
                break;
918
919
            case 'started':
920
                if ($currentUser === $meeting->getUser()) {
921
                    return $meeting->getMeetingInfoGet()->join_url;
922
                }
923
924
                if ($isGlobal) {
925
                    return $this->registerUser($meeting, $currentUser)->getCreatedRegistration()->join_url;
926
                }
927
928
                if ($meeting->isCourseMeeting()) {
929
                    if ($this->userIsCourseConferenceManager()) {
930
                        return $meeting->getMeetingInfoGet()->start_url;
931
                    }
932
933
                    $sessionId = api_get_session_id();
934
                    $courseCode= api_get_course_id();
935
936
                    if (empty($sessionId)) {
937
                        $isSubscribed = CourseManager::is_user_subscribed_in_course($userId, $courseCode, false);
938
                    } else {
939
                        $isSubscribed = CourseManager::is_user_subscribed_in_course($userId, $courseCode, true, $sessionId);
940
                    }
941
942
                    if ($isSubscribed) {
943
                        if ($meeting->isCourseGroupMeeting()) {
944
                            $isInGroup = GroupManager::isUserInGroup($userId, $meeting->getGroup());
945
                            if (false === $isInGroup) {
946
                                throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting'));
947
                            }
948
                        }
949
950
                        if (\Chamilo\PluginBundle\Zoom\API\Meeting::TYPE_INSTANT == $meeting->getMeetingInfoGet()->type) {
951
                            return $meeting->getMeetingInfoGet()->join_url;
952
                        }
953
954
                        return $this->registerUser($meeting, $currentUser)->getCreatedRegistration()->join_url;
955
                    }
956
957
                    throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting'));
958
                }
959
960
                // If registration is required for user meetings
961
                $registrant = $meeting->getRegistrant($currentUser);
962
                if (null == $registrant) {
963
                    throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting'));
964
                }
965
                return $registrant->getCreatedRegistration()->join_url;
966
        }
967
968
        return null;
969
    }
970
971
    /**
972
     * @param Meeting $meeting
973
     */
974
    public function userIsConferenceManager($meeting)
975
    {
976
        if (null === $meeting) {
977
            return false;
978
        }
979
980
        if (api_is_coach() || api_is_platform_admin()) {
981
            return true;
982
        }
983
984
        if ($meeting->isCourseMeeting() && api_get_course_id() && api_is_course_admin()) {
985
            return true;
986
        }
987
988
        return $meeting->isUserMeeting() && $meeting->getUser()->getId() == api_get_user_id();
989
    }
990
991
    /**
992
     * @return bool
993
     */
994
    public function userIsCourseConferenceManager()
995
    {
996
        if (api_is_coach() || api_is_platform_admin()) {
997
            return true;
998
        }
999
1000
        if (api_get_course_id() && api_is_course_admin()) {
1001
            return true;
1002
        }
1003
1004
        return false;
1005
    }
1006
1007
    /**
1008
     * Update local recording list from remote Zoom server's version.
1009
     *
1010
     * @param DateTime $startDate
1011
     * @param DateTime $endDate
1012
     *
1013
     * @throws OptimisticLockException
1014
     * @throws Exception
1015
     */
1016
    public function reloadPeriodRecordings($startDate, $endDate)
1017
    {
1018
        $em            = Database::getManager();
1019
        $recordingRepo = $this->getRecordingRepository();
1020
        $meetingRepo   = $this->getMeetingRepository();
1021
        $recordings    = RecordingList::loadPeriodRecordings($startDate, $endDate);
1022
1023
        foreach ($recordings as $recordingMeeting) {
1024
            $recordingEntity = $recordingRepo->findOneBy(['uuid' => $recordingMeeting->uuid]);
1025
            if (null === $recordingEntity) {
1026
                $recordingEntity = new Recording();
1027
                $meeting         = $meetingRepo->findOneBy(['meetingId' => $recordingMeeting->id]);
1028
                if (null === $meeting) {
1029
                    try {
1030
                        $meetingInfoGet = MeetingInfoGet::fromId($recordingMeeting->id);
1031
                    } catch (Exception $exception) {
1032
                        $meetingInfoGet = null; // deleted meeting with recordings
1033
                    }
1034
                    if (null !== $meetingInfoGet) {
1035
                        $meeting = $this->createMeetingFromMeeting(
1036
                            (new Meeting())->setMeetingInfoGet($meetingInfoGet)
1037
                        );
1038
                        $em->persist($meeting);
1039
                    }
1040
                }
1041
                if (null !== $meeting) {
1042
                    $recordingEntity->setMeeting($meeting);
1043
                }
1044
            }
1045
            $recordingEntity->setRecordingMeeting($recordingMeeting);
1046
            $em->persist($recordingEntity);
1047
        }
1048
1049
        $em->flush();
1050
    }
1051
1052
    /**
1053
     * @return RecordingRepository|EntityRepository
1054
     */
1055
    public static function getRecordingRepository()
1056
    {
1057
        return Database::getManager()->getRepository(Recording::class);
1058
    }
1059
1060
    public function getToolbar($returnUrl = '')
1061
    {
1062
        if (!api_is_platform_admin()) {
1063
            return '';
1064
        }
1065
1066
        $actionsLeft = '';
1067
        $back        = '';
1068
        $courseId    = api_get_course_id();
1069
1070
        if (empty($courseId)) {
1071
            $actionsLeft .= Display::url(
1072
                Display::getMdiIcon(ToolIcon::VIDEOCONFERENCE, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, $this->get_lang('Meetings')),
1073
                api_get_path(WEB_PLUGIN_PATH) . 'zoom/meetings.php'
1074
            );
1075
        } else {
1076
            $actionsLeft .= Display::url(
1077
                Display::getMdiIcon(ToolIcon::VIDEOCONFERENCE, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, $this->get_lang('Meetings')),
1078
                api_get_path(WEB_PLUGIN_PATH) . 'zoom/start.php?' . api_get_cidreq()
1079
            );
1080
        }
1081
1082
        if (!empty($returnUrl)) {
1083
            $back = Display::url(
1084
                Display::getMdiIcon(ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Back')),
1085
                $returnUrl
1086
            );
1087
        }
1088
1089
        if (api_is_platform_admin()) {
1090
            $actionsLeft .= Display::url(
1091
                    Display::getMdiIcon(ToolIcon::SETTINGS, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Settings')),
1092
                    api_get_path(WEB_CODE_PATH) . 'admin/configure_plugin.php?plugin=zoom'
1093
                ) . $back;
1094
        }
1095
1096
        return Display::toolbarAction('toolbar', [$actionsLeft]);
1097
    }
1098
1099
    public function getRecordingSetting()
1100
    {
1101
        $recording = (string) $this->get('enableCloudRecording');
1102
        if (in_array($recording, [self::RECORDING_TYPE_LOCAL, self::RECORDING_TYPE_CLOUD], true)) {
1103
            return $recording;
1104
        }
1105
        return self::RECORDING_TYPE_NONE;
1106
    }
1107
1108
    public function hasRecordingAvailable()
1109
    {
1110
        $recording = $this->getRecordingSetting();
1111
        return self::RECORDING_TYPE_NONE !== $recording;
1112
    }
1113
1114
    /**
1115
     * Updates meeting registrants list. Adds the missing registrants and removes the extra.
1116
     *
1117
     * @param Meeting $meeting
1118
     * @param User[]  $users   list of users to be registered
1119
     *
1120
     * @throws Exception
1121
     */
1122
    private function updateRegistrantList($meeting, $users)
1123
    {
1124
        $usersToAdd = [];
1125
        foreach ($users as $user) {
1126
            $found = false;
1127
            foreach ($meeting->getRegistrants() as $registrant) {
1128
                if ($registrant->getUser() === $user) {
1129
                    $found = true;
1130
                    break;
1131
                }
1132
            }
1133
            if (!$found) {
1134
                $usersToAdd[] = $user;
1135
            }
1136
        }
1137
1138
        $registrantsToRemove = [];
1139
        foreach ($meeting->getRegistrants() as $registrant) {
1140
            $found = false;
1141
            foreach ($users as $user) {
1142
                if ($registrant->getUser() === $user) {
1143
                    $found = true;
1144
                    break;
1145
                }
1146
            }
1147
            if (!$found) {
1148
                $registrantsToRemove[] = $registrant;
1149
            }
1150
        }
1151
1152
        $this->registerUsers($meeting, $usersToAdd);
1153
        $this->unregister($meeting, $registrantsToRemove);
1154
    }
1155
1156
    /**
1157
     * Register users to a meeting.
1158
     *
1159
     * @param Meeting $meeting
1160
     * @param User[]  $users
1161
     *
1162
     * @throws OptimisticLockException
1163
     *
1164
     * @return array failed registrations [ user id => errorMessage ]
1165
     */
1166
    private function registerUsers($meeting, $users)
1167
    {
1168
        $failedUsers = [];
1169
        foreach ($users as $user) {
1170
            try {
1171
                $this->registerUser($meeting, $user, false);
1172
            } catch (Exception $exception) {
1173
                $failedUsers[$user->getId()] = $exception->getMessage();
1174
            }
1175
        }
1176
        Database::getManager()->flush();
1177
1178
        return $failedUsers;
1179
    }
1180
1181
    /**
1182
     * @throws Exception
1183
     * @throws OptimisticLockException
1184
     */
1185
    private function registerUser(Meeting $meeting, User $user, $andFlush = true)
1186
    {
1187
        if (empty($user->getEmail())) {
1188
            throw new Exception($this->get_lang('CannotRegisterWithoutEmailAddress'));
1189
        }
1190
1191
        $meetingRegistrant = MeetingRegistrant::fromEmailAndFirstName(
1192
            $user->getEmail(),
1193
            $user->getFirstname(),
1194
            $user->getLastname()
1195
        );
1196
1197
        $registrantEntity = (new Registrant())
1198
            ->setMeeting($meeting)
1199
            ->setUser($user)
1200
            ->setMeetingRegistrant($meetingRegistrant)
1201
            ->setCreatedRegistration($meeting->getMeetingInfoGet()->addRegistrant($meetingRegistrant));
1202
1203
        Database::getManager()->persist($registrantEntity);
1204
1205
        if ($andFlush) {
1206
            Database::getManager()->flush($registrantEntity);
1207
        }
1208
1209
        return $registrantEntity;
1210
    }
1211
1212
    /**
1213
     * Removes registrants from a meeting.
1214
     *
1215
     * @param Meeting      $meeting
1216
     * @param Registrant[] $registrants
1217
     *
1218
     * @throws Exception
1219
     */
1220
    private function unregister($meeting, $registrants)
1221
    {
1222
        $meetingRegistrants = [];
1223
        foreach ($registrants as $registrant) {
1224
            $meetingRegistrants[] = $registrant->getMeetingRegistrant();
1225
        }
1226
        $meeting->getMeetingInfoGet()->removeRegistrants($meetingRegistrants);
1227
1228
        $em = Database::getManager();
1229
        foreach ($registrants as $registrant) {
1230
            $em->remove($registrant);
1231
        }
1232
        $em->flush();
1233
    }
1234
1235
    /**
1236
     * Starts a new instant meeting and redirects to its start url.
1237
     *
1238
     * @param string       $topic
1239
     * @param User|null    $user
1240
     * @param Course|null  $course
1241
     * @param CGroup|null  $group
1242
     * @param Session|null $session
1243
     *
1244
     * @throws Exception
1245
     */
1246
    private function startInstantMeeting($topic, $user = null, $course = null, $group = null, $session = null)
1247
    {
1248
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_INSTANT);
1249
        $meeting = $this->createMeetingFromMeeting(
1250
            (new Meeting())
1251
                ->setMeetingInfoGet($meetingInfoGet)
1252
                ->setUser($user)
1253
                ->setGroup($group)
1254
                ->setCourse($course)
1255
                ->setSession($session)
1256
        );
1257
        api_location($meeting->getMeetingInfoGet()->start_url);
1258
    }
1259
1260
    /**
1261
     * Creates a meeting on Zoom servers and stores it in the local database.
1262
     *
1263
     * @param Meeting $meeting a new, unsaved meeting with at least a type and a topic
1264
     *
1265
     * @throws Exception
1266
     */
1267
    private function createMeetingFromMeeting($meeting)
1268
    {
1269
        $currentUser = api_get_user_entity(api_get_user_id());
1270
1271
        $meeting->getMeetingInfoGet()->settings->contact_email                 = $currentUser->getEmail();
1272
        $meeting->getMeetingInfoGet()->settings->contact_name                  = $currentUser->getFullName();
1273
        $meeting->getMeetingInfoGet()->settings->auto_recording                = $this->getRecordingSetting();
1274
        $meeting->getMeetingInfoGet()->settings->registrants_email_notification= false;
1275
1276
        // Send create to Zoom.
1277
        $meeting->setMeetingInfoGet($meeting->getMeetingInfoGet()->create());
1278
1279
        Database::getManager()->persist($meeting);
1280
        Database::getManager()->flush();
1281
1282
        return $meeting;
1283
    }
1284
1285
    /**
1286
     * @throws Exception
1287
     */
1288
    private function createGlobalMeeting()
1289
    {
1290
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType(
1291
            $this->get_lang('GlobalMeeting'),
1292
            MeetingInfoGet::TYPE_SCHEDULED
1293
        );
1294
        $meetingInfoGet->start_time = (new DateTime())->format(DateTimeInterface::ISO8601);
1295
        $meetingInfoGet->duration   = 60;
1296
        $meetingInfoGet->settings->approval_type =
1297
            ('true' === $this->get('enableParticipantRegistration'))
1298
                ? MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE
1299
                : MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED;
1300
1301
        $meetingInfoGet->settings->participant_video            = true;
1302
        $meetingInfoGet->settings->join_before_host             = true;
1303
        $meetingInfoGet->settings->registrants_email_notification= false;
1304
1305
        return $this->createMeetingFromMeeting((new Meeting())->setMeetingInfoGet($meetingInfoGet));
1306
    }
1307
1308
    /**
1309
     * Schedules a meeting and returns it.
1310
     *
1311
     * @param DateTime $startTime
1312
     * @param int      $duration
1313
     * @param string   $topic
1314
     * @param string   $agenda
1315
     * @param string   $password
1316
     *
1317
     * @throws Exception
1318
     */
1319
    private function createScheduleMeeting(
1320
        User $user = null,
1321
        Course $course = null,
1322
        CGroup $group = null,
1323
        Session $session = null,
1324
        $startTime,
1325
        $duration,
1326
        $topic,
1327
        $agenda,
1328
        $password
1329
    ) {
1330
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_SCHEDULED);
1331
        $meetingInfoGet->duration = $duration;
1332
        $meetingInfoGet->start_time = $startTime->format(DateTimeInterface::ISO8601);
1333
        $meetingInfoGet->agenda = $agenda;
1334
        $meetingInfoGet->password = $password;
1335
        $meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED;
1336
        if ('true' === $this->get('enableParticipantRegistration')) {
1337
            $meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE;
1338
        }
1339
1340
        return $this->createMeetingFromMeeting(
1341
            (new Meeting())
1342
                ->setMeetingInfoGet($meetingInfoGet)
1343
                ->setUser($user)
1344
                ->setCourse($course)
1345
                ->setGroup($group)
1346
                ->setSession($session)
1347
        );
1348
    }
1349
1350
    /**
1351
     * Registers all the course users to a course meeting.
1352
     *
1353
     * @param Meeting $meeting
1354
     *
1355
     * @throws OptimisticLockException
1356
     */
1357
    private function registerAllCourseUsers($meeting)
1358
    {
1359
        $this->registerUsers($meeting, $meeting->getRegistrableUsers());
1360
    }
1361
}
1362