Passed
Push — 1.11.x ( 1b35e4...608c0e )
by Julito
14:08
created

ZoomPlugin::registerAllCourseUsers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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