Passed
Push — 1.11.x ( 97ce99...30a845 )
by Julito
13:32
created

ZoomPlugin::install()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 8
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\PluginBundle\Zoom\API\JWTClient;
8
use Chamilo\PluginBundle\Zoom\API\MeetingInfoGet;
9
use Chamilo\PluginBundle\Zoom\API\MeetingRegistrant;
10
use Chamilo\PluginBundle\Zoom\API\MeetingSettings;
11
use Chamilo\PluginBundle\Zoom\API\RecordingFile;
12
use Chamilo\PluginBundle\Zoom\API\RecordingList;
13
use Chamilo\PluginBundle\Zoom\MeetingEntity;
14
use Chamilo\PluginBundle\Zoom\MeetingEntityRepository;
15
use Chamilo\PluginBundle\Zoom\RecordingEntity;
16
use Chamilo\PluginBundle\Zoom\RecordingEntityRepository;
17
use Chamilo\PluginBundle\Zoom\RegistrantEntity;
18
use Chamilo\PluginBundle\Zoom\RegistrantEntityRepository;
19
use Chamilo\UserBundle\Entity\User;
20
use Doctrine\ORM\EntityRepository;
21
use Doctrine\ORM\OptimisticLockException;
22
use Doctrine\ORM\Tools\SchemaTool;
23
use Doctrine\ORM\Tools\ToolsException;
24
25
/**
26
 * Class ZoomPlugin. Integrates Zoom meetings in courses.
27
 */
28
class ZoomPlugin extends Plugin
29
{
30
    public $isCoursePlugin = true;
31
32
    /**
33
     * @var JWTClient
34
     */
35
    private $jwtClient;
36
37
    /**
38
     * ZoomPlugin constructor.
39
     * {@inheritdoc}
40
     * Initializes the API JWT client and the entity repositories.
41
     */
42
    public function __construct()
43
    {
44
        parent::__construct(
45
            '0.2',
46
            'Sébastien Ducoulombier, Julio Montoya',
47
            [
48
                'tool_enable' => 'boolean',
49
                'apiKey' => 'text',
50
                'apiSecret' => 'text',
51
                'verificationToken' => 'text',
52
                'enableParticipantRegistration' => 'boolean',
53
                'enableCloudRecording' => 'boolean',
54
                'enableGlobalConference' => 'boolean',
55
                'enableGlobalConferencePerUser' => 'boolean',
56
                'globalConferenceAllowRoles' => [
57
                    'type' => 'select',
58
                    'options' => [
59
                        PLATFORM_ADMIN => get_lang('Administrator'),
60
                        COURSEMANAGER => get_lang('Teacher'),
61
                        STUDENT => get_lang('Student'),
62
                        STUDENT_BOSS => get_lang('StudentBoss'),
63
                    ],
64
                    'attributes' => ['multiple' => 'multiple'],
65
                ],
66
                'globalConferencePerUserAllowRoles' => [
67
                    'type' => 'select',
68
                    'options' => [
69
                        PLATFORM_ADMIN => get_lang('Administrator'),
70
                        COURSEMANAGER => get_lang('Teacher'),
71
                        STUDENT => get_lang('Student'),
72
                        STUDENT_BOSS => get_lang('StudentBoss'),
73
                    ],
74
                    'attributes' => ['multiple' => 'multiple'],
75
                ],
76
            ]
77
        );
78
79
        $this->isAdminPlugin = true;
80
        $this->jwtClient = new JWTClient($this->get('apiKey'), $this->get('apiSecret'));
81
    }
82
83
    /**
84
     * Caches and returns an instance of this class.
85
     *
86
     * @return ZoomPlugin the instance to use
87
     */
88
    public static function create()
89
    {
90
        static $instance = null;
91
92
        return $instance ? $instance : $instance = new self();
93
    }
94
95
    /**
96
     * @return bool
97
     */
98
    public static function currentUserCanJoinGlobalMeeting()
99
    {
100
        $user = api_get_user_entity(api_get_user_id());
101
102
        if (null === $user) {
103
            return false;
104
        }
105
106
        //return 'true' === api_get_plugin_setting('zoom', 'enableGlobalConference') && api_user_is_login();
107
        return
108
            'true' === api_get_plugin_setting('zoom', 'enableGlobalConference')
109
            && in_array(
110
                (api_is_platform_admin() ? PLATFORM_ADMIN : $user->getStatus()),
111
                (array) api_get_plugin_setting('zoom', 'globalConferenceAllowRoles')
112
            )
113
            ;
114
    }
115
116
    /**
117
     * @return bool
118
     */
119
    public static function currentUserCanCreateUserMeeting()
120
    {
121
        $user = api_get_user_entity(api_get_user_id());
122
123
        if (null === $user) {
124
            return false;
125
        }
126
127
        return
128
            'true' === api_get_plugin_setting('zoom', 'enableGlobalConferencePerUser')
129
            && in_array(
130
                (api_is_platform_admin() ? PLATFORM_ADMIN : $user->getStatus()),
131
                (array) api_get_plugin_setting('zoom', 'globalConferencePerUserAllowRoles')
132
            )
133
        ;
134
    }
135
136
    /**
137
     * @return array [ $title => $link ]
138
     */
139
    public function meetingsToWhichCurrentUserIsRegisteredComingSoon()
140
    {
141
        $linkTemplate = api_get_path(WEB_PLUGIN_PATH).'zoom/join_meeting.php?meetingId=%s';
142
        $user = api_get_user_entity(api_get_user_id());
143
        $meetings = self::getRegistrantRepository()->meetingsComingSoonRegistrationsForUser($user);
144
        $items = [];
145
        foreach ($meetings as $registrant) {
146
            $meeting = $registrant->getMeeting();
147
            $items[
148
                sprintf(
149
                    $this->get_lang('DateMeetingTitle'),
150
                    $meeting->formattedStartTime,
151
                    $meeting->getMeetingInfoGet()->topic
152
                )
153
            ] = sprintf($linkTemplate, $meeting->getId());
154
        }
155
156
        return $items;
157
    }
158
159
    /**
160
     * @return array
161
     */
162
    public function getProfileBlockItems()
163
    {
164
        $elements = $this->meetingsToWhichCurrentUserIsRegisteredComingSoon();
165
        if (self::currentUserCanJoinGlobalMeeting()) {
166
            $elements[$this->get_lang('CreateGlobalVideoConference')] = api_get_path(WEB_PLUGIN_PATH).'zoom/global.php';
167
        }
168
169
        if (self::currentUserCanCreateUserMeeting()) {
170
            $elements[$this->get_lang('CreateUserVideoConference')] = api_get_path(WEB_PLUGIN_PATH).'zoom/user.php';
171
        }
172
        $items = [];
173
        foreach ($elements as $title => $link) {
174
            $items[] = [
175
                'class' => 'video-conference',
176
                'icon' => Display::return_icon(
177
                    'zoom.png',
178
                    get_lang('VideoConference')
179
                ),
180
                'link' => $link,
181
                'title' => $title,
182
            ];
183
        }
184
185
        return $items;
186
    }
187
188
    /**
189
     * @return MeetingEntityRepository|EntityRepository
190
     */
191
    public static function getMeetingRepository()
192
    {
193
        return Database::getManager()->getRepository(MeetingEntity::class);
194
    }
195
196
    /**
197
     * @return RecordingEntityRepository|EntityRepository
198
     */
199
    public static function getRecordingRepository()
200
    {
201
        return Database::getManager()->getRepository(RecordingEntity::class);
202
    }
203
204
    /**
205
     * @return RegistrantEntityRepository|EntityRepository
206
     */
207
    public static function getRegistrantRepository()
208
    {
209
        return Database::getManager()->getRepository(RegistrantEntity::class);
210
    }
211
212
    /**
213
     * Creates this plugin's related tables in the internal database.
214
     * Installs course fields in all courses.
215
     *
216
     * @throws ToolsException
217
     */
218
    public function install()
219
    {
220
        (new SchemaTool(Database::getManager()))->createSchema([
221
            Database::getManager()->getClassMetadata(MeetingEntity::class),
222
            Database::getManager()->getClassMetadata(RecordingEntity::class),
223
            Database::getManager()->getClassMetadata(RegistrantEntity::class),
224
        ]);
225
        $this->install_course_fields_in_all_courses();
226
    }
227
228
    /**
229
     * Drops this plugins' related tables from the internal database.
230
     * Uninstalls course fields in all courses().
231
     */
232
    public function uninstall()
233
    {
234
        (new SchemaTool(Database::getManager()))->dropSchema([
235
            Database::getManager()->getClassMetadata(MeetingEntity::class),
236
            Database::getManager()->getClassMetadata(RecordingEntity::class),
237
            Database::getManager()->getClassMetadata(RegistrantEntity::class),
238
        ]);
239
        $this->uninstall_course_fields_in_all_courses();
240
    }
241
242
    /**
243
     * Generates the search form to include in the meeting list administration page.
244
     * The form has DatePickers 'start' and 'end' and Checkbox 'reloadRecordingLists'.
245
     *
246
     * @return FormValidator the form
247
     */
248
    public function getAdminSearchForm()
249
    {
250
        $form = new FormValidator('search');
251
        $form->addHeader($this->get_lang('SearchMeeting'));
252
        $form->addDatePicker('start', get_lang('StartDate'));
253
        $form->addDatePicker('end', get_lang('EndDate'));
254
        $form->addButtonSearch(get_lang('Search'));
255
        $oneMonth = new DateInterval('P1M');
256
        if ($form->validate()) {
257
            try {
258
                $start = new DateTime($form->getSubmitValue('start'));
259
            } catch (Exception $exception) {
260
                $start = new DateTime();
261
                $start->sub($oneMonth);
262
            }
263
            try {
264
                $end = new DateTime($form->getSubmitValue('end'));
265
            } catch (Exception $exception) {
266
                $end = new DateTime();
267
                $end->add($oneMonth);
268
            }
269
        } else {
270
            $start = new DateTime();
271
            $start->sub($oneMonth);
272
            $end = new DateTime();
273
            $end->add($oneMonth);
274
        }
275
        try {
276
            $form->setDefaults([
277
                'start' => $start->format('Y-m-d'),
278
                'end' => $end->format('Y-m-d'),
279
            ]);
280
        } catch (Exception $exception) {
281
            error_log(join(':', [__FILE__, __LINE__, $exception]));
282
        }
283
284
        return $form;
285
    }
286
287
    /**
288
     * Generates a meeting edit form and updates the meeting on validation.
289
     *
290
     * @param MeetingEntity $meetingEntity the meeting
291
     *
292
     * @throws Exception
293
     *
294
     * @return FormValidator
295
     */
296
    public function getEditMeetingForm($meetingEntity)
297
    {
298
        $meetingInfoGet = $meetingEntity->getMeetingInfoGet();
299
        $form = new FormValidator('edit', 'post', $_SERVER['REQUEST_URI']);
300
        $form->addHeader($this->get_lang('UpdateMeeting'));
301
        $form->addText('topic', $this->get_lang('Topic'));
302
        if ($meetingEntity->requiresDateAndDuration()) {
303
            $startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('StartTime'));
304
            $form->setRequired($startTimeDatePicker);
305
            $durationNumeric = $form->addNumeric('duration', $this->get_lang('DurationInMinutes'));
306
            $form->setRequired($durationNumeric);
307
        }
308
        $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]);
309
        // $form->addText('password', get_lang('Password'), false, ['maxlength' => '10']);
310
        $form->addButtonUpdate(get_lang('Update'));
311
        if ($form->validate()) {
312
            if ($meetingEntity->requiresDateAndDuration()) {
313
                $meetingInfoGet->start_time = (new DateTime($form->getSubmitValue('startTime')))->format(
314
                    DateTimeInterface::ISO8601
315
                );
316
                $meetingInfoGet->timezone = date_default_timezone_get();
317
                $meetingInfoGet->duration = (int) $form->getSubmitValue('duration');
318
            }
319
            $meetingInfoGet->topic = $form->getSubmitValue('topic');
320
            $meetingInfoGet->agenda = $form->getSubmitValue('agenda');
321
            try {
322
                $meetingInfoGet->update();
323
                $meetingEntity->setMeetingInfoGet($meetingInfoGet);
324
                Database::getManager()->persist($meetingEntity);
325
                Database::getManager()->flush();
326
                Display::addFlash(
327
                    Display::return_message($this->get_lang('MeetingUpdated'), 'confirm')
328
                );
329
            } catch (Exception $exception) {
330
                Display::addFlash(
331
                    Display::return_message($exception->getMessage(), 'error')
332
                );
333
            }
334
        }
335
        $defaults = [
336
            'topic' => $meetingInfoGet->topic,
337
            'agenda' => $meetingInfoGet->agenda,
338
        ];
339
        if ($meetingEntity->requiresDateAndDuration()) {
340
            $defaults['startTime'] = $meetingEntity->startDateTime->format('Y-m-d H:i');
341
            $defaults['duration'] = $meetingInfoGet->duration;
342
        }
343
        $form->setDefaults($defaults);
344
345
        return $form;
346
    }
347
348
    /**
349
     * Generates a meeting delete form and deletes the meeting on validation.
350
     *
351
     * @param MeetingEntity $meetingEntity
352
     * @param string        $returnURL     where to redirect to on successful deletion
353
     * @param string
354
     *
355
     * @throws Exception
356
     *
357
     * @return FormValidator
358
     */
359
    public function getDeleteMeetingForm($meetingEntity, $returnURL, $type)
360
    {
361
        $id = $meetingEntity->getMeetingId();
362
        $type = Security::remove_XSS($type);
363
364
        $form = new FormValidator('delete', 'post', api_get_self().'?type='.$type.'&meetingId='.$id);
365
        $form->addButtonDelete($this->get_lang('DeleteMeeting'));
366
        if ($form->validate()) {
367
            $this->deleteMeeting($meetingEntity, $returnURL);
368
        }
369
370
        return $form;
371
    }
372
373
    /**
374
     * @param MeetingEntity $meetingEntity
375
     * @param string        $returnURL
376
     *
377
     * @return false
378
     */
379
    public function deleteMeeting($meetingEntity, $returnURL)
380
    {
381
        if (null === $meetingEntity) {
382
            return false;
383
        }
384
385
        try {
386
            $meetingEntity->getMeetingInfoGet()->delete();
387
            Database::getManager()->remove($meetingEntity);
388
            Database::getManager()->flush();
389
390
            Display::addFlash(
391
                Display::return_message($this->get_lang('MeetingDeleted'), 'confirm')
392
            );
393
            api_location($returnURL);
394
        } catch (Exception $exception) {
395
            Display::addFlash(
396
                Display::return_message($exception->getMessage(), 'error')
397
            );
398
        }
399
    }
400
401
    /**
402
     * Generates a registrant list update form listing course and session users.
403
     * Updates the list on validation.
404
     *
405
     * @param MeetingEntity $meetingEntity
406
     *
407
     * @throws Exception
408
     *
409
     * @return FormValidator
410
     */
411
    public function getRegisterParticipantForm($meetingEntity)
412
    {
413
        $form = new FormValidator('register', 'post', $_SERVER['REQUEST_URI']);
414
        $userIdSelect = $form->addSelect('userIds', $this->get_lang('RegisteredUsers'));
415
        $userIdSelect->setMultiple(true);
416
        $form->addButtonSend($this->get_lang('UpdateRegisteredUserList'));
417
418
        $users = $meetingEntity->getRegistrableUsers();
419
        foreach ($users as $user) {
420
            $userIdSelect->addOption(
421
                api_get_person_name($user->getFirstname(), $user->getLastname()),
422
                $user->getId()
423
            );
424
        }
425
426
        if ($form->validate()) {
427
            $selectedUserIds = $form->getSubmitValue('userIds');
428
            $selectedUsers = [];
429
            foreach ($users as $user) {
430
                if (in_array($user->getId(), $selectedUserIds)) {
431
                    $selectedUsers[] = $user;
432
                }
433
            }
434
            try {
435
                $this->updateRegistrantList($meetingEntity, $selectedUsers);
436
                Display::addFlash(
437
                    Display::return_message($this->get_lang('RegisteredUserListWasUpdated'), 'confirm')
438
                );
439
            } catch (Exception $exception) {
440
                Display::addFlash(
441
                    Display::return_message($exception->getMessage(), 'error')
442
                );
443
            }
444
        }
445
        $registeredUserIds = [];
446
        foreach ($meetingEntity->getRegistrants() as $registrant) {
447
            $registeredUserIds[] = $registrant->getUser()->getId();
448
        }
449
        $userIdSelect->setSelected($registeredUserIds);
450
451
        return $form;
452
    }
453
454
    /**
455
     * Generates a meeting recording files management form.
456
     * Takes action on validation.
457
     *
458
     * @param MeetingEntity $meeting
459
     *
460
     * @throws Exception
461
     *
462
     * @return FormValidator
463
     */
464
    public function getFileForm($meeting)
465
    {
466
        $form = new FormValidator('fileForm', 'post', $_SERVER['REQUEST_URI']);
467
        if (!$meeting->getRecordings()->isEmpty()) {
468
            $fileIdSelect = $form->addSelect('fileIds', get_lang('Files'));
469
            $fileIdSelect->setMultiple(true);
470
            foreach ($meeting->getRecordings() as &$recording) {
471
                // $recording->instanceDetails = $plugin->getPastMeetingInstanceDetails($instance->uuid);
472
                $options = [];
473
                foreach ($recording->getRecordingMeeting()->recording_files as $file) {
474
                    $options[] = [
475
                        'text' => sprintf(
476
                            '%s.%s (%s)',
477
                            $file->recording_type,
478
                            $file->file_type,
479
                            //$file->formattedFileSize
480
                            $file->file_size
481
                        ),
482
                        'value' => $file->id,
483
                    ];
484
                }
485
                $fileIdSelect->addOptGroup(
486
                    $options,
487
                    sprintf("%s (%s)", $recording->formattedStartTime, $recording->formattedDuration)
488
                );
489
            }
490
            $actions = [];
491
            if ($meeting->isCourseMeeting()) {
492
                $actions['CreateLinkInCourse'] = $this->get_lang('CreateLinkInCourse');
493
                $actions['CopyToCourse'] = $this->get_lang('CopyToCourse');
494
            }
495
            $actions['DeleteFile'] = $this->get_lang('DeleteFile');
496
            $form->addRadio(
497
                'action',
498
                get_lang('Action'),
499
                $actions
500
            );
501
            $form->addButtonUpdate($this->get_lang('DoIt'));
502
            if ($form->validate()) {
503
                foreach ($meeting->getRecordings() as $recording) {
504
                    foreach ($recording->files as $file) {
505
                        if (in_array($file->id, $form->getSubmitValue('fileIds'))) {
506
                            $name = sprintf(
507
                                $this->get_lang('XRecordingOfMeetingXFromXDurationXDotX'),
508
                                $file->recording_type,
509
                                $meeting->getId(),
510
                                $recording->formattedStartTime,
511
                                $recording->formattedDuration,
512
                                $file->file_type
513
                            );
514
                            $action = $form->getSubmitValue('action');
515
                            if ('CreateLinkInCourse' === $action && $meeting->isCourseMeeting()) {
516
                                try {
517
                                    $this->createLinkToFileInCourse($meeting, $file, $name);
518
                                    Display::addFlash(
519
                                        Display::return_message($this->get_lang('LinkToFileWasCreatedInCourse'), 'success')
520
                                    );
521
                                } catch (Exception $exception) {
522
                                    Display::addFlash(
523
                                        Display::return_message($exception->getMessage(), 'error')
524
                                    );
525
                                }
526
                            } elseif ('CopyToCourse' === $action && $meeting->isCourseMeeting()) {
527
                                try {
528
                                    $this->copyFileToCourse($meeting, $file, $name);
529
                                    Display::addFlash(
530
                                        Display::return_message($this->get_lang('FileWasCopiedToCourse'), 'confirm')
531
                                    );
532
                                } catch (Exception $exception) {
533
                                    Display::addFlash(
534
                                        Display::return_message($exception->getMessage(), 'error')
535
                                    );
536
                                }
537
                            } elseif ('DeleteFile' === $action) {
538
                                try {
539
                                    $file->delete();
540
                                    Display::addFlash(
541
                                        Display::return_message($this->get_lang('FileWasDeleted'), 'confirm')
542
                                    );
543
                                } catch (Exception $exception) {
544
                                    Display::addFlash(
545
                                        Display::return_message($exception->getMessage(), 'error')
546
                                    );
547
                                }
548
                            }
549
                        }
550
                    }
551
                }
552
            }
553
        }
554
555
        return $form;
556
    }
557
558
    /**
559
     * Generates a form to fast and easily create and start an instant meeting.
560
     * On validation, create it then redirect to it and exit.
561
     *
562
     * @param User    $user
563
     * @param Course  $course
564
     * @param Session $session
565
     *
566
     * @return FormValidator
567
     */
568
    public function getCreateInstantMeetingForm($user, $course, $session)
569
    {
570
        $form = new FormValidator('createInstantMeetingForm', 'post', '', '_blank');
571
        $form->addButton('startButton', $this->get_lang('StartInstantMeeting'), 'video-camera', 'primary');
572
        if ($form->validate()) {
573
            try {
574
                $this->startInstantMeeting($this->get_lang('InstantMeeting'), $user, $course, $session);
575
            } catch (Exception $exception) {
576
                Display::addFlash(
577
                    Display::return_message($exception->getMessage(), 'error')
578
                );
579
            }
580
        }
581
582
        return $form;
583
    }
584
585
    /**
586
     * Generates a form to schedule a meeting.
587
     * On validation, creates it and redirects to its page.
588
     *
589
     * @param User|null    $user
590
     * @param Course|null  $course
591
     * @param Session|null $session
592
     *
593
     * @throws Exception
594
     *
595
     * @return FormValidator
596
     */
597
    public function getScheduleMeetingForm($user, $course = null, $session = null)
598
    {
599
        $form = new FormValidator('scheduleMeetingForm');
600
        $form->addHeader($this->get_lang('ScheduleAMeeting'));
601
        $startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('StartTime'));
602
        $form->setRequired($startTimeDatePicker);
603
604
        $form->addText('topic', $this->get_lang('Topic'), true);
605
        $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]);
606
607
        $durationNumeric = $form->addNumeric('duration', $this->get_lang('DurationInMinutes'));
608
        $form->setRequired($durationNumeric);
609
610
        // $passwordText = $form->addText('password', get_lang('Password'), false, ['maxlength' => '10']);
611
        if (null !== $course) {
612
            $registrationOptions = [
613
                'RegisterAllCourseUsers' => $this->get_lang('RegisterAllCourseUsers'),
614
            ];
615
            $groups = GroupManager::get_groups();
616
            if (!empty($groups)) {
617
                $registrationOptions['RegisterTheseGroupMembers'] = get_lang('RegisterTheseGroupMembers');
618
            }
619
            $registrationOptions['RegisterNoUser'] = $this->get_lang('RegisterNoUser');
620
            $userRegistrationRadio = $form->addRadio(
621
                'userRegistration',
622
                $this->get_lang('UserRegistration'),
623
                $registrationOptions
624
            );
625
            $groupOptions = [];
626
            foreach ($groups as $group) {
627
                $groupOptions[$group['id']] = $group['name'];
628
            }
629
            $groupIdsSelect = $form->addSelect(
630
                'groupIds',
631
                $this->get_lang('RegisterTheseGroupMembers'),
632
                $groupOptions
633
            );
634
            $groupIdsSelect->setMultiple(true);
635
            if (!empty($groups)) {
636
                $jsCode = sprintf(
637
                    "getElementById('%s').parentNode.parentNode.parentNode.style.display = getElementById('%s').checked ? 'block' : 'none'",
638
                    $groupIdsSelect->getAttribute('id'),
639
                    $userRegistrationRadio->getelements()[1]->getAttribute('id')
640
                );
641
642
                $form->setAttribute('onchange', $jsCode);
643
            }
644
        }
645
        $form->addButtonCreate(get_lang('Save'));
646
647
        if ($form->validate()) {
648
            try {
649
                $newMeeting = $this->scheduleMeeting(
650
                    $user,
651
                    $course,
652
                    $session,
653
                    new DateTime($form->getSubmitValue('startTime')),
654
                    $form->getSubmitValue('duration'),
655
                    $form->getSubmitValue('topic'),
656
                    $form->getSubmitValue('agenda'),
657
                    ''
658
                );
659
                Display::addFlash(
660
                    Display::return_message($this->get_lang('NewMeetingCreated'))
661
                );
662
                if ($newMeeting->isCourseMeeting()) {
663
                    if ('RegisterAllCourseUsers' === $form->getSubmitValue('userRegistration')) {
664
                        $this->registerAllCourseUsers($newMeeting);
665
                        Display::addFlash(
666
                            Display::return_message($this->get_lang('AllCourseUsersWereRegistered'))
667
                        );
668
                    } elseif ('RegisterTheseGroupMembers' === $form->getSubmitValue('userRegistration')) {
669
                        $userIds = [];
670
                        foreach ($form->getSubmitValue('groupIds') as $groupId) {
671
                            $userIds = array_unique(array_merge($userIds, GroupManager::get_users($groupId)));
672
                        }
673
                        $users = Database::getManager()->getRepository('ChamiloUserBundle:User')->findBy(
674
                            ['id' => $userIds]
675
                        );
676
                        $this->registerUsers($newMeeting, $users);
677
                        Display::addFlash(
678
                            Display::return_message($this->get_lang('GroupUsersWereRegistered'))
679
                        );
680
                    }
681
                    api_location('meeting.php?type=start&meetingId='.$newMeeting->getMeetingId());
682
                } elseif (null !== $user) {
683
                    api_location('meeting.php?type=user&meetingId='.$newMeeting->getMeetingId());
684
                }
685
            } catch (Exception $exception) {
686
                Display::addFlash(
687
                    Display::return_message($exception->getMessage(), 'error')
688
                );
689
            }
690
        } else {
691
            $form->setDefaults(
692
                [
693
                    'duration' => 60,
694
                    'userRegistration' => 'RegisterAllCourseUsers',
695
                ]
696
            );
697
        }
698
699
        return $form;
700
    }
701
702
    /**
703
     * @param MeetingEntity $meetingEntity
704
     *
705
     * @return bool whether the logged-in user can manage conferences in this context, that is either
706
     *              the current course or session coach, the platform admin or the current course admin
707
     */
708
    public function userIsConferenceManager($meetingEntity)
709
    {
710
        if (null === $meetingEntity) {
711
            return false;
712
        }
713
714
        return api_is_coach()
715
            || api_is_platform_admin()
716
            || $meetingEntity->isCourseMeeting() && api_get_course_id() && api_is_course_admin()
717
            || $meetingEntity->isUserMeeting() && $meetingEntity->getUser()->getId() == api_get_user_id();
718
    }
719
720
    /**
721
     * @param Course $course
722
     *
723
     * @return bool whether the logged-in user can manage conferences in this context, that is either
724
     *              the current course or session coach, the platform admin or the current course admin
725
     */
726
    public function userIsCourseConferenceManager($course)
727
    {
728
        return api_is_coach()
729
            || api_is_platform_admin()
730
            || api_get_course_id() && api_is_course_admin();
731
    }
732
733
    /**
734
     * Adds to the meeting course documents a link to a meeting instance recording file.
735
     *
736
     * @param MeetingEntity $meeting
737
     * @param RecordingFile $file
738
     * @param string        $name
739
     *
740
     * @throws Exception
741
     */
742
    public function createLinkToFileInCourse($meeting, $file, $name)
743
    {
744
        $course = $meeting->getCourse();
745
        if (null === $course) {
746
            throw new Exception('This meeting is not linked to a course');
747
        }
748
        $courseInfo = api_get_course_info_by_id($course->getId());
749
        if (empty($courseInfo)) {
750
            throw new Exception('This meeting is not linked to a valid course');
751
        }
752
        $path = '/zoom_meeting_recording_file_'.$file->id.'.'.$file->file_type;
753
        $docId = DocumentManager::addCloudLink($courseInfo, $path, $file->play_url, $name);
754
        if (!$docId) {
755
            throw new Exception(get_lang(DocumentManager::cloudLinkExists($courseInfo, $path, $file->play_url) ? 'UrlAlreadyExists' : 'ErrorAddCloudLink'));
756
        }
757
    }
758
759
    /**
760
     * Copies a recording file to a meeting's course.
761
     *
762
     * @param MeetingEntity $meeting
763
     * @param RecordingFile $file
764
     * @param string        $name
765
     *
766
     * @throws Exception
767
     */
768
    public function copyFileToCourse($meeting, $file, $name)
769
    {
770
        $course = $meeting->getCourse();
771
        if (null === $course) {
772
            throw new Exception('This meeting is not linked to a course');
773
        }
774
        $courseInfo = api_get_course_info_by_id($course->getId());
775
        if (empty($courseInfo)) {
776
            throw new Exception('This meeting is not linked to a valid course');
777
        }
778
        $tmpFile = tmpfile();
779
        if (false === $tmpFile) {
780
            throw new Exception('tmpfile() returned false');
781
        }
782
        $curl = curl_init($file->getFullDownloadURL($this->jwtClient->token));
783
        if (false === $curl) {
784
            throw new Exception('Could not init curl: '.curl_error($curl));
785
        }
786
        if (!curl_setopt_array(
787
            $curl,
788
            [
789
                CURLOPT_FILE => $tmpFile,
790
                CURLOPT_FOLLOWLOCATION => true,
791
                CURLOPT_MAXREDIRS => 10,
792
                CURLOPT_TIMEOUT => 120,
793
            ]
794
        )) {
795
            throw new Exception("Could not set curl options: ".curl_error($curl));
796
        }
797
        if (false === curl_exec($curl)) {
798
            throw new Exception("curl_exec failed: ".curl_error($curl));
799
        }
800
        $newPath = handle_uploaded_document(
801
            $courseInfo,
802
            [
803
                'name' => $name,
804
                'tmp_name' => stream_get_meta_data($tmpFile)['uri'],
805
                'size' => filesize(stream_get_meta_data($tmpFile)['uri']),
806
                'from_file' => true,
807
                'type' => $file->file_type,
808
            ],
809
            '/',
810
            api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document',
811
            api_get_user_id(),
812
            0,
813
            null,
814
            0,
815
            '',
816
            true,
817
            false,
818
            null,
819
            $meeting->getSession()->getId(),
820
            true
821
        );
822
        fclose($tmpFile);
823
        if (false === $newPath) {
824
            throw new Exception('could not handle uploaded document');
825
        }
826
    }
827
828
    /**
829
     * Return the current global meeting (create it if needed).
830
     *
831
     * @throws Exception
832
     *
833
     * @return string
834
     */
835
    public function getGlobalMeeting()
836
    {
837
        foreach ($this->getMeetingRepository()->unfinishedGlobalMeetings() as $meeting) {
838
            return $meeting;
839
        }
840
841
        return $this->createGlobalMeeting();
842
    }
843
844
    /**
845
     * Returns the URL to enter (start or join) a meeting or null if not possible to enter the meeting,
846
     * The returned URL depends on the meeting current status (waiting, started or finished) and the current user.
847
     *
848
     * @param MeetingEntity $meeting
849
     *
850
     * @throws Exception
851
     * @throws OptimisticLockException
852
     *
853
     * @return string|null
854
     */
855
    public function getStartOrJoinMeetingURL($meeting)
856
    {
857
        $status = $meeting->getMeetingInfoGet()->status;
858
        $currentUser = api_get_user_entity(api_get_user_id());
859
        $isGlobal = 'true' === $this->get('enableGlobalConference') && $meeting->isGlobalMeeting();
860
861
        switch ($status) {
862
            case 'ended':
863
                if ($currentUser === $meeting->getUser()) {
864
                    return $meeting->getMeetingInfoGet()->start_url;
865
                }
866
                break;
867
            case 'waiting':
868
                // Zoom does not allow for a new meeting to be started on first participant join.
869
                // It requires the host to start the meeting first.
870
                // Therefore for global meetings we must make the first participant the host
871
                // that is use start_url rather than join_url.
872
                // the participant will not be registered and will appear as the Zoom user account owner.
873
                // For course and user meetings, only the host can start the meeting.
874
                if ($isGlobal || $currentUser === $meeting->getUser()) {
875
                    return $meeting->getMeetingInfoGet()->start_url;
876
                }
877
                break;
878
            case 'started':
879
                if ($currentUser === $meeting->getUser()) {
880
                    return $meeting->getMeetingInfoGet()->join_url;
881
                }
882
883
                // the participant is not registered, he can join only the global meeting (automatic registration)
884
                if ($isGlobal) {
885
                    return $this->registerUser($meeting, $currentUser)->getCreatedRegistration()->join_url;
886
                }
887
888
                if ('true' === $this->get('enableParticipantRegistration')) {
889
                //if ('true' === $this->get('enableParticipantRegistration') && $meeting->requiresRegistration()) {
890
                    // the participant must be registered
891
                    $registrant = $meeting->getRegistrant($currentUser);
892
                    if (null !== $registrant) {
893
                        // the participant is registered
894
                        return $registrant->getCreatedRegistration()->join_url;
895
                    }
896
                }
897
                break;
898
        }
899
900
        return null;
901
    }
902
903
    /**
904
     * Update local recording list from remote Zoom server's version.
905
     * Kept to implement a future administration button ("import existing data from zoom server").
906
     *
907
     * @param DateTime $startDate
908
     * @param DateTime $endDate
909
     *
910
     * @throws OptimisticLockException
911
     * @throws Exception
912
     */
913
    public function reloadPeriodRecordings($startDate, $endDate)
914
    {
915
        $em = Database::getManager();
916
        $recordingRepo = $this->getRecordingRepository();
917
        $meetingRepo = $this->getMeetingRepository();
918
        $recordings = RecordingList::loadPeriodRecordings($startDate, $endDate);
919
920
        foreach ($recordings as $recordingMeeting) {
921
            $recordingEntity = $recordingRepo->findOneBy(['uuid' => $recordingMeeting->uuid]);
922
            if (null === $recordingEntity) {
923
                $recordingEntity = new RecordingEntity();
924
                $meetingEntity = $meetingRepo->findOneBy(['meetingId' => $recordingMeeting->id]);
925
                if (null === $meetingEntity) {
926
                    try {
927
                        $meetingInfoGet = MeetingInfoGet::fromId($recordingMeeting->id);
928
                    } catch (Exception $exception) {
929
                        $meetingInfoGet = null; // deleted meeting with recordings
930
                    }
931
                    if (null !== $meetingInfoGet) {
932
                        $meetingEntity = $this->createMeetingFromMeetingEntity(
933
                            (new MeetingEntity())->setMeetingInfoGet($meetingInfoGet)
934
                        );
935
                        $em->persist($meetingEntity);
936
                    }
937
                }
938
                if (null !== $meetingEntity) {
939
                    $recordingEntity->setMeeting($meetingEntity);
940
                }
941
            }
942
            $recordingEntity->setRecordingMeeting($recordingMeeting);
943
            $em->persist($recordingEntity);
944
        }
945
        $em->flush();
946
    }
947
948
    public function getToolbar($returnUrl = '')
949
    {
950
        if (!api_is_platform_admin()) {
951
            return '';
952
        }
953
954
        $actionsLeft = '';
955
        $back = '';
956
957
        if ('true' === api_get_plugin_setting('zoom', 'enableGlobalConference')) {
958
            $actionsLeft .=
959
                Display::url(
960
                    Display::return_icon('links.png', $this->get_lang('GlobalMeeting'), null, ICON_SIZE_MEDIUM),
961
                    api_get_path(WEB_PLUGIN_PATH).'zoom/admin.php'
962
                )
963
            ;
964
        }
965
966
        if ('true' === api_get_plugin_setting('zoom', 'enableGlobalConferencePerUser')) {
967
            $actionsLeft .=
968
                Display::url(
969
                    Display::return_icon('user.png', $this->get_lang('GlobalMeetingPerUser'), null, ICON_SIZE_MEDIUM),
970
                    api_get_path(WEB_PLUGIN_PATH).'zoom/user.php'
971
                )
972
            ;
973
        }
974
975
        if (!empty($returnUrl)) {
976
            $back = Display::url(
977
                Display::return_icon('back.png', get_lang('Back'), null, ICON_SIZE_MEDIUM),
978
                $returnUrl
979
            );
980
        }
981
982
        $actionsLeft .=
983
            Display::url(
984
            Display::return_icon('settings.png', get_lang('Settings'), null, ICON_SIZE_MEDIUM),
985
            api_get_path(WEB_CODE_PATH).'admin/configure_plugin.php?name=zoom'
986
            ).$back
987
        ;
988
989
        return Display::toolbarAction('toolbar', [$actionsLeft]);
990
    }
991
992
    /**
993
     * Creates a meeting on Zoom servers and stores it in the local database.
994
     *
995
     * @param MeetingEntity $meeting a new, unsaved meeting with at least a type and a topic
996
     *
997
     * @throws Exception
998
     *
999
     * @return MeetingEntity
1000
     */
1001
    private function createMeetingFromMeetingEntity($meeting)
1002
    {
1003
        $approvalType = $meeting->getMeetingInfoGet()->settings->approval_type;
1004
        $meeting->getMeetingInfoGet()->settings->auto_recording = 'true' === $this->get('enableCloudRecording')
1005
            ? 'cloud'
1006
            : 'local';
1007
        $meeting->getMeetingInfoGet()->settings->registrants_email_notification = false;
1008
        $meeting->setMeetingInfoGet($meeting->getMeetingInfoGet()->create());
1009
        $meeting->getMeetingInfoGet()->settings->approval_type = $approvalType;
1010
1011
        Database::getManager()->persist($meeting);
1012
        Database::getManager()->flush();
1013
1014
        return $meeting;
1015
    }
1016
1017
    /**
1018
     * @throws Exception
1019
     *
1020
     * @return MeetingEntity
1021
     */
1022
    private function createGlobalMeeting()
1023
    {
1024
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType(
1025
            $this->get_lang('GlobalMeeting'),
1026
            MeetingInfoGet::TYPE_SCHEDULED
1027
        );
1028
        $meetingInfoGet->start_time = (new DateTime())->format(DateTimeInterface::ISO8601);
1029
        $meetingInfoGet->duration = 60;
1030
        $meetingInfoGet->settings->approval_type =
1031
            ('true' === $this->get('enableParticipantRegistration'))
1032
                ? MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE
1033
                : MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED;
1034
        // $meetingInfoGet->settings->host_video = true;
1035
        $meetingInfoGet->settings->participant_video = true;
1036
        $meetingInfoGet->settings->join_before_host = true;
1037
        $meetingInfoGet->settings->registrants_email_notification = false;
1038
1039
        return $this->createMeetingFromMeetingEntity((new MeetingEntity())->setMeetingInfoGet($meetingInfoGet));
1040
    }
1041
1042
    /**
1043
     * Schedules a meeting and returns it.
1044
     * set $course, $session and $user to null in order to create a global meeting.
1045
     *
1046
     * @param User|null    $user      the current user, for a course meeting or a user meeting
1047
     * @param Course|null  $course    the course, for a course meeting
1048
     * @param Session|null $session   the session, for a course meeting
1049
     * @param DateTime     $startTime meeting local start date-time (configure local timezone on your Zoom account)
1050
     * @param int          $duration  in minutes
1051
     * @param string       $topic     short title of the meeting, required
1052
     * @param string       $agenda    ordre du jour
1053
     * @param string       $password  meeting password
1054
     *
1055
     * @throws Exception
1056
     *
1057
     * @return MeetingEntity meeting
1058
     */
1059
    private function scheduleMeeting($user, $course, $session, $startTime, $duration, $topic, $agenda, $password)
1060
    {
1061
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_SCHEDULED);
1062
        $meetingInfoGet->duration = $duration;
1063
        $meetingInfoGet->start_time = $startTime->format(DateTimeInterface::ISO8601);
1064
        $meetingInfoGet->agenda = $agenda;
1065
        $meetingInfoGet->password = $password;
1066
        $meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED;
1067
        if ('true' === $this->get('enableParticipantRegistration')) {
1068
            $meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE;
1069
        }
1070
1071
        return $this->createMeetingFromMeetingEntity(
1072
            (new MeetingEntity())
1073
                ->setMeetingInfoGet($meetingInfoGet)
1074
                ->setUser($user)
1075
                ->setCourse($course)
1076
                ->setSession($session)
1077
        );
1078
    }
1079
1080
    /**
1081
     * Starts a new instant meeting and redirects to its start url.
1082
     *
1083
     * @param string       $topic
1084
     * @param User|null    $user
1085
     * @param Course|null  $course
1086
     * @param Session|null $session
1087
     *
1088
     * @throws Exception
1089
     */
1090
    private function startInstantMeeting($topic, $user = null, $course = null, $session = null)
1091
    {
1092
        $meeting = $this->createMeetingFromMeetingEntity(
1093
            (new MeetingEntity())
1094
                ->setMeetingInfoGet(MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_INSTANT))
1095
                ->setUser($user)
1096
                ->setCourse($course)
1097
                ->setSession($session)
1098
        );
1099
        api_location($meeting->getMeetingInfoGet()->start_url);
1100
    }
1101
1102
    /**
1103
     * @param MeetingEntity $meetingEntity
1104
     * @param User          $user
1105
     * @param bool          $andFlush
1106
     *
1107
     * @throws OptimisticLockException
1108
     * @throws Exception
1109
     *
1110
     * @return RegistrantEntity
1111
     */
1112
    private function registerUser($meetingEntity, $user, $andFlush = true)
1113
    {
1114
        if (empty($user->getEmail())) {
1115
            throw new Exception($this->get_lang('CannotRegisterWithoutEmailAddress'));
1116
        }
1117
1118
        $meetingRegistrant = MeetingRegistrant::fromEmailAndFirstName(
1119
            $user->getEmail(),
1120
            $user->getFirstname(),
1121
            $user->getLastname()
1122
        );
1123
1124
        $registrantEntity = (new RegistrantEntity())
1125
            ->setMeeting($meetingEntity)
1126
            ->setUser($user)
1127
            ->setMeetingRegistrant($meetingRegistrant)
1128
            ->setCreatedRegistration($meetingEntity->getMeetingInfoGet()->addRegistrant($meetingRegistrant));
1129
        Database::getManager()->persist($registrantEntity);
1130
        if ($andFlush) {
1131
            Database::getManager()->flush($registrantEntity);
1132
        }
1133
1134
        return $registrantEntity;
1135
    }
1136
1137
    /**
1138
     * Register users to a meeting.
1139
     *
1140
     * @param MeetingEntity $meetingEntity
1141
     * @param User[]        $users
1142
     *
1143
     * @throws OptimisticLockException
1144
     *
1145
     * @return User[] failed registrations [ user id => errorMessage ]
1146
     */
1147
    private function registerUsers($meetingEntity, $users)
1148
    {
1149
        $failedUsers = [];
1150
        foreach ($users as $user) {
1151
            try {
1152
                $this->registerUser($meetingEntity, $user, false);
1153
            } catch (Exception $exception) {
1154
                $failedUsers[$user->getId()] = $exception->getMessage();
1155
            }
1156
        }
1157
        Database::getManager()->flush();
1158
1159
        return $failedUsers;
1160
    }
1161
1162
    /**
1163
     * Registers all the course users to a course meeting.
1164
     *
1165
     * @param MeetingEntity $meetingEntity
1166
     *
1167
     * @throws OptimisticLockException
1168
     */
1169
    private function registerAllCourseUsers($meetingEntity)
1170
    {
1171
        $this->registerUsers($meetingEntity, $meetingEntity->getRegistrableUsers());
1172
    }
1173
1174
    /**
1175
     * Removes registrants from a meeting.
1176
     *
1177
     * @param MeetingEntity      $meetingEntity
1178
     * @param RegistrantEntity[] $registrants
1179
     *
1180
     * @throws Exception
1181
     */
1182
    private function unregister($meetingEntity, $registrants)
1183
    {
1184
        $meetingRegistrants = [];
1185
        foreach ($registrants as $registrant) {
1186
            $meetingRegistrants[] = $registrant->getMeetingRegistrant();
1187
        }
1188
        $meetingEntity->getMeetingInfoGet()->removeRegistrants($meetingRegistrants);
1189
        $em = Database::getManager();
1190
        foreach ($registrants as $registrant) {
1191
            $em->remove($registrant);
1192
        }
1193
        $em->flush();
1194
    }
1195
1196
    /**
1197
     * Updates meeting registrants list. Adds the missing registrants and removes the extra.
1198
     *
1199
     * @param MeetingEntity $meetingEntity
1200
     * @param User[]        $users         list of users to be registered
1201
     *
1202
     * @throws Exception
1203
     */
1204
    private function updateRegistrantList($meetingEntity, $users)
1205
    {
1206
        $usersToAdd = [];
1207
        foreach ($users as $user) {
1208
            $found = false;
1209
            foreach ($meetingEntity->getRegistrants() as $registrant) {
1210
                if ($registrant->getUser() === $user) {
1211
                    $found = true;
1212
                    break;
1213
                }
1214
            }
1215
            if (!$found) {
1216
                $usersToAdd[] = $user;
1217
            }
1218
        }
1219
        $registrantsToRemove = [];
1220
        foreach ($meetingEntity->getRegistrants() as $registrant) {
1221
            $found = false;
1222
            foreach ($users as $user) {
1223
                if ($registrant->getUser() === $user) {
1224
                    $found = true;
1225
                    break;
1226
                }
1227
            }
1228
            if (!$found) {
1229
                $registrantsToRemove[] = $registrant;
1230
            }
1231
        }
1232
        $this->registerUsers($meetingEntity, $usersToAdd);
1233
        $this->unregister($meetingEntity, $registrantsToRemove);
1234
    }
1235
}
1236