Passed
Pull Request — 1.11.x (#4151)
by Angel Fernando Quiroz
09:24
created

ZoomPlugin::getSignature()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
rs 10
cc 1
nc 1
nop 2
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\CourseBundle\Entity\CGroupInfo;
8
use Chamilo\PluginBundle\Zoom\API\JWTClient;
9
use Chamilo\PluginBundle\Zoom\API\MeetingInfoGet;
10
use Chamilo\PluginBundle\Zoom\API\MeetingRegistrant;
11
use Chamilo\PluginBundle\Zoom\API\MeetingSettings;
12
use Chamilo\PluginBundle\Zoom\API\RecordingFile;
13
use Chamilo\PluginBundle\Zoom\API\RecordingList;
14
use Chamilo\PluginBundle\Zoom\Meeting;
15
use Chamilo\PluginBundle\Zoom\MeetingActivity;
16
use Chamilo\PluginBundle\Zoom\MeetingRepository;
17
use Chamilo\PluginBundle\Zoom\Recording;
18
use Chamilo\PluginBundle\Zoom\RecordingRepository;
19
use Chamilo\PluginBundle\Zoom\Registrant;
20
use Chamilo\PluginBundle\Zoom\RegistrantRepository;
21
use Chamilo\PluginBundle\Zoom\Signature;
22
use Chamilo\UserBundle\Entity\User;
23
use Doctrine\ORM\EntityRepository;
24
use Doctrine\ORM\OptimisticLockException;
25
use Doctrine\ORM\Tools\SchemaTool;
26
use Doctrine\ORM\Tools\ToolsException;
27
28
/**
29
 * Class ZoomPlugin. Integrates Zoom meetings in courses.
30
 */
31
class ZoomPlugin extends Plugin
32
{
33
    const RECORDING_TYPE_CLOUD = 'cloud';
34
    const RECORDING_TYPE_LOCAL = 'local';
35
    const RECORDING_TYPE_NONE = 'none';
36
    public $isCoursePlugin = true;
37
38
    /**
39
     * @var JWTClient
40
     */
41
    private $jwtClient;
42
43
    /**
44
     * ZoomPlugin constructor.
45
     * {@inheritdoc}
46
     * Initializes the API JWT client and the entity repositories.
47
     */
48
    public function __construct()
49
    {
50
        parent::__construct(
51
            '0.4',
52
            'Sébastien Ducoulombier, Julio Montoya, Angel Fernando Quiroz Campos',
53
            [
54
                'tool_enable' => 'boolean',
55
                'apiKey' => 'text',
56
                'apiSecret' => 'text',
57
                'verificationToken' => 'text',
58
                'enableParticipantRegistration' => 'boolean',
59
                'enableCloudRecording' => [
60
                    'type' => 'select',
61
                    'options' => [
62
                        self::RECORDING_TYPE_CLOUD => 'Cloud',
63
                        self::RECORDING_TYPE_LOCAL => 'Local',
64
                        self::RECORDING_TYPE_NONE => get_lang('None'),
65
                    ],
66
                ],
67
                'enableGlobalConference' => 'boolean',
68
                'globalConferenceAllowRoles' => [
69
                    'type' => 'select',
70
                    'options' => [
71
                        PLATFORM_ADMIN => get_lang('Administrator'),
72
                        COURSEMANAGER => get_lang('Teacher'),
73
                        STUDENT => get_lang('Student'),
74
                        STUDENT_BOSS => get_lang('StudentBoss'),
75
                    ],
76
                    'attributes' => ['multiple' => 'multiple'],
77
                ],
78
            ]
79
        );
80
81
        $this->isAdminPlugin = true;
82
        $this->jwtClient = new JWTClient($this->get('apiKey'), $this->get('apiSecret'));
83
    }
84
85
    /**
86
     * Caches and returns an instance of this class.
87
     *
88
     * @return ZoomPlugin the instance to use
89
     */
90
    public static function create()
91
    {
92
        static $instance = null;
93
94
        return $instance ? $instance : $instance = new self();
95
    }
96
97
    /**
98
     * @return bool
99
     */
100
    public static function currentUserCanJoinGlobalMeeting()
101
    {
102
        $user = api_get_user_entity(api_get_user_id());
103
104
        if (null === $user) {
105
            return false;
106
        }
107
108
        //return 'true' === api_get_plugin_setting('zoom', 'enableGlobalConference') && api_user_is_login();
109
        return
110
            'true' === api_get_plugin_setting('zoom', 'enableGlobalConference')
111
            && in_array(
112
                (api_is_platform_admin() ? PLATFORM_ADMIN : $user->getStatus()),
113
                (array) api_get_plugin_setting('zoom', 'globalConferenceAllowRoles')
114
            );
115
    }
116
117
    /**
118
     * @return array
119
     */
120
    public function getProfileBlockItems()
121
    {
122
        $elements = $this->meetingsToWhichCurrentUserIsRegisteredComingSoon();
123
        $addMeetingLink = false;
124
        if (self::currentUserCanJoinGlobalMeeting()) {
125
            $addMeetingLink = true;
126
        }
127
128
        if ($addMeetingLink) {
129
            $elements[$this->get_lang('Meetings')] = api_get_path(WEB_PLUGIN_PATH).'zoom/meetings.php';
130
        }
131
132
        $items = [];
133
        foreach ($elements as $title => $link) {
134
            $items[] = [
135
                'class' => 'video-conference',
136
                'icon' => Display::return_icon(
137
                    'bbb.png',
138
                    get_lang('VideoConference')
139
                ),
140
                'link' => $link,
141
                'title' => $title,
142
            ];
143
        }
144
145
        return $items;
146
    }
147
148
    /**
149
     * @return array [ $title => $link ]
150
     */
151
    public function meetingsToWhichCurrentUserIsRegisteredComingSoon()
152
    {
153
        $linkTemplate = api_get_path(WEB_PLUGIN_PATH).'zoom/join_meeting.php?meetingId=%s';
154
        $user = api_get_user_entity(api_get_user_id());
155
        $meetings = self::getRegistrantRepository()->meetingsComingSoonRegistrationsForUser($user);
156
        $items = [];
157
        foreach ($meetings as $registrant) {
158
            $meeting = $registrant->getMeeting();
159
            $meetingInfoGet = $meeting->getMeetingInfoGet();
160
            $items[sprintf(
161
                $this->get_lang('DateMeetingTitle'),
162
                $meeting->formattedStartTime,
163
                $meetingInfoGet->topic
164
            )] = sprintf($linkTemplate, $meetingInfoGet->id);
165
        }
166
167
        return $items;
168
    }
169
170
    /**
171
     * @return RegistrantRepository|EntityRepository
172
     */
173
    public static function getRegistrantRepository()
174
    {
175
        return Database::getManager()->getRepository(Registrant::class);
176
    }
177
178
    /**
179
     * Creates this plugin's related tables in the internal database.
180
     * Installs course fields in all courses.
181
     *
182
     * @throws ToolsException
183
     */
184
    public function install()
185
    {
186
        $schemaManager = Database::getManager()->getConnection()->getSchemaManager();
187
188
        $tablesExists = $schemaManager->tablesExist(
189
            [
190
                'plugin_zoom_meeting',
191
                'plugin_zoom_meeting_activity',
192
                'plugin_zoom_recording',
193
                'plugin_zoom_registrant',
194
                'plugin_zoom_signature',
195
            ]
196
        );
197
198
        if ($tablesExists) {
199
            return;
200
        }
201
202
        $em = Database::getManager();
203
204
        (new SchemaTool($em))->createSchema(
205
            [
206
                $em->getClassMetadata(Meeting::class),
207
                $em->getClassMetadata(MeetingActivity::class),
208
                $em->getClassMetadata(Recording::class),
209
                $em->getClassMetadata(Registrant::class),
210
                $em->getClassMetadata(Signature::class),
211
            ]
212
        );
213
214
        // Copy icons into the main/img/icons folder
215
        $iconName = 'zoom_meet';
216
        $iconsList = [
217
            '64/'.$iconName.'.png',
218
            '64/'.$iconName.'_na.png',
219
            '32/'.$iconName.'.png',
220
            '32/'.$iconName.'_na.png',
221
            '22/'.$iconName.'.png',
222
            '22/'.$iconName.'_na.png',
223
        ];
224
        $sourceDir = api_get_path(SYS_PLUGIN_PATH).'zoom/resources/img/';
225
        $destinationDir = api_get_path(SYS_CODE_PATH).'img/icons/';
226
        foreach ($iconsList as $icon) {
227
            $src = $sourceDir.$icon;
228
            $dest = $destinationDir.$icon;
229
            copy($src, $dest);
230
        }
231
232
        $this->install_course_fields_in_all_courses(true, 'zoom_meet.png');
233
    }
234
235
    /**
236
     * Drops this plugins' related tables from the internal database.
237
     * Uninstalls course fields in all courses().
238
     */
239
    public function uninstall()
240
    {
241
        $em = Database::getManager();
242
243
        (new SchemaTool($em))->dropSchema(
244
            [
245
                $em->getClassMetadata(Meeting::class),
246
                $em->getClassMetadata(MeetingActivity::class),
247
                $em->getClassMetadata(Recording::class),
248
                $em->getClassMetadata(Registrant::class),
249
                $em->getClassMetadata(Signature::class),
250
            ]
251
        );
252
        $this->uninstall_course_fields_in_all_courses();
253
254
        // Remove icons from the main/img/icons folder
255
        $iconName = 'zoom_meet';
256
        $iconsList = [
257
            '64/'.$iconName.'.png',
258
            '64/'.$iconName.'_na.png',
259
            '32/'.$iconName.'.png',
260
            '32/'.$iconName.'_na.png',
261
            '22/'.$iconName.'.png',
262
            '22/'.$iconName.'_na.png',
263
        ];
264
        $destinationDir = api_get_path(SYS_CODE_PATH).'img/icons/';
265
        foreach ($iconsList as $icon) {
266
            $dest = $destinationDir.$icon;
267
            if (is_file($dest)) {
268
                @unlink($dest);
269
            }
270
        }
271
    }
272
273
    /**
274
     * Generates the search form to include in the meeting list administration page.
275
     * The form has DatePickers 'start' and 'end' and Checkbox 'reloadRecordingLists'.
276
     *
277
     * @return FormValidator the form
278
     */
279
    public function getAdminSearchForm()
280
    {
281
        $form = new FormValidator('search');
282
        $form->addHeader($this->get_lang('SearchMeeting'));
283
        $form->addDatePicker('start', get_lang('StartDate'));
284
        $form->addDatePicker('end', get_lang('EndDate'));
285
        $form->addButtonSearch(get_lang('Search'));
286
        $oneMonth = new DateInterval('P1M');
287
        if ($form->validate()) {
288
            try {
289
                $start = new DateTime($form->getSubmitValue('start'));
290
            } catch (Exception $exception) {
291
                $start = new DateTime();
292
                $start->sub($oneMonth);
293
            }
294
            try {
295
                $end = new DateTime($form->getSubmitValue('end'));
296
            } catch (Exception $exception) {
297
                $end = new DateTime();
298
                $end->add($oneMonth);
299
            }
300
        } else {
301
            $start = new DateTime();
302
            $start->sub($oneMonth);
303
            $end = new DateTime();
304
            $end->add($oneMonth);
305
        }
306
        try {
307
            $form->setDefaults(
308
                [
309
                    'start' => $start->format('Y-m-d'),
310
                    'end' => $end->format('Y-m-d'),
311
                ]
312
            );
313
        } catch (Exception $exception) {
314
            error_log(join(':', [__FILE__, __LINE__, $exception]));
315
        }
316
317
        return $form;
318
    }
319
320
    /**
321
     * Generates a meeting edit form and updates the meeting on validation.
322
     *
323
     * @param Meeting $meeting the meeting
324
     *
325
     * @throws Exception
326
     *
327
     * @return FormValidator
328
     */
329
    public function getEditMeetingForm($meeting)
330
    {
331
        $meetingInfoGet = $meeting->getMeetingInfoGet();
332
        $form = new FormValidator('edit', 'post', $_SERVER['REQUEST_URI']);
333
        $form->addHeader($this->get_lang('UpdateMeeting'));
334
        $form->addText('topic', $this->get_lang('Topic'));
335
        if ($meeting->requiresDateAndDuration()) {
336
            $startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('StartTime'));
337
            $form->setRequired($startTimeDatePicker);
338
            $durationNumeric = $form->addNumeric('duration', $this->get_lang('DurationInMinutes'));
339
            $form->setRequired($durationNumeric);
340
        }
341
        $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]);
342
        //$form->addLabel(get_lang('Password'), $meeting->getMeetingInfoGet()->password);
343
        // $form->addText('password', get_lang('Password'), false, ['maxlength' => '10']);
344
        $form->addCheckBox('sign_attendance', $this->get_lang('SignAttendance'), get_lang('Yes'));
345
        $form->addTextarea('reason_to_sign', $this->get_lang('ReasonToSign'), ['rows' => 5]);
346
        $form->addButtonUpdate(get_lang('Update'));
347
        if ($form->validate()) {
348
            $values = $form->exportValues();
349
350
            if ($meeting->requiresDateAndDuration()) {
351
                $meetingInfoGet->start_time = (new DateTime($values['startTime']))->format(
352
                    DATE_ATOM
353
                );
354
                $meetingInfoGet->timezone = date_default_timezone_get();
355
                $meetingInfoGet->duration = (int) $values['duration'];
356
            }
357
            $meetingInfoGet->topic = $values['topic'];
358
            $meetingInfoGet->agenda = $values['agenda'];
359
            $meeting
360
                ->setSignAttendance(isset($values['sign_attendance']))
361
                ->setReasonToSignAttendance($values['reason_to_sign']);
362
363
            try {
364
                $meetingInfoGet->update();
365
                $meeting->setMeetingInfoGet($meetingInfoGet);
366
                Database::getManager()->persist($meeting);
367
                Database::getManager()->flush();
368
                Display::addFlash(
369
                    Display::return_message($this->get_lang('MeetingUpdated'), 'confirm')
370
                );
371
            } catch (Exception $exception) {
372
                Display::addFlash(
373
                    Display::return_message($exception->getMessage(), 'error')
374
                );
375
            }
376
        }
377
        $defaults = [
378
            'topic' => $meetingInfoGet->topic,
379
            'agenda' => $meetingInfoGet->agenda,
380
        ];
381
        if ($meeting->requiresDateAndDuration()) {
382
            $defaults['startTime'] = $meeting->startDateTime->format('Y-m-d H:i');
383
            $defaults['duration'] = $meetingInfoGet->duration;
384
        }
385
        $defaults['sign_attendance'] = $meeting->isSignAttendance();
386
        $defaults['reason_to_sign'] = $meeting->getReasonToSignAttendance();
387
        $form->setDefaults($defaults);
388
389
        return $form;
390
    }
391
392
    /**
393
     * Generates a meeting delete form and deletes the meeting on validation.
394
     *
395
     * @param Meeting $meeting
396
     * @param string  $returnURL where to redirect to on successful deletion
397
     *
398
     * @throws Exception
399
     *
400
     * @return FormValidator
401
     */
402
    public function getDeleteMeetingForm($meeting, $returnURL)
403
    {
404
        $id = $meeting->getMeetingId();
405
        $form = new FormValidator('delete', 'post', api_get_self().'?meetingId='.$id);
406
        $form->addButtonDelete($this->get_lang('DeleteMeeting'));
407
        if ($form->validate()) {
408
            $this->deleteMeeting($meeting, $returnURL);
409
        }
410
411
        return $form;
412
    }
413
414
    /**
415
     * @param Meeting $meeting
416
     * @param string  $returnURL
417
     *
418
     * @return false
419
     */
420
    public function deleteMeeting($meeting, $returnURL)
421
    {
422
        if (null === $meeting) {
423
            return false;
424
        }
425
426
        $em = Database::getManager();
427
        try {
428
            // No need to delete a instant meeting.
429
            if (\Chamilo\PluginBundle\Zoom\API\Meeting::TYPE_INSTANT != $meeting->getMeetingInfoGet()->type) {
430
                $meeting->getMeetingInfoGet()->delete();
431
            }
432
433
            $em->remove($meeting);
434
            $em->flush();
435
436
            Display::addFlash(
437
                Display::return_message($this->get_lang('MeetingDeleted'), 'confirm')
438
            );
439
            api_location($returnURL);
440
        } catch (Exception $exception) {
441
            $this->handleException($exception);
442
        }
443
    }
444
445
    /**
446
     * @param Exception $exception
447
     */
448
    public function handleException($exception)
449
    {
450
        if ($exception instanceof Exception) {
0 ignored issues
show
introduced by
$exception is always a sub-type of Exception.
Loading history...
451
            $error = json_decode($exception->getMessage());
452
            $message = $exception->getMessage();
453
            if ($error->message) {
454
                $message = $error->message;
455
            }
456
            Display::addFlash(
457
                Display::return_message($message, 'error')
458
            );
459
        }
460
    }
461
462
    /**
463
     * Generates a registrant list update form listing course and session users.
464
     * Updates the list on validation.
465
     *
466
     * @param Meeting $meeting
467
     *
468
     * @throws Exception
469
     *
470
     * @return FormValidator
471
     */
472
    public function getRegisterParticipantForm($meeting)
473
    {
474
        $form = new FormValidator('register', 'post', $_SERVER['REQUEST_URI']);
475
        $userIdSelect = $form->addSelect('userIds', $this->get_lang('RegisteredUsers'));
476
        $userIdSelect->setMultiple(true);
477
        $form->addButtonSend($this->get_lang('UpdateRegisteredUserList'));
478
479
        $users = $meeting->getRegistrableUsers();
480
        foreach ($users as $user) {
481
            $userIdSelect->addOption(
482
                api_get_person_name($user->getFirstname(), $user->getLastname()),
483
                $user->getId()
484
            );
485
        }
486
487
        if ($form->validate()) {
488
            $selectedUserIds = $form->getSubmitValue('userIds');
489
            $selectedUsers = [];
490
            if (!empty($selectedUserIds)) {
491
                foreach ($users as $user) {
492
                    if (in_array($user->getId(), $selectedUserIds)) {
493
                        $selectedUsers[] = $user;
494
                    }
495
                }
496
            }
497
498
            try {
499
                $this->updateRegistrantList($meeting, $selectedUsers);
500
                Display::addFlash(
501
                    Display::return_message($this->get_lang('RegisteredUserListWasUpdated'), 'confirm')
502
                );
503
            } catch (Exception $exception) {
504
                Display::addFlash(
505
                    Display::return_message($exception->getMessage(), 'error')
506
                );
507
            }
508
        }
509
        $registeredUserIds = [];
510
        foreach ($meeting->getRegistrants() as $registrant) {
511
            $registeredUserIds[] = $registrant->getUser()->getId();
512
        }
513
        $userIdSelect->setSelected($registeredUserIds);
514
515
        return $form;
516
    }
517
518
    /**
519
     * Generates a meeting recording files management form.
520
     * Takes action on validation.
521
     *
522
     * @param Meeting $meeting
523
     *
524
     * @throws Exception
525
     *
526
     * @return FormValidator
527
     */
528
    public function getFileForm($meeting, $returnURL)
529
    {
530
        $form = new FormValidator('fileForm', 'post', $_SERVER['REQUEST_URI']);
531
        if (!$meeting->getRecordings()->isEmpty()) {
532
            $fileIdSelect = $form->addSelect('fileIds', get_lang('Files'));
533
            $fileIdSelect->setMultiple(true);
534
            $recordingList = $meeting->getRecordings();
535
            foreach ($recordingList as &$recording) {
536
                // $recording->instanceDetails = $plugin->getPastMeetingInstanceDetails($instance->uuid);
537
                $options = [];
538
                $recordings = $recording->getRecordingMeeting()->recording_files;
539
                foreach ($recordings as $file) {
540
                    $options[] = [
541
                        'text' => sprintf(
542
                            '%s.%s (%s)',
543
                            $file->recording_type,
544
                            $file->file_type,
545
                            $file->file_size
546
                        ),
547
                        'value' => $file->id,
548
                    ];
549
                }
550
                $fileIdSelect->addOptGroup(
551
                    $options,
552
                    sprintf("%s (%s)", $recording->formattedStartTime, $recording->formattedDuration)
553
                );
554
            }
555
            $actions = [];
556
            if ($meeting->isCourseMeeting()) {
557
                $actions['CreateLinkInCourse'] = $this->get_lang('CreateLinkInCourse');
558
                $actions['CopyToCourse'] = $this->get_lang('CopyToCourse');
559
            }
560
            $actions['DeleteFile'] = $this->get_lang('DeleteFile');
561
            $form->addRadio(
562
                'action',
563
                get_lang('Action'),
564
                $actions
565
            );
566
            $form->addButtonUpdate($this->get_lang('DoIt'));
567
            if ($form->validate()) {
568
                $action = $form->getSubmitValue('action');
569
                $idList = $form->getSubmitValue('fileIds');
570
571
                foreach ($recordingList as $recording) {
572
                    $recordings = $recording->getRecordingMeeting()->recording_files;
573
574
                    foreach ($recordings as $file) {
575
                        if (in_array($file->id, $idList)) {
576
                            $name = sprintf(
577
                                $this->get_lang('XRecordingOfMeetingXFromXDurationXDotX'),
578
                                $file->recording_type,
579
                                $meeting->getId(),
580
                                $recording->formattedStartTime,
581
                                $recording->formattedDuration,
582
                                $file->file_type
583
                            );
584
                            if ('CreateLinkInCourse' === $action && $meeting->isCourseMeeting()) {
585
                                try {
586
                                    $this->createLinkToFileInCourse($meeting, $file, $name);
587
                                    Display::addFlash(
588
                                        Display::return_message(
589
                                            $this->get_lang('LinkToFileWasCreatedInCourse'),
590
                                            'success'
591
                                        )
592
                                    );
593
                                } catch (Exception $exception) {
594
                                    Display::addFlash(
595
                                        Display::return_message($exception->getMessage(), 'error')
596
                                    );
597
                                }
598
                            } elseif ('CopyToCourse' === $action && $meeting->isCourseMeeting()) {
599
                                try {
600
                                    $this->copyFileToCourse($meeting, $file, $name);
601
                                    Display::addFlash(
602
                                        Display::return_message($this->get_lang('FileWasCopiedToCourse'), 'confirm')
603
                                    );
604
                                } catch (Exception $exception) {
605
                                    Display::addFlash(
606
                                        Display::return_message($exception->getMessage(), 'error')
607
                                    );
608
                                }
609
                            } elseif ('DeleteFile' === $action) {
610
                                try {
611
                                    $name = $file->recording_type;
612
                                    $file->delete();
613
                                    Display::addFlash(
614
                                        Display::return_message($this->get_lang('FileWasDeleted').': '.$name, 'confirm')
615
                                    );
616
                                } catch (Exception $exception) {
617
                                    Display::addFlash(
618
                                        Display::return_message($exception->getMessage(), 'error')
619
                                    );
620
                                }
621
                            }
622
                        }
623
                    }
624
                }
625
                api_location($returnURL);
626
            }
627
        }
628
629
        return $form;
630
    }
631
632
    /**
633
     * Adds to the meeting course documents a link to a meeting instance recording file.
634
     *
635
     * @param Meeting       $meeting
636
     * @param RecordingFile $file
637
     * @param string        $name
638
     *
639
     * @throws Exception
640
     */
641
    public function createLinkToFileInCourse($meeting, $file, $name)
642
    {
643
        $course = $meeting->getCourse();
644
        if (null === $course) {
645
            throw new Exception('This meeting is not linked to a course');
646
        }
647
        $courseInfo = api_get_course_info_by_id($course->getId());
648
        if (empty($courseInfo)) {
649
            throw new Exception('This meeting is not linked to a valid course');
650
        }
651
        $path = '/zoom_meeting_recording_file_'.$file->id.'.'.$file->file_type;
652
        $docId = DocumentManager::addCloudLink($courseInfo, $path, $file->play_url, $name);
653
        if (!$docId) {
654
            throw new Exception(get_lang(DocumentManager::cloudLinkExists($courseInfo, $path, $file->play_url) ? 'UrlAlreadyExists' : 'ErrorAddCloudLink'));
655
        }
656
    }
657
658
    /**
659
     * Copies a recording file to a meeting's course.
660
     *
661
     * @param Meeting       $meeting
662
     * @param RecordingFile $file
663
     * @param string        $name
664
     *
665
     * @throws Exception
666
     */
667
    public function copyFileToCourse($meeting, $file, $name)
668
    {
669
        $course = $meeting->getCourse();
670
        if (null === $course) {
671
            throw new Exception('This meeting is not linked to a course');
672
        }
673
        $courseInfo = api_get_course_info_by_id($course->getId());
674
        if (empty($courseInfo)) {
675
            throw new Exception('This meeting is not linked to a valid course');
676
        }
677
        $tmpFile = tmpfile();
678
        if (false === $tmpFile) {
679
            throw new Exception('tmpfile() returned false');
680
        }
681
        $curl = curl_init($file->getFullDownloadURL($this->jwtClient->token));
682
        if (false === $curl) {
683
            throw new Exception('Could not init curl: '.curl_error($curl));
684
        }
685
        if (!curl_setopt_array(
686
            $curl,
687
            [
688
                CURLOPT_FILE => $tmpFile,
689
                CURLOPT_FOLLOWLOCATION => true,
690
                CURLOPT_MAXREDIRS => 10,
691
                CURLOPT_TIMEOUT => 120,
692
            ]
693
        )) {
694
            throw new Exception("Could not set curl options: ".curl_error($curl));
695
        }
696
        if (false === curl_exec($curl)) {
697
            throw new Exception("curl_exec failed: ".curl_error($curl));
698
        }
699
700
        $sessionId = 0;
701
        $session = $meeting->getSession();
702
        if (null !== $session) {
703
            $sessionId = $session->getId();
704
        }
705
706
        $groupId = 0;
707
        $group = $meeting->getGroup();
708
        if (null !== $group) {
709
            $groupId = $group->getIid();
710
        }
711
712
        $newPath = handle_uploaded_document(
713
            $courseInfo,
714
            [
715
                'name' => $name,
716
                'tmp_name' => stream_get_meta_data($tmpFile)['uri'],
717
                'size' => filesize(stream_get_meta_data($tmpFile)['uri']),
718
                'from_file' => true,
719
                'move_file' => true,
720
                'type' => $file->file_type,
721
            ],
722
            api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document',
723
            '/',
724
            api_get_user_id(),
725
            $groupId,
726
            null,
727
            0,
728
            'overwrite',
729
            true,
730
            false,
731
            null,
732
            $sessionId,
733
            true
734
        );
735
736
        fclose($tmpFile);
737
        if (false === $newPath) {
738
            throw new Exception('Could not handle uploaded document');
739
        }
740
    }
741
742
    /**
743
     * Generates a form to fast and easily create and start an instant meeting.
744
     * On validation, create it then redirect to it and exit.
745
     *
746
     * @return FormValidator
747
     */
748
    public function getCreateInstantMeetingForm(
749
        User $user,
750
        Course $course,
751
        CGroupInfo $group = null,
752
        Session $session = null
753
    ) {
754
        $extraUrl = '';
755
        if (!empty($course)) {
756
            $extraUrl = api_get_cidreq();
757
        }
758
        $form = new FormValidator('createInstantMeetingForm', 'post', api_get_self().'?'.$extraUrl, '_blank');
759
        $form->addButton('startButton', $this->get_lang('StartInstantMeeting'), 'video-camera', 'primary');
760
        if ($form->validate()) {
761
            try {
762
                $this->startInstantMeeting($this->get_lang('InstantMeeting'), $user, $course, $group, $session);
763
            } catch (Exception $exception) {
764
                Display::addFlash(
765
                    Display::return_message($exception->getMessage(), 'error')
766
                );
767
            }
768
        }
769
770
        return $form;
771
    }
772
773
    /**
774
     * Generates a form to schedule a meeting.
775
     * On validation, creates it and redirects to its page.
776
     *
777
     * @throws Exception
778
     *
779
     * @return FormValidator
780
     */
781
    public function getScheduleMeetingForm(User $user, Course $course = null, CGroupInfo $group = null, Session $session = null)
782
    {
783
        $extraUrl = '';
784
        if (!empty($course)) {
785
            $extraUrl = api_get_cidreq();
786
        }
787
        $form = new FormValidator('scheduleMeetingForm', 'post', api_get_self().'?'.$extraUrl);
788
        $form->addHeader($this->get_lang('ScheduleAMeeting'));
789
        $startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('StartTime'));
790
        $form->setRequired($startTimeDatePicker);
791
792
        $form->addText('topic', $this->get_lang('Topic'), true);
793
        $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]);
794
795
        $durationNumeric = $form->addNumeric('duration', $this->get_lang('DurationInMinutes'));
796
        $form->setRequired($durationNumeric);
797
798
        if (null === $course && 'true' === $this->get('enableGlobalConference')) {
799
            $options = [];
800
            $options['everyone'] = $this->get_lang('ForEveryone');
801
            $options['registered_users'] = $this->get_lang('SomeUsers');
802
            if (!empty($options)) {
803
                if (1 === count($options)) {
804
                    $form->addHidden('type', key($options));
805
                } else {
806
                    $form->addSelect('type', $this->get_lang('ConferenceType'), $options);
807
                }
808
            }
809
        } else {
810
            // To course
811
            $form->addHidden('type', 'course');
812
        }
813
814
        /*
815
       // $passwordText = $form->addText('password', get_lang('Password'), false, ['maxlength' => '10']);
816
       if (null !== $course) {
817
           $registrationOptions = [
818
               'RegisterAllCourseUsers' => $this->get_lang('RegisterAllCourseUsers'),
819
           ];
820
           $groups = GroupManager::get_groups();
821
           if (!empty($groups)) {
822
               $registrationOptions['RegisterTheseGroupMembers'] = get_lang('RegisterTheseGroupMembers');
823
           }
824
           $registrationOptions['RegisterNoUser'] = $this->get_lang('RegisterNoUser');
825
           $userRegistrationRadio = $form->addRadio(
826
               'userRegistration',
827
               $this->get_lang('UserRegistration'),
828
               $registrationOptions
829
           );
830
           $groupOptions = [];
831
           foreach ($groups as $group) {
832
               $groupOptions[$group['id']] = $group['name'];
833
           }
834
           $groupIdsSelect = $form->addSelect(
835
               'groupIds',
836
               $this->get_lang('RegisterTheseGroupMembers'),
837
               $groupOptions
838
           );
839
           $groupIdsSelect->setMultiple(true);
840
           if (!empty($groups)) {
841
               $jsCode = sprintf(
842
                   "getElementById('%s').parentNode.parentNode.parentNode.style.display = getElementById('%s').checked ? 'block' : 'none'",
843
                   $groupIdsSelect->getAttribute('id'),
844
                   $userRegistrationRadio->getelements()[1]->getAttribute('id')
845
               );
846
847
               $form->setAttribute('onchange', $jsCode);
848
           }
849
       }*/
850
851
        $form->addCheckBox('sign_attendance', $this->get_lang('SignAttendance'), get_lang('Yes'));
852
        $form->addTextarea('reason_to_sign', $this->get_lang('ReasonToSign'), ['rows' => 5]);
853
854
        $form->addButtonCreate(get_lang('Save'));
855
856
        if ($form->validate()) {
857
            $formValues = $form->exportValues();
858
            $type = $form->getSubmitValue('type');
859
860
            switch ($type) {
861
                case 'everyone':
862
                    $user = null;
863
                    $group = null;
864
                    $course = null;
865
                    $session = null;
866
867
                    break;
868
                case 'registered_users':
869
                    //$user = null;
870
                    $course = null;
871
                    $session = null;
872
873
                    break;
874
                case 'course':
875
                    $user = null;
876
                    //$course = null;
877
                    //$session = null;
878
879
                    break;
880
            }
881
882
            try {
883
                $newMeeting = $this->createScheduleMeeting(
884
                    $user,
885
                    $course,
886
                    $group,
887
                    $session,
888
                    new DateTime($form->getSubmitValue('startTime')),
889
                    $form->getSubmitValue('duration'),
890
                    $form->getSubmitValue('topic'),
891
                    $form->getSubmitValue('agenda'),
892
                    substr(uniqid('z', true), 0, 10),
893
                    isset($formValues['sign_attendance']),
894
                    $formValues['reason_to_sign']
895
                );
896
897
                Display::addFlash(
898
                    Display::return_message($this->get_lang('NewMeetingCreated'))
899
                );
900
901
                if ($newMeeting->isCourseMeeting()) {
902
                    if ('RegisterAllCourseUsers' === $form->getSubmitValue('userRegistration')) {
903
                        $this->registerAllCourseUsers($newMeeting);
904
                        Display::addFlash(
905
                            Display::return_message($this->get_lang('AllCourseUsersWereRegistered'))
906
                        );
907
                    } elseif ('RegisterTheseGroupMembers' === $form->getSubmitValue('userRegistration')) {
908
                        $userIds = [];
909
                        foreach ($form->getSubmitValue('groupIds') as $groupId) {
910
                            $userIds = array_unique(array_merge($userIds, GroupManager::get_users($groupId)));
911
                        }
912
                        $users = Database::getManager()->getRepository('ChamiloUserBundle:User')->findBy(
913
                            ['id' => $userIds]
914
                        );
915
                        $this->registerUsers($newMeeting, $users);
916
                        Display::addFlash(
917
                            Display::return_message($this->get_lang('GroupUsersWereRegistered'))
918
                        );
919
                    }
920
                }
921
                api_location('meeting.php?meetingId='.$newMeeting->getMeetingId().'&'.$extraUrl);
922
            } catch (Exception $exception) {
923
                Display::addFlash(
924
                    Display::return_message($exception->getMessage(), 'error')
925
                );
926
            }
927
        } else {
928
            $form->setDefaults(
929
                [
930
                    'duration' => 60,
931
                    'userRegistration' => 'RegisterAllCourseUsers',
932
                ]
933
            );
934
        }
935
936
        return $form;
937
    }
938
939
    /**
940
     * Return the current global meeting (create it if needed).
941
     *
942
     * @throws Exception
943
     *
944
     * @return string
945
     */
946
    public function getGlobalMeeting()
947
    {
948
        foreach ($this->getMeetingRepository()->unfinishedGlobalMeetings() as $meeting) {
949
            return $meeting;
950
        }
951
952
        return $this->createGlobalMeeting();
953
    }
954
955
    /**
956
     * @return MeetingRepository|EntityRepository
957
     */
958
    public static function getMeetingRepository()
959
    {
960
        return Database::getManager()->getRepository(Meeting::class);
961
    }
962
963
    /**
964
     * Returns the URL to enter (start or join) a meeting or null if not possible to enter the meeting,
965
     * The returned URL depends on the meeting current status (waiting, started or finished) and the current user.
966
     *
967
     * @param Meeting $meeting
968
     *
969
     * @throws OptimisticLockException
970
     * @throws Exception
971
     *
972
     * @return string|null
973
     */
974
    public function getStartOrJoinMeetingURL($meeting)
975
    {
976
        $status = $meeting->getMeetingInfoGet()->status;
977
        $userId = api_get_user_id();
978
        $currentUser = api_get_user_entity($userId);
979
        $isGlobal = 'true' === $this->get('enableGlobalConference') && $meeting->isGlobalMeeting();
980
981
        switch ($status) {
982
            case 'ended':
983
                if ($this->userIsConferenceManager($meeting)) {
984
                    return $meeting->getMeetingInfoGet()->start_url;
985
                }
986
                break;
987
            case 'waiting':
988
                // Zoom does not allow for a new meeting to be started on first participant join.
989
                // It requires the host to start the meeting first.
990
                // Therefore for global meetings we must make the first participant the host
991
                // that is use start_url rather than join_url.
992
                // the participant will not be registered and will appear as the Zoom user account owner.
993
                // For course and user meetings, only the host can start the meeting.
994
                if ($this->userIsConferenceManager($meeting)) {
995
                    return $meeting->getMeetingInfoGet()->start_url;
996
                }
997
998
                break;
999
            case 'started':
1000
                // User per conference.
1001
                if ($currentUser === $meeting->getUser()) {
1002
                    return $meeting->getMeetingInfoGet()->join_url;
1003
                }
1004
1005
                // The participant is not registered, he can join only the global meeting (automatic registration).
1006
                if ($isGlobal) {
1007
                    return $this->registerUser($meeting, $currentUser)->getCreatedRegistration()->join_url;
1008
                }
1009
1010
                if ($meeting->isCourseMeeting()) {
1011
                    if ($this->userIsCourseConferenceManager()) {
1012
                        return $meeting->getMeetingInfoGet()->start_url;
1013
                    }
1014
1015
                    $sessionId = api_get_session_id();
1016
                    $courseCode = api_get_course_id();
1017
1018
                    if (empty($sessionId)) {
1019
                        $isSubscribed = CourseManager::is_user_subscribed_in_course(
1020
                            $userId,
1021
                            $courseCode,
1022
                            false
1023
                        );
1024
                    } else {
1025
                        $isSubscribed = CourseManager::is_user_subscribed_in_course(
1026
                            $userId,
1027
                            $courseCode,
1028
                            true,
1029
                            $sessionId
1030
                        );
1031
                    }
1032
1033
                    if ($isSubscribed) {
1034
                        if ($meeting->isCourseGroupMeeting()) {
1035
                            $groupInfo = GroupManager::get_group_properties($meeting->getGroup()->getIid(), true);
1036
                            $isInGroup = GroupManager::is_user_in_group($userId, $groupInfo);
1037
                            if (false === $isInGroup) {
1038
                                throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting'));
1039
                            }
1040
                        }
1041
1042
                        if (\Chamilo\PluginBundle\Zoom\API\Meeting::TYPE_INSTANT == $meeting->getMeetingInfoGet()->type) {
1043
                            return $meeting->getMeetingInfoGet()->join_url;
1044
                        }
1045
1046
                        return $this->registerUser($meeting, $currentUser)->getCreatedRegistration()->join_url;
1047
                    }
1048
1049
                    throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting'));
1050
                }
1051
1052
                //if ('true' === $this->get('enableParticipantRegistration')) {
1053
                    //if ('true' === $this->get('enableParticipantRegistration') && $meeting->requiresRegistration()) {
1054
                    // the participant must be registered
1055
                    $registrant = $meeting->getRegistrant($currentUser);
1056
                    if (null == $registrant) {
1057
                        throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting'));
1058
                    }
1059
1060
                    // the participant is registered
1061
                    return $registrant->getCreatedRegistration()->join_url;
1062
                //}
1063
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
1064
        }
1065
1066
        return null;
1067
    }
1068
1069
    /**
1070
     * @param Meeting $meeting
1071
     *
1072
     * @return bool whether the logged-in user can manage conferences in this context, that is either
1073
     *              the current course or session coach, the platform admin or the current course admin
1074
     */
1075
    public function userIsConferenceManager($meeting)
1076
    {
1077
        if (null === $meeting) {
1078
            return false;
1079
        }
1080
1081
        if (api_is_coach() || api_is_platform_admin()) {
1082
            return true;
1083
        }
1084
1085
        if ($meeting->isCourseMeeting() && api_get_course_id() && api_is_course_admin()) {
1086
            return true;
1087
        }
1088
1089
        return $meeting->isUserMeeting() && $meeting->getUser()->getId() == api_get_user_id();
1090
    }
1091
1092
    /**
1093
     * @return bool whether the logged-in user can manage conferences in this context, that is either
1094
     *              the current course or session coach, the platform admin or the current course admin
1095
     */
1096
    public function userIsCourseConferenceManager()
1097
    {
1098
        if (api_is_coach() || api_is_platform_admin()) {
1099
            return true;
1100
        }
1101
1102
        if (api_get_course_id() && api_is_course_admin()) {
1103
            return true;
1104
        }
1105
1106
        return false;
1107
    }
1108
1109
    /**
1110
     * Update local recording list from remote Zoom server's version.
1111
     * Kept to implement a future administration button ("import existing data from zoom server").
1112
     *
1113
     * @param DateTime $startDate
1114
     * @param DateTime $endDate
1115
     *
1116
     * @throws OptimisticLockException
1117
     * @throws Exception
1118
     */
1119
    public function reloadPeriodRecordings($startDate, $endDate)
1120
    {
1121
        $em = Database::getManager();
1122
        $recordingRepo = $this->getRecordingRepository();
1123
        $meetingRepo = $this->getMeetingRepository();
1124
        $recordings = RecordingList::loadPeriodRecordings($startDate, $endDate);
1125
1126
        foreach ($recordings as $recordingMeeting) {
1127
            $recordingEntity = $recordingRepo->findOneBy(['uuid' => $recordingMeeting->uuid]);
1128
            if (null === $recordingEntity) {
1129
                $recordingEntity = new Recording();
1130
                $meeting = $meetingRepo->findOneBy(['meetingId' => $recordingMeeting->id]);
1131
                if (null === $meeting) {
1132
                    try {
1133
                        $meetingInfoGet = MeetingInfoGet::fromId($recordingMeeting->id);
1134
                    } catch (Exception $exception) {
1135
                        $meetingInfoGet = null; // deleted meeting with recordings
1136
                    }
1137
                    if (null !== $meetingInfoGet) {
1138
                        $meeting = $this->createMeetingFromMeeting(
1139
                            (new Meeting())->setMeetingInfoGet($meetingInfoGet)
1140
                        );
1141
                        $em->persist($meeting);
1142
                    }
1143
                }
1144
                if (null !== $meeting) {
1145
                    $recordingEntity->setMeeting($meeting);
1146
                }
1147
            }
1148
            $recordingEntity->setRecordingMeeting($recordingMeeting);
1149
            $em->persist($recordingEntity);
1150
        }
1151
        $em->flush();
1152
    }
1153
1154
    /**
1155
     * @return RecordingRepository|EntityRepository
1156
     */
1157
    public static function getRecordingRepository()
1158
    {
1159
        return Database::getManager()->getRepository(Recording::class);
1160
    }
1161
1162
    public function getToolbar($returnUrl = '')
1163
    {
1164
        if (!api_is_platform_admin()) {
1165
            return '';
1166
        }
1167
1168
        $actionsLeft = '';
1169
        $back = '';
1170
        $courseId = api_get_course_id();
1171
        if (empty($courseId)) {
1172
            $actionsLeft .=
1173
                Display::url(
1174
                    Display::return_icon('bbb.png', $this->get_lang('Meetings'), null, ICON_SIZE_MEDIUM),
1175
                    api_get_path(WEB_PLUGIN_PATH).'zoom/meetings.php'
1176
                );
1177
        } else {
1178
            $actionsLeft .=
1179
                Display::url(
1180
                    Display::return_icon('bbb.png', $this->get_lang('Meetings'), null, ICON_SIZE_MEDIUM),
1181
                    api_get_path(WEB_PLUGIN_PATH).'zoom/start.php?'.api_get_cidreq()
1182
                );
1183
        }
1184
1185
        if (!empty($returnUrl)) {
1186
            $back = Display::url(
1187
                Display::return_icon('back.png', get_lang('Back'), null, ICON_SIZE_MEDIUM),
1188
                $returnUrl
1189
            );
1190
        }
1191
1192
        if (api_is_platform_admin()) {
1193
            $actionsLeft .=
1194
                Display::url(
1195
                    Display::return_icon('settings.png', get_lang('Settings'), null, ICON_SIZE_MEDIUM),
1196
                    api_get_path(WEB_CODE_PATH).'admin/configure_plugin.php?name=zoom'
1197
                ).$back;
1198
        }
1199
1200
        return Display::toolbarAction('toolbar', [$actionsLeft]);
1201
    }
1202
1203
    public function getRecordingSetting()
1204
    {
1205
        $recording = (string) $this->get('enableCloudRecording');
1206
1207
        if (in_array($recording, [self::RECORDING_TYPE_LOCAL, self::RECORDING_TYPE_CLOUD], true)) {
1208
            return $recording;
1209
        }
1210
1211
        return self::RECORDING_TYPE_NONE;
1212
    }
1213
1214
    public function hasRecordingAvailable()
1215
    {
1216
        $recording = $this->getRecordingSetting();
1217
1218
        return self::RECORDING_TYPE_NONE !== $recording;
1219
    }
1220
1221
    /**
1222
     * @throws OptimisticLockException
1223
     * @throws \Doctrine\ORM\ORMException
1224
     */
1225
    public function saveSignature(Registrant $registrant, string $file): bool
1226
    {
1227
        if (empty($file)) {
1228
            return false;
1229
        }
1230
1231
        $signature = $registrant->getSignature();
1232
1233
        if (null !== $signature) {
1234
            return false;
1235
        }
1236
1237
        $signature = new Signature();
1238
        $signature
1239
            ->setFile($file)
1240
            ->setRegisteredAt(api_get_utc_datetime(null, false, true))
1241
        ;
1242
1243
        $registrant->setSignature($signature);
1244
1245
        $em = Database::getManager();
1246
        $em->persist($signature);
1247
        $em->flush();
1248
1249
        return true;
1250
    }
1251
1252
    public function getSignature(int $userId, Meeting $meeting): ?Signature
1253
    {
1254
        $signatureRepo = Database::getManager()
1255
            ->getRepository(Signature::class)
1256
        ;
1257
1258
        return $signatureRepo->findOneBy(['user' => $userId, 'meeting' => $meeting]);
1259
    }
1260
1261
    public function exportSignatures(Meeting $meeting, $formatToExport)
1262
    {
1263
        $signatures = array_map(
1264
            function (Registrant $registrant) use ($formatToExport) {
1265
                $signature = $registrant->getSignature();
1266
1267
                $item = [
1268
                    $registrant->getUser()->getLastname(),
1269
                    $registrant->getUser()->getFirstname(),
1270
                    $signature
1271
                        ? api_convert_and_format_date($signature->getRegisteredAt(), DATE_TIME_FORMAT_LONG)
1272
                        : '-',
1273
                ];
1274
1275
                if ('pdf' === $formatToExport) {
1276
                    $item[] = $signature
1277
                        ? Display::img($signature->getFile(), '', ['style' => 'width: 150px;'], false)
1278
                        : '-';
1279
                }
1280
1281
                return $item;
1282
            },
1283
            $meeting->getRegistrants()->toArray()
1284
        );
1285
1286
        $data = array_merge(
1287
            [
1288
                [
1289
                    get_lang('LastName'),
1290
                    get_lang('FirstName'),
1291
                    get_lang('DateTime'),
1292
                    'pdf' === $formatToExport ? get_lang('File') : null,
1293
                ],
1294
            ],
1295
            $signatures
1296
        );
1297
1298
        if ('pdf' === $formatToExport) {
1299
            $params = [
1300
                'filename' => get_lang('Attendance'),
1301
                'pdf_title' => get_lang('Attendance'),
1302
                'pdf_description' => $meeting->getIntroduction(),
1303
                'show_teacher_as_myself' => false,
1304
            ];
1305
1306
            Export::export_table_pdf($data, $params);
1307
        }
1308
1309
        if ('xls' === $formatToExport) {
1310
            $introduction = array_map(
1311
                function ($line) {
1312
                    return [
1313
                        strip_tags(trim($line)),
1314
                    ];
1315
                },
1316
                explode(PHP_EOL, $meeting->getIntroduction())
1317
            );
1318
1319
            Export::arrayToXls(
1320
                array_merge($introduction, $data),
1321
                get_lang('Attendance')
1322
            );
1323
        }
1324
    }
1325
1326
    /**
1327
     * Updates meeting registrants list. Adds the missing registrants and removes the extra.
1328
     *
1329
     * @param Meeting $meeting
1330
     * @param User[]  $users   list of users to be registered
1331
     *
1332
     * @throws Exception
1333
     */
1334
    private function updateRegistrantList($meeting, $users)
1335
    {
1336
        $usersToAdd = [];
1337
        foreach ($users as $user) {
1338
            $found = false;
1339
            foreach ($meeting->getRegistrants() as $registrant) {
1340
                if ($registrant->getUser() === $user) {
1341
                    $found = true;
1342
                    break;
1343
                }
1344
            }
1345
            if (!$found) {
1346
                $usersToAdd[] = $user;
1347
            }
1348
        }
1349
        $registrantsToRemove = [];
1350
        foreach ($meeting->getRegistrants() as $registrant) {
1351
            $found = false;
1352
            foreach ($users as $user) {
1353
                if ($registrant->getUser() === $user) {
1354
                    $found = true;
1355
                    break;
1356
                }
1357
            }
1358
            if (!$found) {
1359
                $registrantsToRemove[] = $registrant;
1360
            }
1361
        }
1362
        $this->registerUsers($meeting, $usersToAdd);
1363
        $this->unregister($meeting, $registrantsToRemove);
1364
    }
1365
1366
    /**
1367
     * Register users to a meeting.
1368
     *
1369
     * @param Meeting $meeting
1370
     * @param User[]  $users
1371
     *
1372
     * @throws OptimisticLockException
1373
     *
1374
     * @return User[] failed registrations [ user id => errorMessage ]
1375
     */
1376
    private function registerUsers($meeting, $users)
1377
    {
1378
        $failedUsers = [];
1379
        foreach ($users as $user) {
1380
            try {
1381
                $this->registerUser($meeting, $user, false);
1382
            } catch (Exception $exception) {
1383
                $failedUsers[$user->getId()] = $exception->getMessage();
1384
            }
1385
        }
1386
        Database::getManager()->flush();
1387
1388
        return $failedUsers;
1389
    }
1390
1391
    /**
1392
     * @throws Exception
1393
     * @throws OptimisticLockException
1394
     *
1395
     * @return Registrant
1396
     */
1397
    private function registerUser(Meeting $meeting, User $user, $andFlush = true)
1398
    {
1399
        if (empty($user->getEmail())) {
1400
            throw new Exception($this->get_lang('CannotRegisterWithoutEmailAddress'));
1401
        }
1402
1403
        $meetingRegistrant = MeetingRegistrant::fromEmailAndFirstName(
1404
            $user->getEmail(),
1405
            $user->getFirstname(),
1406
            $user->getLastname()
1407
        );
1408
1409
        $registrantEntity = (new Registrant())
1410
            ->setMeeting($meeting)
1411
            ->setUser($user)
1412
            ->setMeetingRegistrant($meetingRegistrant)
1413
            ->setCreatedRegistration($meeting->getMeetingInfoGet()->addRegistrant($meetingRegistrant));
1414
        Database::getManager()->persist($registrantEntity);
1415
1416
        if ($andFlush) {
1417
            Database::getManager()->flush($registrantEntity);
1418
        }
1419
1420
        return $registrantEntity;
1421
    }
1422
1423
    /**
1424
     * Removes registrants from a meeting.
1425
     *
1426
     * @param Meeting      $meeting
1427
     * @param Registrant[] $registrants
1428
     *
1429
     * @throws Exception
1430
     */
1431
    private function unregister($meeting, $registrants)
1432
    {
1433
        $meetingRegistrants = [];
1434
        foreach ($registrants as $registrant) {
1435
            $meetingRegistrants[] = $registrant->getMeetingRegistrant();
1436
        }
1437
        $meeting->getMeetingInfoGet()->removeRegistrants($meetingRegistrants);
1438
        $em = Database::getManager();
1439
        foreach ($registrants as $registrant) {
1440
            $em->remove($registrant);
1441
        }
1442
        $em->flush();
1443
    }
1444
1445
    /**
1446
     * Starts a new instant meeting and redirects to its start url.
1447
     *
1448
     * @param string          $topic
1449
     * @param User|null       $user
1450
     * @param Course|null     $course
1451
     * @param CGroupInfo|null $group
1452
     * @param Session|null    $session
1453
     *
1454
     * @throws Exception
1455
     */
1456
    private function startInstantMeeting($topic, $user = null, $course = null, $group = null, $session = null)
1457
    {
1458
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_INSTANT);
1459
        //$meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE;
1460
        $meeting = $this->createMeetingFromMeeting(
1461
            (new Meeting())
1462
                ->setMeetingInfoGet($meetingInfoGet)
1463
                ->setUser($user)
1464
                ->setGroup($group)
1465
                ->setCourse($course)
1466
                ->setSession($session)
1467
        );
1468
        api_location($meeting->getMeetingInfoGet()->start_url);
1469
    }
1470
1471
    /**
1472
     * Creates a meeting on Zoom servers and stores it in the local database.
1473
     *
1474
     * @param Meeting $meeting a new, unsaved meeting with at least a type and a topic
1475
     *
1476
     * @throws Exception
1477
     *
1478
     * @return Meeting
1479
     */
1480
    private function createMeetingFromMeeting($meeting)
1481
    {
1482
        $currentUser = api_get_user_entity(api_get_user_id());
1483
1484
        $meeting->getMeetingInfoGet()->settings->contact_email = $currentUser->getEmail();
1485
        $meeting->getMeetingInfoGet()->settings->contact_name = $currentUser->getFullname();
1486
        $meeting->getMeetingInfoGet()->settings->auto_recording = $this->getRecordingSetting();
1487
        $meeting->getMeetingInfoGet()->settings->registrants_email_notification = false;
1488
1489
        //$meeting->getMeetingInfoGet()->host_email = $currentUser->getEmail();
1490
        //$meeting->getMeetingInfoGet()->settings->alternative_hosts = $currentUser->getEmail();
1491
1492
        // Send create to Zoom.
1493
        $meeting->setMeetingInfoGet($meeting->getMeetingInfoGet()->create());
1494
1495
        Database::getManager()->persist($meeting);
1496
        Database::getManager()->flush();
1497
1498
        return $meeting;
1499
    }
1500
1501
    /**
1502
     * @throws Exception
1503
     *
1504
     * @return Meeting
1505
     */
1506
    private function createGlobalMeeting()
1507
    {
1508
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType(
1509
            $this->get_lang('GlobalMeeting'),
1510
            MeetingInfoGet::TYPE_SCHEDULED
1511
        );
1512
        $meetingInfoGet->start_time = (new DateTime())->format(DATE_ATOM);
1513
        $meetingInfoGet->duration = 60;
1514
        $meetingInfoGet->settings->approval_type =
1515
            ('true' === $this->get('enableParticipantRegistration'))
1516
                ? MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE
1517
                : MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED;
1518
        // $meetingInfoGet->settings->host_video = true;
1519
        $meetingInfoGet->settings->participant_video = true;
1520
        $meetingInfoGet->settings->join_before_host = true;
1521
        $meetingInfoGet->settings->registrants_email_notification = false;
1522
1523
        return $this->createMeetingFromMeeting((new Meeting())->setMeetingInfoGet($meetingInfoGet));
1524
    }
1525
1526
    /**
1527
     * Schedules a meeting and returns it.
1528
     * set $course, $session and $user to null in order to create a global meeting.
1529
     *
1530
     * @param DateTime $startTime meeting local start date-time (configure local timezone on your Zoom account)
1531
     * @param int      $duration  in minutes
1532
     * @param string   $topic     short title of the meeting, required
1533
     * @param string   $agenda    ordre du jour
1534
     * @param string   $password  meeting password
1535
     *
1536
     * @throws Exception
1537
     *
1538
     * @return Meeting meeting
1539
     */
1540
    private function createScheduleMeeting(
1541
        User $user = null,
1542
        Course $course = null,
1543
        CGroupInfo $group = null,
1544
        Session $session = null,
1545
        $startTime,
1546
        $duration,
1547
        $topic,
1548
        $agenda,
1549
        $password,
1550
        bool $signAttendance = false,
1551
        string $reasonToSignAttendance = ''
1552
    ) {
1553
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_SCHEDULED);
1554
        $meetingInfoGet->duration = $duration;
1555
        $meetingInfoGet->start_time = $startTime->format(DATE_ATOM);
1556
        $meetingInfoGet->agenda = $agenda;
1557
        $meetingInfoGet->password = $password;
1558
        $meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED;
1559
        if ('true' === $this->get('enableParticipantRegistration')) {
1560
            $meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE;
1561
        }
1562
1563
        return $this->createMeetingFromMeeting(
1564
            (new Meeting())
1565
                ->setMeetingInfoGet($meetingInfoGet)
1566
                ->setUser($user)
1567
                ->setCourse($course)
1568
                ->setGroup($group)
1569
                ->setSession($session)
1570
                ->setSignAttendance($signAttendance)
1571
                ->setReasonToSignAttendance($reasonToSignAttendance)
1572
        );
1573
    }
1574
1575
    /**
1576
     * Registers all the course users to a course meeting.
1577
     *
1578
     * @param Meeting $meeting
1579
     *
1580
     * @throws OptimisticLockException
1581
     */
1582
    private function registerAllCourseUsers($meeting)
1583
    {
1584
        $this->registerUsers($meeting, $meeting->getRegistrableUsers());
1585
    }
1586
}
1587