Passed
Pull Request — master (#7018)
by
unknown
10:03
created

ZoomPlugin::create()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
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