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

ZoomPlugin::create()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

Loading history...
1224
        }
1225
1226
        return null;
1227
    }
1228
1229
    /**
1230
     * @param Meeting $meeting
1231
     *
1232
     * @return bool whether the logged-in user can manage conferences in this context, that is either
1233
     *              the current course or session coach, the platform admin or the current course admin
1234
     */
1235
    public function userIsConferenceManager($meeting)
1236
    {
1237
        if (null === $meeting) {
1238
            return false;
1239
        }
1240
1241
        if (api_is_coach() || api_is_platform_admin()) {
1242
            return true;
1243
        }
1244
1245
        if ($meeting->isCourseMeeting() && api_get_course_id() && api_is_course_admin()) {
1246
            return true;
1247
        }
1248
1249
        return $meeting->isUserMeeting() && $meeting->getUser()->getId() == api_get_user_id();
1250
    }
1251
1252
    /**
1253
     * @return bool whether the logged-in user can manage conferences in this context, that is either
1254
     *              the current course or session coach, the platform admin or the current course admin
1255
     */
1256
    public function userIsCourseConferenceManager()
1257
    {
1258
        if (api_is_coach() || api_is_platform_admin()) {
1259
            return true;
1260
        }
1261
1262
        if (api_get_course_id() && api_is_course_admin()) {
1263
            return true;
1264
        }
1265
1266
        return false;
1267
    }
1268
1269
    /**
1270
     * Update local recording list from remote Zoom server's version.
1271
     * Kept to implement a future administration button ("import existing data from zoom server").
1272
     *
1273
     * @param DateTime $startDate
1274
     * @param DateTime $endDate
1275
     *
1276
     * @throws OptimisticLockException
1277
     * @throws Exception
1278
     */
1279
    public function reloadPeriodRecordings($startDate, $endDate)
1280
    {
1281
        $em = Database::getManager();
1282
        $recordingRepo = $this->getRecordingRepository();
1283
        $meetingRepo = $this->getMeetingRepository();
1284
        $recordings = RecordingList::loadPeriodRecordings($startDate, $endDate);
1285
1286
        foreach ($recordings as $recordingMeeting) {
1287
            $recordingEntity = $recordingRepo->findOneBy(['uuid' => $recordingMeeting->uuid]);
1288
            if (null === $recordingEntity) {
1289
                $recordingEntity = new Recording();
1290
                $meeting = $meetingRepo->findOneBy(['meetingId' => $recordingMeeting->id]);
1291
                if (null === $meeting) {
1292
                    try {
1293
                        $meetingInfoGet = MeetingInfoGet::fromId($recordingMeeting->id);
1294
                    } catch (Exception $exception) {
1295
                        $meetingInfoGet = null; // deleted meeting with recordings
1296
                    }
1297
                    if (null !== $meetingInfoGet) {
1298
                        $meeting = $this->createMeetingFromMeeting(
1299
                            (new Meeting())->setMeetingInfoGet($meetingInfoGet)
1300
                        );
1301
                        $em->persist($meeting);
1302
                    }
1303
                }
1304
                if (null !== $meeting) {
1305
                    $recordingEntity->setMeeting($meeting);
1306
                }
1307
            }
1308
            $recordingEntity->setRecordingMeeting($recordingMeeting);
1309
            $em->persist($recordingEntity);
1310
        }
1311
        $em->flush();
1312
    }
1313
1314
    /**
1315
     * @return RecordingRepository|EntityRepository
1316
     */
1317
    public static function getRecordingRepository()
1318
    {
1319
        return Database::getManager()->getRepository(Recording::class);
1320
    }
1321
1322
    public function getToolbar($returnUrl = '')
1323
    {
1324
        if (!api_is_platform_admin()) {
1325
            return '';
1326
        }
1327
1328
        $actionsLeft = '';
1329
        $back = '';
1330
        $courseId = api_get_course_id();
1331
        if (empty($courseId)) {
1332
            $actionsLeft .=
1333
                Display::url(
1334
                    Display::return_icon('bbb.png', $this->get_lang('Meetings'), null, ICON_SIZE_MEDIUM),
1335
                    api_get_path(WEB_PLUGIN_PATH).'zoom/meetings.php'
1336
                );
1337
        } else {
1338
            $actionsLeft .=
1339
                Display::url(
1340
                    Display::return_icon('bbb.png', $this->get_lang('Meetings'), null, ICON_SIZE_MEDIUM),
1341
                    api_get_path(WEB_PLUGIN_PATH).'zoom/start.php?'.api_get_cidreq()
1342
                );
1343
        }
1344
1345
        if (!empty($returnUrl)) {
1346
            $back = Display::url(
1347
                Display::return_icon('back.png', get_lang('Back'), null, ICON_SIZE_MEDIUM),
1348
                $returnUrl
1349
            );
1350
        }
1351
1352
        if (api_is_platform_admin()) {
1353
            $actionsLeft .= Display::url(
1354
                Display::return_icon('agenda.png', get_lang('Calendar'), [], ICON_SIZE_MEDIUM),
1355
                'calendar.php'
1356
            );
1357
            $actionsLeft .=
1358
                Display::url(
1359
                    Display::return_icon('settings.png', get_lang('Settings'), null, ICON_SIZE_MEDIUM),
1360
                    api_get_path(WEB_CODE_PATH).'admin/configure_plugin.php?name=zoom'
1361
                ).$back;
1362
        }
1363
1364
        return Display::toolbarAction('toolbar', [$actionsLeft]);
1365
    }
1366
1367
    public function getRecordingSetting()
1368
    {
1369
        $recording = (string) $this->get('enableCloudRecording');
1370
1371
        if (in_array($recording, [self::RECORDING_TYPE_LOCAL, self::RECORDING_TYPE_CLOUD], true)) {
1372
            return $recording;
1373
        }
1374
1375
        return self::RECORDING_TYPE_NONE;
1376
    }
1377
1378
    public function hasRecordingAvailable()
1379
    {
1380
        $recording = $this->getRecordingSetting();
1381
1382
        return self::RECORDING_TYPE_NONE !== $recording;
1383
    }
1384
1385
    /**
1386
     * @throws Exception
1387
     */
1388
    public function createWebinarFromSchema(Webinar $webinar, WebinarSchema $schema): Webinar
1389
    {
1390
        $currentUser = api_get_user_entity(api_get_user_id());
1391
1392
        $schema->settings->contact_email = $currentUser->getEmail();
1393
        $schema->settings->contact_name = $currentUser->getFullname();
1394
        $schema->settings->auto_recording = $this->getRecordingSetting();
1395
        $schema->settings->registrants_email_notification = false;
1396
        $schema->settings->attendees_and_panelists_reminder_email_notification->enable = false;
1397
        $schema->settings->follow_up_attendees_email_notification->enable = false;
1398
        $schema->settings->follow_up_absentees_email_notification->enable = false;
1399
1400
        $schema = $schema->create($webinar->getAccountEmail());
1401
1402
        $webinar->setWebinarSchema($schema);
1403
1404
        $em = Database::getManager();
1405
        $em->persist($webinar);
1406
        $em->flush();
1407
1408
        return $webinar;
1409
    }
1410
1411
    public function getAccountEmails(): array
1412
    {
1413
        $currentValue = $this->get('accountSelector');
1414
1415
        if (empty($currentValue)) {
1416
            return [];
1417
        }
1418
1419
        $emails = explode(';', $currentValue);
1420
        $trimmed = array_map('trim', $emails);
1421
        $filtered = array_filter($trimmed);
1422
1423
        return array_combine($filtered, $filtered);
1424
    }
1425
1426
    /**
1427
     * Register users to a meeting.
1428
     *
1429
     * @param User[] $users
1430
     *
1431
     * @throws OptimisticLockException
1432
     *
1433
     * @return User[] failed registrations [ user id => errorMessage ]
1434
     */
1435
    public function registerUsers(Meeting $meeting, array $users)
1436
    {
1437
        $failedUsers = [];
1438
        foreach ($users as $user) {
1439
            try {
1440
                $this->registerUser($meeting, $user, false);
1441
            } catch (Exception $exception) {
1442
                $failedUsers[$user->getId()] = $exception->getMessage();
1443
            }
1444
        }
1445
        Database::getManager()->flush();
1446
1447
        return $failedUsers;
1448
    }
1449
1450
    /**
1451
     * Removes registrants from a meeting.
1452
     *
1453
     * @param Registrant[] $registrants
1454
     *
1455
     * @throws Exception
1456
     */
1457
    public function unregister(Meeting $meeting, array $registrants)
1458
    {
1459
        $meetingRegistrants = [];
1460
        foreach ($registrants as $registrant) {
1461
            $meetingRegistrants[] = $registrant->getMeetingRegistrant();
1462
        }
1463
1464
        if ($meeting instanceof Webinar) {
1465
            $meeting->getWebinarSchema()->removeRegistrants($meetingRegistrants);
1466
        } else {
1467
            $meeting->getMeetingInfoGet()->removeRegistrants($meetingRegistrants);
1468
        }
1469
1470
        $em = Database::getManager();
1471
        foreach ($registrants as $registrant) {
1472
            $em->remove($registrant);
1473
        }
1474
        $em->flush();
1475
    }
1476
1477
    /**
1478
     * Updates meeting registrants list. Adds the missing registrants and removes the extra.
1479
     *
1480
     * @param Meeting $meeting
1481
     * @param User[]  $users   list of users to be registered
1482
     *
1483
     * @throws Exception
1484
     */
1485
    private function updateRegistrantList($meeting, $users)
1486
    {
1487
        $usersToAdd = [];
1488
        foreach ($users as $user) {
1489
            $found = false;
1490
            foreach ($meeting->getRegistrants() as $registrant) {
1491
                if ($registrant->getUser() === $user) {
1492
                    $found = true;
1493
                    break;
1494
                }
1495
            }
1496
            if (!$found) {
1497
                $usersToAdd[] = $user;
1498
            }
1499
        }
1500
        $registrantsToRemove = [];
1501
        foreach ($meeting->getRegistrants() as $registrant) {
1502
            $found = false;
1503
            foreach ($users as $user) {
1504
                if ($registrant->getUser() === $user) {
1505
                    $found = true;
1506
                    break;
1507
                }
1508
            }
1509
            if (!$found) {
1510
                $registrantsToRemove[] = $registrant;
1511
            }
1512
        }
1513
        $this->registerUsers($meeting, $usersToAdd);
1514
        $this->unregister($meeting, $registrantsToRemove);
1515
    }
1516
1517
    /**
1518
     * @throws Exception
1519
     * @throws OptimisticLockException
1520
     *
1521
     * @return Registrant
1522
     */
1523
    private function registerUser(Meeting $meeting, User $user, $andFlush = true)
1524
    {
1525
        if (empty($user->getEmail())) {
1526
            throw new Exception($this->get_lang('CannotRegisterWithoutEmailAddress'));
1527
        }
1528
1529
        if ($meeting instanceof Webinar) {
1530
            $meetingRegistrant = WebinarRegistrantSchema::fromEmailAndFirstName(
1531
                $user->getEmail(),
1532
                $user->getFirstname(),
1533
                $user->getLastname()
1534
            );
1535
        } else {
1536
            $meetingRegistrant = MeetingRegistrant::fromEmailAndFirstName(
1537
                $user->getEmail(),
1538
                $user->getFirstname(),
1539
                $user->getLastname()
1540
            );
1541
        }
1542
1543
        $registrantEntity = (new Registrant())
1544
            ->setMeeting($meeting)
1545
            ->setUser($user)
1546
            ->setMeetingRegistrant($meetingRegistrant)
1547
        ;
1548
1549
        if ($meeting instanceof Webinar) {
1550
            $registrantEntity->setCreatedRegistration($meeting->getWebinarSchema()->addRegistrant($meetingRegistrant));
1551
        } else {
1552
            $registrantEntity->setCreatedRegistration($meeting->getMeetingInfoGet()->addRegistrant($meetingRegistrant));
1553
        }
1554
1555
        Database::getManager()->persist($registrantEntity);
1556
1557
        if ($andFlush) {
1558
            Database::getManager()->flush($registrantEntity);
1559
        }
1560
1561
        return $registrantEntity;
1562
    }
1563
1564
    /**
1565
     * Starts a new instant meeting and redirects to its start url.
1566
     *
1567
     * @param string          $topic
1568
     * @param User|null       $user
1569
     * @param Course|null     $course
1570
     * @param CGroupInfo|null $group
1571
     * @param Session|null    $session
1572
     *
1573
     * @throws Exception
1574
     */
1575
    private function startInstantMeeting($topic, $user = null, $course = null, $group = null, $session = null)
1576
    {
1577
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_INSTANT);
1578
        //$meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE;
1579
        $meeting = $this->createMeetingFromMeeting(
1580
            (new Meeting())
1581
                ->setMeetingInfoGet($meetingInfoGet)
1582
                ->setUser($user)
1583
                ->setGroup($group)
1584
                ->setCourse($course)
1585
                ->setSession($session)
1586
        );
1587
        api_location($meeting->getMeetingInfoGet()->start_url);
1588
    }
1589
1590
    /**
1591
     * Creates a meeting on Zoom servers and stores it in the local database.
1592
     *
1593
     * @param Meeting $meeting a new, unsaved meeting with at least a type and a topic
1594
     *
1595
     * @throws Exception
1596
     *
1597
     * @return Meeting
1598
     */
1599
    private function createMeetingFromMeeting($meeting)
1600
    {
1601
        $currentUser = api_get_user_entity(api_get_user_id());
1602
1603
        $meeting->getMeetingInfoGet()->settings->contact_email = $currentUser->getEmail();
1604
        $meeting->getMeetingInfoGet()->settings->contact_name = $currentUser->getFullname();
1605
        $meeting->getMeetingInfoGet()->settings->auto_recording = $this->getRecordingSetting();
1606
        $meeting->getMeetingInfoGet()->settings->registrants_email_notification = false;
1607
1608
        //$meeting->getMeetingInfoGet()->host_email = $currentUser->getEmail();
1609
        //$meeting->getMeetingInfoGet()->settings->alternative_hosts = $currentUser->getEmail();
1610
1611
        // Send create to Zoom.
1612
        $meeting->setMeetingInfoGet(
1613
            $meeting->getMeetingInfoGet()->create(
1614
                $meeting->getAccountEmail()
1615
            )
1616
        );
1617
1618
        Database::getManager()->persist($meeting);
1619
        Database::getManager()->flush();
1620
1621
        return $meeting;
1622
    }
1623
1624
    /**
1625
     * @throws Exception
1626
     *
1627
     * @return Meeting
1628
     */
1629
    private function createGlobalMeeting()
1630
    {
1631
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType(
1632
            $this->get_lang('GlobalMeeting'),
1633
            MeetingInfoGet::TYPE_SCHEDULED
1634
        );
1635
        $meetingInfoGet->start_time = (new DateTime())->format(DATE_ATOM);
1636
        $meetingInfoGet->duration = 60;
1637
        $meetingInfoGet->settings->approval_type =
1638
            ('true' === $this->get('enableParticipantRegistration'))
1639
                ? MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE
1640
                : MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED;
1641
        // $meetingInfoGet->settings->host_video = true;
1642
        $meetingInfoGet->settings->participant_video = true;
1643
        $meetingInfoGet->settings->join_before_host = true;
1644
        $meetingInfoGet->settings->registrants_email_notification = false;
1645
1646
        return $this->createMeetingFromMeeting((new Meeting())->setMeetingInfoGet($meetingInfoGet));
1647
    }
1648
1649
    /**
1650
     * Schedules a meeting and returns it.
1651
     * set $course, $session and $user to null in order to create a global meeting.
1652
     *
1653
     * @param DateTime $startTime meeting local start date-time (configure local timezone on your Zoom account)
1654
     * @param int      $duration  in minutes
1655
     * @param string   $topic     short title of the meeting, required
1656
     * @param string   $agenda    ordre du jour
1657
     * @param string   $password  meeting password
1658
     *
1659
     * @throws Exception
1660
     *
1661
     * @return Meeting meeting
1662
     */
1663
    private function createScheduleMeeting(
1664
        User $user = null,
1665
        Course $course = null,
1666
        CGroupInfo $group = null,
1667
        Session $session = null,
1668
        $startTime,
1669
        $duration,
1670
        $topic,
1671
        $agenda,
1672
        $password,
1673
        string $accountEmail = null
1674
    ) {
1675
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_SCHEDULED);
1676
        $meetingInfoGet->duration = $duration;
1677
        $meetingInfoGet->start_time = $startTime->format(DATE_ATOM);
1678
        $meetingInfoGet->agenda = $agenda;
1679
        $meetingInfoGet->password = $password;
1680
        $meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED;
1681
        if ('true' === $this->get('enableParticipantRegistration')) {
1682
            $meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE;
1683
        }
1684
1685
        return $this->createMeetingFromMeeting(
1686
            (new Meeting())
1687
                ->setMeetingInfoGet($meetingInfoGet)
1688
                ->setUser($user)
1689
                ->setCourse($course)
1690
                ->setGroup($group)
1691
                ->setSession($session)
1692
                ->setAccountEmail($accountEmail)
1693
        );
1694
    }
1695
1696
    /**
1697
     * @throws Exception
1698
     */
1699
    private function createScheduleWebinar(
1700
        ?User $user,
1701
        ?Course $course,
1702
        ?CGroupInfo $group,
1703
        ?Session $session,
1704
        DateTime $startTime,
1705
        $duration,
1706
        $topic,
1707
        $agenda,
1708
        $password,
1709
        string $accountEmail = null
1710
    ): Webinar {
1711
        $webinarSchema = WebinarSchema::fromTopicAndType($topic);
1712
        $webinarSchema->duration = $duration;
1713
        $webinarSchema->start_time = $startTime->format(DATE_ATOM);
1714
        $webinarSchema->agenda = $agenda;
1715
        $webinarSchema->password = $password;
1716
1717
        if ('true' === $this->get('enableParticipantRegistration')) {
1718
            $webinarSchema->settings->approval_type = WebinarSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE;
1719
        }
1720
1721
        $webinar = (new Webinar())
1722
            ->setUser($user)
1723
            ->setCourse($course)
1724
            ->setGroup($group)
1725
            ->setSession($session)
1726
            ->setAccountEmail($accountEmail)
1727
        ;
1728
1729
        return $this->createWebinarFromSchema($webinar, $webinarSchema);
1730
    }
1731
1732
    /**
1733
     * Registers all the course users to a course meeting.
1734
     *
1735
     * @param Meeting $meeting
1736
     *
1737
     * @throws OptimisticLockException
1738
     */
1739
    private function registerAllCourseUsers($meeting)
1740
    {
1741
        $this->registerUsers($meeting, $meeting->getRegistrableUsers());
1742
    }
1743
}
1744