Passed
Pull Request — 1.11.x (#4160)
by Angel Fernando Quiroz
18:18 queued 07:20
created

ZoomPlugin::createMeetingFromMeeting()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
c 0
b 0
f 0
dl 0
loc 23
rs 9.9
cc 1
nc 1
nop 1
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\BaseMeetingTrait;
9
use Chamilo\PluginBundle\Zoom\API\JWTClient;
10
use Chamilo\PluginBundle\Zoom\API\MeetingInfoGet;
11
use Chamilo\PluginBundle\Zoom\API\MeetingRegistrant;
12
use Chamilo\PluginBundle\Zoom\API\MeetingSettings;
13
use Chamilo\PluginBundle\Zoom\API\RecordingFile;
14
use Chamilo\PluginBundle\Zoom\API\RecordingList;
15
use Chamilo\PluginBundle\Zoom\API\WebinarRegistrantSchema;
16
use Chamilo\PluginBundle\Zoom\API\WebinarSchema;
17
use Chamilo\PluginBundle\Zoom\API\WebinarSettings;
18
use Chamilo\PluginBundle\Zoom\Meeting;
19
use Chamilo\PluginBundle\Zoom\MeetingActivity;
20
use Chamilo\PluginBundle\Zoom\MeetingRepository;
21
use Chamilo\PluginBundle\Zoom\Recording;
22
use Chamilo\PluginBundle\Zoom\RecordingRepository;
23
use Chamilo\PluginBundle\Zoom\Registrant;
24
use Chamilo\PluginBundle\Zoom\RegistrantRepository;
25
use Chamilo\PluginBundle\Zoom\Signature;
26
use Chamilo\PluginBundle\Zoom\Webinar;
27
use Chamilo\UserBundle\Entity\User;
28
use Doctrine\ORM\EntityRepository;
29
use Doctrine\ORM\OptimisticLockException;
30
use Doctrine\ORM\Tools\SchemaTool;
31
use Doctrine\ORM\Tools\ToolsException;
32
33
/**
34
 * Class ZoomPlugin. Integrates Zoom meetings in courses.
35
 */
36
class ZoomPlugin extends Plugin
37
{
38
    const RECORDING_TYPE_CLOUD = 'cloud';
39
    const RECORDING_TYPE_LOCAL = 'local';
40
    const RECORDING_TYPE_NONE = 'none';
41
    public $isCoursePlugin = true;
42
43
    /**
44
     * @var JWTClient
45
     */
46
    private $jwtClient;
47
48
    /**
49
     * ZoomPlugin constructor.
50
     * {@inheritdoc}
51
     * Initializes the API JWT client and the entity repositories.
52
     */
53
    public function __construct()
54
    {
55
        parent::__construct(
56
            '0.4',
57
            'Sébastien Ducoulombier, Julio Montoya, Angel Fernando Quiroz Campos',
58
            [
59
                'tool_enable' => 'boolean',
60
                'apiKey' => 'text',
61
                'apiSecret' => 'text',
62
                'verificationToken' => 'text',
63
                'enableParticipantRegistration' => 'boolean',
64
                'enableCloudRecording' => [
65
                    'type' => 'select',
66
                    'options' => [
67
                        self::RECORDING_TYPE_CLOUD => 'Cloud',
68
                        self::RECORDING_TYPE_LOCAL => 'Local',
69
                        self::RECORDING_TYPE_NONE => get_lang('None'),
70
                    ],
71
                ],
72
                'enableGlobalConference' => 'boolean',
73
                'globalConferenceAllowRoles' => [
74
                    'type' => 'select',
75
                    'options' => [
76
                        PLATFORM_ADMIN => get_lang('Administrator'),
77
                        COURSEMANAGER => get_lang('Teacher'),
78
                        STUDENT => get_lang('Student'),
79
                        STUDENT_BOSS => get_lang('StudentBoss'),
80
                    ],
81
                    'attributes' => ['multiple' => 'multiple'],
82
                ],
83
                'accountSelector' => 'text',
84
            ]
85
        );
86
87
        $this->isAdminPlugin = true;
88
        $this->jwtClient = new JWTClient($this->get('apiKey'), $this->get('apiSecret'));
89
    }
90
91
    /**
92
     * Caches and returns an instance of this class.
93
     *
94
     * @return ZoomPlugin the instance to use
95
     */
96
    public static function create()
97
    {
98
        static $instance = null;
99
100
        return $instance ? $instance : $instance = new self();
101
    }
102
103
    /**
104
     * @return bool
105
     */
106
    public static function currentUserCanJoinGlobalMeeting()
107
    {
108
        $user = api_get_user_entity(api_get_user_id());
109
110
        if (null === $user) {
111
            return false;
112
        }
113
114
        //return 'true' === api_get_plugin_setting('zoom', 'enableGlobalConference') && api_user_is_login();
115
        return
116
            'true' === api_get_plugin_setting('zoom', 'enableGlobalConference')
117
            && in_array(
118
                (api_is_platform_admin() ? PLATFORM_ADMIN : $user->getStatus()),
119
                (array) api_get_plugin_setting('zoom', 'globalConferenceAllowRoles')
120
            );
121
    }
122
123
    /**
124
     * @return array
125
     */
126
    public function getProfileBlockItems()
127
    {
128
        $elements = $this->meetingsToWhichCurrentUserIsRegisteredComingSoon();
129
        $addMeetingLink = false;
130
        if (self::currentUserCanJoinGlobalMeeting()) {
131
            $addMeetingLink = true;
132
        }
133
134
        if ($addMeetingLink) {
135
            $elements[$this->get_lang('Meetings')] = api_get_path(WEB_PLUGIN_PATH).'zoom/meetings.php';
136
        }
137
138
        $items = [];
139
        foreach ($elements as $title => $link) {
140
            $items[] = [
141
                'class' => 'video-conference',
142
                'icon' => Display::return_icon(
143
                    'bbb.png',
144
                    get_lang('VideoConference')
145
                ),
146
                'link' => $link,
147
                'title' => $title,
148
            ];
149
        }
150
151
        return $items;
152
    }
153
154
    /**
155
     * @return array [ $title => $link ]
156
     */
157
    public function meetingsToWhichCurrentUserIsRegisteredComingSoon()
158
    {
159
        $linkTemplate = api_get_path(WEB_PLUGIN_PATH).'zoom/join_meeting.php?meetingId=%s';
160
        $user = api_get_user_entity(api_get_user_id());
161
        $meetings = self::getRegistrantRepository()->meetingsComingSoonRegistrationsForUser($user);
162
        $items = [];
163
        foreach ($meetings as $registrant) {
164
            $meeting = $registrant->getMeeting();
165
166
            $items[sprintf(
167
                $this->get_lang('DateMeetingTitle'),
168
                $meeting->formattedStartTime,
169
                $meeting->getTopic()
170
            )] = sprintf($linkTemplate, $meeting->getMeetingId());
171
        }
172
173
        return $items;
174
    }
175
176
    /**
177
     * @return RegistrantRepository|EntityRepository
178
     */
179
    public static function getRegistrantRepository()
180
    {
181
        return Database::getManager()->getRepository(Registrant::class);
182
    }
183
184
    /**
185
     * Creates this plugin's related tables in the internal database.
186
     * Installs course fields in all courses.
187
     *
188
     * @throws ToolsException
189
     */
190
    public function install()
191
    {
192
        $schemaManager = Database::getManager()->getConnection()->getSchemaManager();
193
194
        $tablesExists = $schemaManager->tablesExist(
195
            [
196
                'plugin_zoom_meeting',
197
                'plugin_zoom_meeting_activity',
198
                'plugin_zoom_recording',
199
                'plugin_zoom_registrant',
200
                'plugin_zoom_signature',
201
            ]
202
        );
203
204
        if ($tablesExists) {
205
            return;
206
        }
207
208
        $em = Database::getManager();
209
210
        (new SchemaTool($em))->createSchema(
211
            [
212
                $em->getClassMetadata(Meeting::class),
213
                $em->getClassMetadata(Webinar::class),
214
                $em->getClassMetadata(MeetingActivity::class),
215
                $em->getClassMetadata(Recording::class),
216
                $em->getClassMetadata(Registrant::class),
217
                $em->getClassMetadata(Signature::class),
218
            ]
219
        );
220
221
        // Copy icons into the main/img/icons folder
222
        $iconName = 'zoom_meet';
223
        $iconsList = [
224
            '64/'.$iconName.'.png',
225
            '64/'.$iconName.'_na.png',
226
            '32/'.$iconName.'.png',
227
            '32/'.$iconName.'_na.png',
228
            '22/'.$iconName.'.png',
229
            '22/'.$iconName.'_na.png',
230
        ];
231
        $sourceDir = api_get_path(SYS_PLUGIN_PATH).'zoom/resources/img/';
232
        $destinationDir = api_get_path(SYS_CODE_PATH).'img/icons/';
233
        foreach ($iconsList as $icon) {
234
            $src = $sourceDir.$icon;
235
            $dest = $destinationDir.$icon;
236
            copy($src, $dest);
237
        }
238
239
        $this->install_course_fields_in_all_courses(true, 'zoom_meet.png');
240
    }
241
242
    /**
243
     * Drops this plugins' related tables from the internal database.
244
     * Uninstalls course fields in all courses().
245
     */
246
    public function uninstall()
247
    {
248
        $em = Database::getManager();
249
250
        (new SchemaTool($em))->dropSchema(
251
            [
252
                $em->getClassMetadata(Meeting::class),
253
                $em->getClassMetadata(Webinar::class),
254
                $em->getClassMetadata(MeetingActivity::class),
255
                $em->getClassMetadata(Recording::class),
256
                $em->getClassMetadata(Registrant::class),
257
                $em->getClassMetadata(Signature::class),
258
            ]
259
        );
260
        $this->uninstall_course_fields_in_all_courses();
261
262
        // Remove icons from the main/img/icons folder
263
        $iconName = 'zoom_meet';
264
        $iconsList = [
265
            '64/'.$iconName.'.png',
266
            '64/'.$iconName.'_na.png',
267
            '32/'.$iconName.'.png',
268
            '32/'.$iconName.'_na.png',
269
            '22/'.$iconName.'.png',
270
            '22/'.$iconName.'_na.png',
271
        ];
272
        $destinationDir = api_get_path(SYS_CODE_PATH).'img/icons/';
273
        foreach ($iconsList as $icon) {
274
            $dest = $destinationDir.$icon;
275
            if (is_file($dest)) {
276
                @unlink($dest);
277
            }
278
        }
279
    }
280
281
    /**
282
     * Generates the search form to include in the meeting list administration page.
283
     * The form has DatePickers 'start' and 'end' and Checkbox 'reloadRecordingLists'.
284
     *
285
     * @return FormValidator the form
286
     */
287
    public function getAdminSearchForm()
288
    {
289
        $form = new FormValidator('search');
290
        $form->addHeader($this->get_lang('SearchMeeting'));
291
        $form->addDatePicker('start', get_lang('StartDate'));
292
        $form->addDatePicker('end', get_lang('EndDate'));
293
        $form->addButtonSearch(get_lang('Search'));
294
        $oneMonth = new DateInterval('P1M');
295
        if ($form->validate()) {
296
            try {
297
                $start = new DateTime($form->getSubmitValue('start'));
298
            } catch (Exception $exception) {
299
                $start = new DateTime();
300
                $start->sub($oneMonth);
301
            }
302
            try {
303
                $end = new DateTime($form->getSubmitValue('end'));
304
            } catch (Exception $exception) {
305
                $end = new DateTime();
306
                $end->add($oneMonth);
307
            }
308
        } else {
309
            $start = new DateTime();
310
            $start->sub($oneMonth);
311
            $end = new DateTime();
312
            $end->add($oneMonth);
313
        }
314
        try {
315
            $form->setDefaults(
316
                [
317
                    'start' => $start->format('Y-m-d'),
318
                    'end' => $end->format('Y-m-d'),
319
                ]
320
            );
321
        } catch (Exception $exception) {
322
            error_log(join(':', [__FILE__, __LINE__, $exception]));
323
        }
324
325
        return $form;
326
    }
327
328
    /**
329
     * @throws Exception
330
     */
331
    public function getEditConferenceForm(Meeting $conference): FormValidator
332
    {
333
        $isWebinar = $conference instanceof Webinar;
334
        $requiresDateAndDuration = $conference->requiresDateAndDuration();
335
336
        /** @var BaseMeetingTrait $schema */
337
        $schema = $isWebinar ? $conference->getWebinarSchema() : $conference->getMeetingInfoGet();
338
339
        $form = new FormValidator('edit', 'post', $_SERVER['REQUEST_URI']);
340
        $form->addHeader(
341
            $isWebinar ? $this->get_lang('UpdateWebinar') : $this->get_lang('UpdateMeeting')
342
        );
343
        $form->addLabel(get_lang('Type'), $conference->typeName);
344
        if ($conference->getAccountEmail()) {
345
            $form->addLabel(
346
                $this->get_lang('AccountEmail'),
347
                $conference->getAccountEmail()
348
            );
349
        }
350
        $form->addText('topic', $this->get_lang('Topic'));
351
352
        if ($requiresDateAndDuration) {
353
            $startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('StartTime'));
354
            $durationNumeric = $form->addNumeric('duration', $this->get_lang('DurationInMinutes'));
355
356
            $form->setRequired($startTimeDatePicker);
357
            $form->setRequired($durationNumeric);
358
        }
359
360
        $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]);
361
        $form->addCheckBox('sign_attendance', $this->get_lang('SignAttendance'), get_lang('Yes'));
362
        $form->addTextarea('reason_to_sign', $this->get_lang('ReasonToSign'), ['rows' => 5]);
363
        $form->addButtonUpdate(get_lang('Update'));
364
365
        if ($form->validate()) {
366
            $formValues = $form->exportValues();
367
368
            $em = Database::getManager();
369
370
            if ($requiresDateAndDuration) {
371
                $schema->start_time = (new DateTime($formValues['startTime']))->format(DATE_ATOM);
372
                $schema->timezone = date_default_timezone_get();
373
                $schema->duration = (int) $formValues['duration'];
374
            }
375
376
            $schema->topic = $formValues['topic'];
377
            $schema->agenda = $formValues['agenda'];
378
379
            $conference
380
                ->setSignAttendance(isset($formValues['sign_attendance']))
381
                ->setReasonToSignAttendance($formValues['reason_to_sign']);
382
383
            try {
384
                $schema->update();
385
386
                if ($isWebinar) {
387
                    $conference->setWebinarSchema($schema);
388
                } else {
389
                    $conference->setMeetingInfoGet($schema);
390
                }
391
392
                $em->persist($conference);
393
                $em->flush();
394
395
                Display::addFlash(
396
                    Display::return_message(
397
                        $isWebinar ? $this->get_lang('WebinarUpdated') : $this->get_lang('MeetingUpdated'),
398
                        'confirm'
399
                    )
400
                );
401
            } catch (Exception $exception) {
402
                Display::addFlash(
403
                    Display::return_message($exception->getMessage(), 'error')
404
                );
405
            }
406
        }
407
408
        $defaults = [
409
            'topic' => $schema->topic,
410
            'agenda' => $schema->agenda,
411
        ];
412
413
        if ($requiresDateAndDuration) {
414
            $defaults['startTime'] = $conference->startDateTime->format('Y-m-d H:i');
415
            $defaults['duration'] = $schema->duration;
416
        }
417
418
        $defaults['sign_attendance'] = $conference->isSignAttendance();
419
        $defaults['reason_to_sign'] = $conference->getReasonToSignAttendance();
420
421
        $form->setDefaults($defaults);
422
423
        return $form;
424
    }
425
426
    /**
427
     * Generates a meeting delete form and deletes the meeting on validation.
428
     *
429
     * @param Meeting $meeting
430
     * @param string  $returnURL where to redirect to on successful deletion
431
     *
432
     * @throws Exception
433
     *
434
     * @return FormValidator
435
     */
436
    public function getDeleteMeetingForm($meeting, $returnURL)
437
    {
438
        $id = $meeting->getMeetingId();
439
        $form = new FormValidator('delete', 'post', api_get_self().'?meetingId='.$id);
440
        $form->addButtonDelete($this->get_lang('DeleteMeeting'));
441
        if ($form->validate()) {
442
            $this->deleteMeeting($meeting, $returnURL);
443
        }
444
445
        return $form;
446
    }
447
448
    public function getDeleteWebinarForm(Webinar $webinar, string $returnURL): FormValidator
449
    {
450
        $id = $webinar->getMeetingId();
451
        $form = new FormValidator('delete', 'post', api_get_self()."?meetingId=$id");
452
        $form->addButtonDelete($this->get_lang('DeleteWebinar'));
453
454
        if ($form->validate()) {
455
            $this->deleteWebinar($webinar, $returnURL);
456
        }
457
458
        return $form;
459
    }
460
461
    /**
462
     * @param Meeting $meeting
463
     * @param string  $returnURL
464
     *
465
     * @return false
466
     */
467
    public function deleteMeeting($meeting, $returnURL)
468
    {
469
        if (null === $meeting) {
470
            return false;
471
        }
472
473
        $em = Database::getManager();
474
        try {
475
            // No need to delete a instant meeting.
476
            if (\Chamilo\PluginBundle\Zoom\API\Meeting::TYPE_INSTANT != $meeting->getMeetingInfoGet()->type) {
477
                $meeting->getMeetingInfoGet()->delete();
478
            }
479
480
            $em->remove($meeting);
481
            $em->flush();
482
483
            Display::addFlash(
484
                Display::return_message($this->get_lang('MeetingDeleted'), 'confirm')
485
            );
486
            api_location($returnURL);
487
        } catch (Exception $exception) {
488
            $this->handleException($exception);
489
        }
490
    }
491
492
    public function deleteWebinar(Webinar $webinar, string $returnURL)
493
    {
494
        $em = Database::getManager();
495
496
        try {
497
            $webinar->getWebinarSchema()->delete();
498
499
            $em->remove($webinar);
500
            $em->flush();
501
502
            Display::addFlash(
503
                Display::return_message($this->get_lang('WebinarDeleted'), 'success')
504
            );
505
506
            api_location($returnURL);
507
        } catch (Exception $exception) {
508
            $this->handleException($exception);
509
        }
510
    }
511
512
    /**
513
     * @param Exception $exception
514
     */
515
    public function handleException($exception)
516
    {
517
        if ($exception instanceof Exception) {
0 ignored issues
show
introduced by
$exception is always a sub-type of Exception.
Loading history...
518
            $error = json_decode($exception->getMessage());
519
            $message = $exception->getMessage();
520
            if ($error->message) {
521
                $message = $error->message;
522
            }
523
            Display::addFlash(
524
                Display::return_message($message, 'error')
525
            );
526
        }
527
    }
528
529
    /**
530
     * Generates a registrant list update form listing course and session users.
531
     * Updates the list on validation.
532
     *
533
     * @param Meeting $meeting
534
     *
535
     * @throws Exception
536
     *
537
     * @return FormValidator
538
     */
539
    public function getRegisterParticipantForm($meeting)
540
    {
541
        $form = new FormValidator('register', 'post', $_SERVER['REQUEST_URI']);
542
        $userIdSelect = $form->addSelect('userIds', $this->get_lang('RegisteredUsers'));
543
        $userIdSelect->setMultiple(true);
544
        $form->addButtonSend($this->get_lang('UpdateRegisteredUserList'));
545
546
        $selfRegistrationUrl = api_get_path(WEB_PLUGIN_PATH)
547
            .'zoom/subscription.php?meetingId='.$meeting->getMeetingId();
548
549
        $form->addHtml(
550
            '<div class="form-group"><div class="col-sm-8 col-sm-offset-2">
551
                <hr style="margin-top: 0;">
552
                <label for="frm-registration__txt-self-registration">'
553
            .$this->get_lang('UrlForSelfRegistration').'</label>
554
                <div class="input-group">
555
                    <input type="text" class="form-control" id="frm-registration__txt-self-registration" value="'
556
            .$selfRegistrationUrl.'">
557
                    <span class="input-group-btn">
558
                        <button class="btn btn-default" type="button"
559
                         onclick="copyTextToClipBoard(\'frm-registration__txt-self-registration\');">'
560
            .$this->get_lang('CopyTextToClipboard').'</button>
561
                    </span>
562
                </div>
563
            </div></div>'
564
        );
565
566
        $users = $meeting->getRegistrableUsers();
567
        foreach ($users as $user) {
568
            $userIdSelect->addOption(
569
                api_get_person_name($user->getFirstname(), $user->getLastname()),
570
                $user->getId()
571
            );
572
        }
573
574
        if ($form->validate()) {
575
            $selectedUserIds = $form->getSubmitValue('userIds');
576
            $selectedUsers = [];
577
            if (!empty($selectedUserIds)) {
578
                foreach ($users as $user) {
579
                    if (in_array($user->getId(), $selectedUserIds)) {
580
                        $selectedUsers[] = $user;
581
                    }
582
                }
583
            }
584
585
            try {
586
                $this->updateRegistrantList($meeting, $selectedUsers);
587
                Display::addFlash(
588
                    Display::return_message($this->get_lang('RegisteredUserListWasUpdated'), 'confirm')
589
                );
590
            } catch (Exception $exception) {
591
                Display::addFlash(
592
                    Display::return_message($exception->getMessage(), 'error')
593
                );
594
            }
595
        }
596
        $registeredUserIds = [];
597
        foreach ($meeting->getRegistrants() as $registrant) {
598
            $registeredUserIds[] = $registrant->getUser()->getId();
599
        }
600
        $userIdSelect->setSelected($registeredUserIds);
601
602
        return $form;
603
    }
604
605
    /**
606
     * Generates a meeting recording files management form.
607
     * Takes action on validation.
608
     *
609
     * @param Meeting $meeting
610
     *
611
     * @throws Exception
612
     *
613
     * @return FormValidator
614
     */
615
    public function getFileForm($meeting, $returnURL)
616
    {
617
        $form = new FormValidator('fileForm', 'post', $_SERVER['REQUEST_URI']);
618
        if (!$meeting->getRecordings()->isEmpty()) {
619
            $fileIdSelect = $form->addSelect('fileIds', get_lang('Files'));
620
            $fileIdSelect->setMultiple(true);
621
            $recordingList = $meeting->getRecordings();
622
            foreach ($recordingList as &$recording) {
623
                // $recording->instanceDetails = $plugin->getPastMeetingInstanceDetails($instance->uuid);
624
                $options = [];
625
                $recordings = $recording->getRecordingMeeting()->recording_files;
626
                foreach ($recordings as $file) {
627
                    $options[] = [
628
                        'text' => sprintf(
629
                            '%s.%s (%s)',
630
                            $file->recording_type,
631
                            $file->file_type,
632
                            $file->file_size
633
                        ),
634
                        'value' => $file->id,
635
                    ];
636
                }
637
                $fileIdSelect->addOptGroup(
638
                    $options,
639
                    sprintf("%s (%s)", $recording->formattedStartTime, $recording->formattedDuration)
640
                );
641
            }
642
            $actions = [];
643
            if ($meeting->isCourseMeeting()) {
644
                $actions['CreateLinkInCourse'] = $this->get_lang('CreateLinkInCourse');
645
                $actions['CopyToCourse'] = $this->get_lang('CopyToCourse');
646
            }
647
            $actions['DeleteFile'] = $this->get_lang('DeleteFile');
648
            $form->addRadio(
649
                'action',
650
                get_lang('Action'),
651
                $actions
652
            );
653
            $form->addButtonUpdate($this->get_lang('DoIt'));
654
            if ($form->validate()) {
655
                $action = $form->getSubmitValue('action');
656
                $idList = $form->getSubmitValue('fileIds');
657
658
                foreach ($recordingList as $recording) {
659
                    $recordings = $recording->getRecordingMeeting()->recording_files;
660
661
                    foreach ($recordings as $file) {
662
                        if (in_array($file->id, $idList)) {
663
                            $name = sprintf(
664
                                $this->get_lang('XRecordingOfMeetingXFromXDurationXDotX'),
665
                                $file->recording_type,
666
                                $meeting->getId(),
667
                                $recording->formattedStartTime,
668
                                $recording->formattedDuration,
669
                                $file->file_type
670
                            );
671
                            if ('CreateLinkInCourse' === $action && $meeting->isCourseMeeting()) {
672
                                try {
673
                                    $this->createLinkToFileInCourse($meeting, $file, $name);
674
                                    Display::addFlash(
675
                                        Display::return_message(
676
                                            $this->get_lang('LinkToFileWasCreatedInCourse'),
677
                                            'success'
678
                                        )
679
                                    );
680
                                } catch (Exception $exception) {
681
                                    Display::addFlash(
682
                                        Display::return_message($exception->getMessage(), 'error')
683
                                    );
684
                                }
685
                            } elseif ('CopyToCourse' === $action && $meeting->isCourseMeeting()) {
686
                                try {
687
                                    $this->copyFileToCourse($meeting, $file, $name);
688
                                    Display::addFlash(
689
                                        Display::return_message($this->get_lang('FileWasCopiedToCourse'), 'confirm')
690
                                    );
691
                                } catch (Exception $exception) {
692
                                    Display::addFlash(
693
                                        Display::return_message($exception->getMessage(), 'error')
694
                                    );
695
                                }
696
                            } elseif ('DeleteFile' === $action) {
697
                                try {
698
                                    $name = $file->recording_type;
699
                                    $file->delete();
700
                                    Display::addFlash(
701
                                        Display::return_message($this->get_lang('FileWasDeleted').': '.$name, 'confirm')
702
                                    );
703
                                } catch (Exception $exception) {
704
                                    Display::addFlash(
705
                                        Display::return_message($exception->getMessage(), 'error')
706
                                    );
707
                                }
708
                            }
709
                        }
710
                    }
711
                }
712
                api_location($returnURL);
713
            }
714
        }
715
716
        return $form;
717
    }
718
719
    /**
720
     * Adds to the meeting course documents a link to a meeting instance recording file.
721
     *
722
     * @param Meeting       $meeting
723
     * @param RecordingFile $file
724
     * @param string        $name
725
     *
726
     * @throws Exception
727
     */
728
    public function createLinkToFileInCourse($meeting, $file, $name)
729
    {
730
        $course = $meeting->getCourse();
731
        if (null === $course) {
732
            throw new Exception('This meeting is not linked to a course');
733
        }
734
        $courseInfo = api_get_course_info_by_id($course->getId());
735
        if (empty($courseInfo)) {
736
            throw new Exception('This meeting is not linked to a valid course');
737
        }
738
        $path = '/zoom_meeting_recording_file_'.$file->id.'.'.$file->file_type;
739
        $docId = DocumentManager::addCloudLink($courseInfo, $path, $file->play_url, $name);
740
        if (!$docId) {
741
            throw new Exception(get_lang(DocumentManager::cloudLinkExists($courseInfo, $path, $file->play_url) ? 'UrlAlreadyExists' : 'ErrorAddCloudLink'));
742
        }
743
    }
744
745
    /**
746
     * Copies a recording file to a meeting's course.
747
     *
748
     * @param Meeting       $meeting
749
     * @param RecordingFile $file
750
     * @param string        $name
751
     *
752
     * @throws Exception
753
     */
754
    public function copyFileToCourse($meeting, $file, $name)
755
    {
756
        $course = $meeting->getCourse();
757
        if (null === $course) {
758
            throw new Exception('This meeting is not linked to a course');
759
        }
760
        $courseInfo = api_get_course_info_by_id($course->getId());
761
        if (empty($courseInfo)) {
762
            throw new Exception('This meeting is not linked to a valid course');
763
        }
764
        $tmpFile = tmpfile();
765
        if (false === $tmpFile) {
766
            throw new Exception('tmpfile() returned false');
767
        }
768
        $curl = curl_init($file->getFullDownloadURL($this->jwtClient->token));
769
        if (false === $curl) {
770
            throw new Exception('Could not init curl: '.curl_error($curl));
771
        }
772
        if (!curl_setopt_array(
773
            $curl,
774
            [
775
                CURLOPT_FILE => $tmpFile,
776
                CURLOPT_FOLLOWLOCATION => true,
777
                CURLOPT_MAXREDIRS => 10,
778
                CURLOPT_TIMEOUT => 120,
779
            ]
780
        )) {
781
            throw new Exception("Could not set curl options: ".curl_error($curl));
782
        }
783
        if (false === curl_exec($curl)) {
784
            throw new Exception("curl_exec failed: ".curl_error($curl));
785
        }
786
787
        $sessionId = 0;
788
        $session = $meeting->getSession();
789
        if (null !== $session) {
790
            $sessionId = $session->getId();
791
        }
792
793
        $groupId = 0;
794
        $group = $meeting->getGroup();
795
        if (null !== $group) {
796
            $groupId = $group->getIid();
797
        }
798
799
        $newPath = handle_uploaded_document(
800
            $courseInfo,
801
            [
802
                'name' => $name,
803
                'tmp_name' => stream_get_meta_data($tmpFile)['uri'],
804
                'size' => filesize(stream_get_meta_data($tmpFile)['uri']),
805
                'from_file' => true,
806
                'move_file' => true,
807
                'type' => $file->file_type,
808
            ],
809
            api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document',
810
            '/',
811
            api_get_user_id(),
812
            $groupId,
813
            null,
814
            0,
815
            'overwrite',
816
            true,
817
            false,
818
            null,
819
            $sessionId,
820
            true
821
        );
822
823
        fclose($tmpFile);
824
        if (false === $newPath) {
825
            throw new Exception('Could not handle uploaded document');
826
        }
827
    }
828
829
    /**
830
     * Generates a form to fast and easily create and start an instant meeting.
831
     * On validation, create it then redirect to it and exit.
832
     *
833
     * @return FormValidator
834
     */
835
    public function getCreateInstantMeetingForm(
836
        User $user,
837
        Course $course,
838
        CGroupInfo $group = null,
839
        Session $session = null
840
    ) {
841
        $extraUrl = '';
842
        if (!empty($course)) {
843
            $extraUrl = api_get_cidreq();
844
        }
845
        $form = new FormValidator('createInstantMeetingForm', 'post', api_get_self().'?'.$extraUrl, '_blank');
846
        $form->addButton('startButton', $this->get_lang('StartInstantMeeting'), 'video-camera', 'primary');
847
        if ($form->validate()) {
848
            try {
849
                $this->startInstantMeeting($this->get_lang('InstantMeeting'), $user, $course, $group, $session);
850
            } catch (Exception $exception) {
851
                Display::addFlash(
852
                    Display::return_message($exception->getMessage(), 'error')
853
                );
854
            }
855
        }
856
857
        return $form;
858
    }
859
860
    /**
861
     * Generates a form to schedule a meeting.
862
     * On validation, creates it and redirects to its page.
863
     *
864
     * @throws Exception
865
     *
866
     * @return FormValidator
867
     */
868
    public function getScheduleMeetingForm(User $user, Course $course = null, CGroupInfo $group = null, Session $session = null)
869
    {
870
        $extraUrl = '';
871
        if (!empty($course)) {
872
            $extraUrl = api_get_cidreq();
873
        }
874
        $form = new FormValidator('scheduleMeetingForm', 'post', api_get_self().'?'.$extraUrl);
875
        $form->addHeader($this->get_lang('ScheduleAMeeting'));
876
877
        $form->addSelect(
878
            'conference_type',
879
            $this->get_lang('ConferenceType'),
880
            [
881
                'meeting' => $this->get_lang('Meeting'),
882
                'webinar' => $this->get_lang('Webinar'),
883
            ]
884
        );
885
        $form->addRule('conference_type', get_lang('ThisFieldIsRequired'), 'required');
886
887
        $startTimeDatePicker = $form->addDateTimePicker('startTime', get_lang('StartTime'));
888
        $form->setRequired($startTimeDatePicker);
889
890
        $form->addText('topic', $this->get_lang('Topic'), true);
891
        $form->addTextarea('agenda', get_lang('Agenda'), ['maxlength' => 2000]);
892
893
        $durationNumeric = $form->addNumeric('duration', $this->get_lang('DurationInMinutes'));
894
        $form->setRequired($durationNumeric);
895
896
        if (null === $course && 'true' === $this->get('enableGlobalConference')) {
897
            $options = [];
898
            $options['everyone'] = $this->get_lang('ForEveryone');
899
            $options['registered_users'] = $this->get_lang('SomeUsers');
900
            if (!empty($options)) {
901
                if (1 === count($options)) {
902
                    $form->addHidden('type', key($options));
903
                } else {
904
                    $form->addSelect('type', $this->get_lang('AudienceType'), $options);
905
                }
906
            }
907
        } else {
908
            // To course
909
            $form->addHidden('type', 'course');
910
        }
911
912
        /*
913
       // $passwordText = $form->addText('password', get_lang('Password'), false, ['maxlength' => '10']);
914
       if (null !== $course) {
915
           $registrationOptions = [
916
               'RegisterAllCourseUsers' => $this->get_lang('RegisterAllCourseUsers'),
917
           ];
918
           $groups = GroupManager::get_groups();
919
           if (!empty($groups)) {
920
               $registrationOptions['RegisterTheseGroupMembers'] = get_lang('RegisterTheseGroupMembers');
921
           }
922
           $registrationOptions['RegisterNoUser'] = $this->get_lang('RegisterNoUser');
923
           $userRegistrationRadio = $form->addRadio(
924
               'userRegistration',
925
               $this->get_lang('UserRegistration'),
926
               $registrationOptions
927
           );
928
           $groupOptions = [];
929
           foreach ($groups as $group) {
930
               $groupOptions[$group['id']] = $group['name'];
931
           }
932
           $groupIdsSelect = $form->addSelect(
933
               'groupIds',
934
               $this->get_lang('RegisterTheseGroupMembers'),
935
               $groupOptions
936
           );
937
           $groupIdsSelect->setMultiple(true);
938
           if (!empty($groups)) {
939
               $jsCode = sprintf(
940
                   "getElementById('%s').parentNode.parentNode.parentNode.style.display = getElementById('%s').checked ? 'block' : 'none'",
941
                   $groupIdsSelect->getAttribute('id'),
942
                   $userRegistrationRadio->getelements()[1]->getAttribute('id')
943
               );
944
945
               $form->setAttribute('onchange', $jsCode);
946
           }
947
       }*/
948
949
        $form->addCheckBox('sign_attendance', $this->get_lang('SignAttendance'), get_lang('Yes'));
950
        $form->addTextarea('reason_to_sign', $this->get_lang('ReasonToSign'), ['rows' => 5]);
951
952
        $accountEmails = $this->getAccountEmails();
953
954
        if (!empty($accountEmails)) {
955
            $form->addSelect('account_email', $this->get_lang('AccountEmail'), $accountEmails);
956
        }
957
958
        $form->addButtonCreate(get_lang('Save'));
959
960
        if ($form->validate()) {
961
            $formValues = $form->exportValues();
962
            $conferenceType = $formValues['conference_type'];
963
            $password = substr(uniqid('z', true), 0, 10);
964
965
            switch ($formValues['type']) {
966
                case 'everyone':
967
                    $user = null;
968
                    $group = null;
969
                    $course = null;
970
                    $session = null;
971
972
                    break;
973
                case 'registered_users':
974
                    //$user = null;
975
                    $course = null;
976
                    $session = null;
977
978
                    break;
979
                case 'course':
980
                    $user = null;
981
                    //$course = null;
982
                    //$session = null;
983
984
                    break;
985
            }
986
987
            $accountEmail = $formValues['account_email'] ?? null;
988
            $accountEmail = $accountEmail && in_array($accountEmail, $accountEmails) ? $accountEmail : null;
989
990
            try {
991
                $startTime = new DateTime($formValues['startTime']);
992
993
                if ('meeting' === $conferenceType) {
994
                    $newMeeting = $this->createScheduleMeeting(
995
                        $user,
996
                        $course,
997
                        $group,
998
                        $session,
999
                        $startTime,
1000
                        $formValues['duration'],
1001
                        $formValues['topic'],
1002
                        $formValues['agenda'],
1003
                        $password,
1004
                        isset($formValues['sign_attendance']),
1005
                        $formValues['reason_to_sign'],
1006
                        $accountEmail
1007
                    );
1008
1009
                    Display::addFlash(
1010
                        Display::return_message($this->get_lang('NewMeetingCreated'))
1011
                    );
1012
                } elseif ('webinar' === $conferenceType) {
1013
                    $newMeeting = $this->createScheduleWebinar(
1014
                        $user,
1015
                        $course,
1016
                        $group,
1017
                        $session,
1018
                        $startTime,
1019
                        $formValues['duration'],
1020
                        $formValues['topic'],
1021
                        $formValues['agenda'],
1022
                        $password,
1023
                        isset($formValues['sign_attendance']),
1024
                        $formValues['reason_to_sign'],
1025
                        $accountEmail
1026
                    );
1027
1028
                    Display::addFlash(
1029
                        Display::return_message($this->get_lang('NewWebinarCreated'))
1030
                    );
1031
                } else {
1032
                    throw new Exception('Invalid conference type');
1033
                }
1034
1035
                if ($newMeeting->isCourseMeeting()) {
1036
                    if ('RegisterAllCourseUsers' === $form->getSubmitValue('userRegistration')) {
1037
                        $this->registerAllCourseUsers($newMeeting);
1038
                        Display::addFlash(
1039
                            Display::return_message($this->get_lang('AllCourseUsersWereRegistered'))
1040
                        );
1041
                    } elseif ('RegisterTheseGroupMembers' === $form->getSubmitValue('userRegistration')) {
1042
                        $userIds = [];
1043
                        foreach ($form->getSubmitValue('groupIds') as $groupId) {
1044
                            $userIds = array_unique(array_merge($userIds, GroupManager::get_users($groupId)));
1045
                        }
1046
                        $users = Database::getManager()->getRepository('ChamiloUserBundle:User')->findBy(
1047
                            ['id' => $userIds]
1048
                        );
1049
                        $this->registerUsers($newMeeting, $users);
1050
                        Display::addFlash(
1051
                            Display::return_message($this->get_lang('GroupUsersWereRegistered'))
1052
                        );
1053
                    }
1054
                }
1055
                api_location('meeting.php?meetingId='.$newMeeting->getMeetingId().'&'.$extraUrl);
1056
            } catch (Exception $exception) {
1057
                Display::addFlash(
1058
                    Display::return_message($exception->getMessage(), 'error')
1059
                );
1060
            }
1061
        } else {
1062
            $form->setDefaults(
1063
                [
1064
                    'duration' => 60,
1065
                    'userRegistration' => 'RegisterAllCourseUsers',
1066
                ]
1067
            );
1068
        }
1069
1070
        return $form;
1071
    }
1072
1073
    /**
1074
     * Return the current global meeting (create it if needed).
1075
     *
1076
     * @throws Exception
1077
     *
1078
     * @return string
1079
     */
1080
    public function getGlobalMeeting()
1081
    {
1082
        foreach ($this->getMeetingRepository()->unfinishedGlobalMeetings() as $meeting) {
1083
            return $meeting;
1084
        }
1085
1086
        return $this->createGlobalMeeting();
1087
    }
1088
1089
    /**
1090
     * @return MeetingRepository|EntityRepository
1091
     */
1092
    public static function getMeetingRepository()
1093
    {
1094
        return Database::getManager()->getRepository(Meeting::class);
1095
    }
1096
1097
    /**
1098
     * Returns the URL to enter (start or join) a meeting or null if not possible to enter the meeting,
1099
     * The returned URL depends on the meeting current status (waiting, started or finished) and the current user.
1100
     *
1101
     * @throws OptimisticLockException
1102
     * @throws Exception
1103
     *
1104
     * @return string|null
1105
     */
1106
    public function getStartOrJoinMeetingURL(Meeting $meeting)
1107
    {
1108
        if ($meeting instanceof Webinar) {
1109
            $status = 'started';
1110
        } else {
1111
            $status = $meeting->getMeetingInfoGet()->status;
1112
        }
1113
1114
        $userId = api_get_user_id();
1115
        $currentUser = api_get_user_entity($userId);
1116
        $isGlobal = 'true' === $this->get('enableGlobalConference') && $meeting->isGlobalMeeting();
1117
1118
        switch ($status) {
1119
            case 'ended':
1120
                if ($this->userIsConferenceManager($meeting)) {
1121
                    return $meeting->getMeetingInfoGet()->start_url;
1122
                }
1123
                break;
1124
            case 'waiting':
1125
                // Zoom does not allow for a new meeting to be started on first participant join.
1126
                // It requires the host to start the meeting first.
1127
                // Therefore for global meetings we must make the first participant the host
1128
                // that is use start_url rather than join_url.
1129
                // the participant will not be registered and will appear as the Zoom user account owner.
1130
                // For course and user meetings, only the host can start the meeting.
1131
                if ($this->userIsConferenceManager($meeting)) {
1132
                    return $meeting->getMeetingInfoGet()->start_url;
1133
                }
1134
1135
                break;
1136
            case 'started':
1137
                // User per conference.
1138
                if ($currentUser === $meeting->getUser()) {
1139
                    return $meeting instanceof Webinar
1140
                        ? $meeting->getWebinarSchema()->start_url
1141
                        : $meeting->getMeetingInfoGet()->join_url;
1142
                }
1143
1144
                // The participant is not registered, he can join only the global meeting (automatic registration).
1145
                if ($isGlobal) {
1146
                    return $this->registerUser($meeting, $currentUser)->getCreatedRegistration()->join_url;
1147
                }
1148
1149
                if ($meeting->isCourseMeeting()) {
1150
                    if ($this->userIsCourseConferenceManager()) {
1151
                        return $meeting instanceof Webinar
1152
                            ? $meeting->getWebinarSchema()->start_url
1153
                            : $meeting->getMeetingInfoGet()->start_url;
1154
                    }
1155
1156
                    $sessionId = api_get_session_id();
1157
                    $courseCode = api_get_course_id();
1158
1159
                    if (empty($sessionId)) {
1160
                        $isSubscribed = CourseManager::is_user_subscribed_in_course(
1161
                            $userId,
1162
                            $courseCode,
1163
                            false
1164
                        );
1165
                    } else {
1166
                        $isSubscribed = CourseManager::is_user_subscribed_in_course(
1167
                            $userId,
1168
                            $courseCode,
1169
                            true,
1170
                            $sessionId
1171
                        );
1172
                    }
1173
1174
                    if ($isSubscribed) {
1175
                        if ($meeting->isCourseGroupMeeting()) {
1176
                            $groupInfo = GroupManager::get_group_properties($meeting->getGroup()->getIid(), true);
1177
                            $isInGroup = GroupManager::is_user_in_group($userId, $groupInfo);
1178
                            if (false === $isInGroup) {
1179
                                throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting'));
1180
                            }
1181
                        }
1182
1183
                        if (!$meeting instanceof Webinar
1184
                            && \Chamilo\PluginBundle\Zoom\API\Meeting::TYPE_INSTANT == $meeting->getMeetingInfoGet()->type
1185
                        ) {
1186
                            return $meeting->getMeetingInfoGet()->join_url;
1187
                        }
1188
1189
                        return $this->registerUser($meeting, $currentUser)->getCreatedRegistration()->join_url;
1190
                    }
1191
1192
                    throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting'));
1193
                }
1194
1195
                //if ('true' === $this->get('enableParticipantRegistration')) {
1196
                    //if ('true' === $this->get('enableParticipantRegistration') && $meeting->requiresRegistration()) {
1197
                    // the participant must be registered
1198
                    $registrant = $meeting->getRegistrantByUser($currentUser);
1199
                    if (null == $registrant) {
1200
                        throw new Exception($this->get_lang('YouAreNotRegisteredToThisMeeting'));
1201
                    }
1202
1203
                    // the participant is registered
1204
                    return $registrant->getCreatedRegistration()->join_url;
1205
                //}
1206
                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...
1207
        }
1208
1209
        return null;
1210
    }
1211
1212
    /**
1213
     * @param Meeting $meeting
1214
     *
1215
     * @return bool whether the logged-in user can manage conferences in this context, that is either
1216
     *              the current course or session coach, the platform admin or the current course admin
1217
     */
1218
    public function userIsConferenceManager($meeting)
1219
    {
1220
        if (null === $meeting) {
1221
            return false;
1222
        }
1223
1224
        if (api_is_coach() || api_is_platform_admin()) {
1225
            return true;
1226
        }
1227
1228
        if ($meeting->isCourseMeeting() && api_get_course_id() && api_is_course_admin()) {
1229
            return true;
1230
        }
1231
1232
        return $meeting->isUserMeeting() && $meeting->getUser()->getId() == api_get_user_id();
1233
    }
1234
1235
    /**
1236
     * @return bool whether the logged-in user can manage conferences in this context, that is either
1237
     *              the current course or session coach, the platform admin or the current course admin
1238
     */
1239
    public function userIsCourseConferenceManager()
1240
    {
1241
        if (api_is_coach() || api_is_platform_admin()) {
1242
            return true;
1243
        }
1244
1245
        if (api_get_course_id() && api_is_course_admin()) {
1246
            return true;
1247
        }
1248
1249
        return false;
1250
    }
1251
1252
    /**
1253
     * Update local recording list from remote Zoom server's version.
1254
     * Kept to implement a future administration button ("import existing data from zoom server").
1255
     *
1256
     * @param DateTime $startDate
1257
     * @param DateTime $endDate
1258
     *
1259
     * @throws OptimisticLockException
1260
     * @throws Exception
1261
     */
1262
    public function reloadPeriodRecordings($startDate, $endDate)
1263
    {
1264
        $em = Database::getManager();
1265
        $recordingRepo = $this->getRecordingRepository();
1266
        $meetingRepo = $this->getMeetingRepository();
1267
        $recordings = RecordingList::loadPeriodRecordings($startDate, $endDate);
1268
1269
        foreach ($recordings as $recordingMeeting) {
1270
            $recordingEntity = $recordingRepo->findOneBy(['uuid' => $recordingMeeting->uuid]);
1271
            if (null === $recordingEntity) {
1272
                $recordingEntity = new Recording();
1273
                $meeting = $meetingRepo->findOneBy(['meetingId' => $recordingMeeting->id]);
1274
                if (null === $meeting) {
1275
                    try {
1276
                        $meetingInfoGet = MeetingInfoGet::fromId($recordingMeeting->id);
1277
                    } catch (Exception $exception) {
1278
                        $meetingInfoGet = null; // deleted meeting with recordings
1279
                    }
1280
                    if (null !== $meetingInfoGet) {
1281
                        $meeting = $this->createMeetingFromMeeting(
1282
                            (new Meeting())->setMeetingInfoGet($meetingInfoGet)
1283
                        );
1284
                        $em->persist($meeting);
1285
                    }
1286
                }
1287
                if (null !== $meeting) {
1288
                    $recordingEntity->setMeeting($meeting);
1289
                }
1290
            }
1291
            $recordingEntity->setRecordingMeeting($recordingMeeting);
1292
            $em->persist($recordingEntity);
1293
        }
1294
        $em->flush();
1295
    }
1296
1297
    /**
1298
     * @return RecordingRepository|EntityRepository
1299
     */
1300
    public static function getRecordingRepository()
1301
    {
1302
        return Database::getManager()->getRepository(Recording::class);
1303
    }
1304
1305
    public function getToolbar($returnUrl = '')
1306
    {
1307
        if (!api_is_platform_admin()) {
1308
            return '';
1309
        }
1310
1311
        $actionsLeft = '';
1312
        $back = '';
1313
        $courseId = api_get_course_id();
1314
        if (empty($courseId)) {
1315
            $actionsLeft .=
1316
                Display::url(
1317
                    Display::return_icon('bbb.png', $this->get_lang('Meetings'), null, ICON_SIZE_MEDIUM),
1318
                    api_get_path(WEB_PLUGIN_PATH).'zoom/meetings.php'
1319
                );
1320
        } else {
1321
            $actionsLeft .=
1322
                Display::url(
1323
                    Display::return_icon('bbb.png', $this->get_lang('Meetings'), null, ICON_SIZE_MEDIUM),
1324
                    api_get_path(WEB_PLUGIN_PATH).'zoom/start.php?'.api_get_cidreq()
1325
                );
1326
        }
1327
1328
        if (!empty($returnUrl)) {
1329
            $back = Display::url(
1330
                Display::return_icon('back.png', get_lang('Back'), null, ICON_SIZE_MEDIUM),
1331
                $returnUrl
1332
            );
1333
        }
1334
1335
        if (api_is_platform_admin()) {
1336
            $actionsLeft .= Display::url(
1337
                Display::return_icon('agenda.png', get_lang('Calendar'), [], ICON_SIZE_MEDIUM),
1338
                'calendar.php'
1339
            );
1340
            $actionsLeft .=
1341
                Display::url(
1342
                    Display::return_icon('settings.png', get_lang('Settings'), null, ICON_SIZE_MEDIUM),
1343
                    api_get_path(WEB_CODE_PATH).'admin/configure_plugin.php?name=zoom'
1344
                ).$back;
1345
        }
1346
1347
        return Display::toolbarAction('toolbar', [$actionsLeft]);
1348
    }
1349
1350
    public function getRecordingSetting()
1351
    {
1352
        $recording = (string) $this->get('enableCloudRecording');
1353
1354
        if (in_array($recording, [self::RECORDING_TYPE_LOCAL, self::RECORDING_TYPE_CLOUD], true)) {
1355
            return $recording;
1356
        }
1357
1358
        return self::RECORDING_TYPE_NONE;
1359
    }
1360
1361
    public function hasRecordingAvailable()
1362
    {
1363
        $recording = $this->getRecordingSetting();
1364
1365
        return self::RECORDING_TYPE_NONE !== $recording;
1366
    }
1367
1368
    /**
1369
     * @throws OptimisticLockException
1370
     * @throws \Doctrine\ORM\ORMException
1371
     */
1372
    public function saveSignature(Registrant $registrant, string $file): bool
1373
    {
1374
        if (empty($file)) {
1375
            return false;
1376
        }
1377
1378
        $signature = $registrant->getSignature();
1379
1380
        if (null !== $signature) {
1381
            return false;
1382
        }
1383
1384
        $signature = new Signature();
1385
        $signature
1386
            ->setFile($file)
1387
            ->setRegisteredAt(api_get_utc_datetime(null, false, true))
1388
        ;
1389
1390
        $registrant->setSignature($signature);
1391
1392
        $em = Database::getManager();
1393
        $em->persist($signature);
1394
        $em->flush();
1395
1396
        return true;
1397
    }
1398
1399
    public function getSignature(int $userId, Meeting $meeting): ?Signature
1400
    {
1401
        $signatureRepo = Database::getManager()
1402
            ->getRepository(Signature::class)
1403
        ;
1404
1405
        return $signatureRepo->findOneBy(['user' => $userId, 'meeting' => $meeting]);
1406
    }
1407
1408
    public function exportSignatures(Meeting $meeting, $formatToExport)
1409
    {
1410
        $signatures = array_map(
1411
            function (Registrant $registrant) use ($formatToExport) {
1412
                $signature = $registrant->getSignature();
1413
1414
                $item = [
1415
                    $registrant->getUser()->getLastname(),
1416
                    $registrant->getUser()->getFirstname(),
1417
                    $signature
1418
                        ? api_convert_and_format_date($signature->getRegisteredAt(), DATE_TIME_FORMAT_LONG)
1419
                        : '-',
1420
                ];
1421
1422
                if ('pdf' === $formatToExport) {
1423
                    $item[] = $signature
1424
                        ? Display::img($signature->getFile(), '', ['style' => 'width: 150px;'], false)
1425
                        : '-';
1426
                }
1427
1428
                return $item;
1429
            },
1430
            $meeting->getRegistrants()->toArray()
1431
        );
1432
1433
        $data = array_merge(
1434
            [
1435
                [
1436
                    get_lang('LastName'),
1437
                    get_lang('FirstName'),
1438
                    get_lang('DateTime'),
1439
                    'pdf' === $formatToExport ? get_lang('File') : null,
1440
                ],
1441
            ],
1442
            $signatures
1443
        );
1444
1445
        if ('pdf' === $formatToExport) {
1446
            $params = [
1447
                'filename' => get_lang('Attendance'),
1448
                'pdf_title' => get_lang('Attendance'),
1449
                'pdf_description' => $meeting->getIntroduction(),
1450
                'show_teacher_as_myself' => false,
1451
            ];
1452
1453
            Export::export_table_pdf($data, $params);
1454
        }
1455
1456
        if ('xls' === $formatToExport) {
1457
            $introduction = array_map(
1458
                function ($line) {
1459
                    return [
1460
                        strip_tags(trim($line)),
1461
                    ];
1462
                },
1463
                explode(PHP_EOL, $meeting->getIntroduction())
1464
            );
1465
1466
            Export::arrayToXls(
1467
                array_merge($introduction, $data),
1468
                get_lang('Attendance')
1469
            );
1470
        }
1471
    }
1472
1473
    /**
1474
     * @throws Exception
1475
     */
1476
    public function createWebinarFromSchema(Webinar $webinar, WebinarSchema $schema): Webinar
1477
    {
1478
        $currentUser = api_get_user_entity(api_get_user_id());
1479
1480
        $schema->settings->contact_email = $currentUser->getEmail();
1481
        $schema->settings->contact_name = $currentUser->getFullname();
1482
        $schema->settings->auto_recording = $this->getRecordingSetting();
1483
        $schema->settings->registrants_email_notification = false;
1484
        $schema->settings->attendees_and_panelists_reminder_email_notification->enable = false;
1485
        $schema->settings->follow_up_attendees_email_notification->enable = false;
1486
        $schema->settings->follow_up_absentees_email_notification->enable = false;
1487
1488
        $schema = $schema->create($webinar->getAccountEmail());
1489
1490
        $webinar->setWebinarSchema($schema);
1491
1492
        $em = Database::getManager();
1493
        $em->persist($webinar);
1494
        $em->flush();
1495
1496
        return $webinar;
1497
    }
1498
1499
    public function getAccountEmails(): array
1500
    {
1501
        $currentValue = $this->get('accountSelector');
1502
1503
        if (empty($currentValue)) {
1504
            return [];
1505
        }
1506
1507
        $emails = explode(';', $currentValue);
1508
        $trimmed = array_map('trim', $emails);
1509
        $filtered = array_filter($trimmed);
1510
1511
        return array_combine($filtered, $filtered);
1512
    }
1513
1514
    /**
1515
     * Register users to a meeting.
1516
     *
1517
     * @param User[] $users
1518
     *
1519
     * @throws OptimisticLockException
1520
     *
1521
     * @return User[] failed registrations [ user id => errorMessage ]
1522
     */
1523
    public function registerUsers(Meeting $meeting, array $users)
1524
    {
1525
        $failedUsers = [];
1526
        foreach ($users as $user) {
1527
            try {
1528
                $this->registerUser($meeting, $user, false);
1529
            } catch (Exception $exception) {
1530
                $failedUsers[$user->getId()] = $exception->getMessage();
1531
            }
1532
        }
1533
        Database::getManager()->flush();
1534
1535
        return $failedUsers;
1536
    }
1537
1538
    /**
1539
     * Removes registrants from a meeting.
1540
     *
1541
     * @param Registrant[] $registrants
1542
     *
1543
     * @throws Exception
1544
     */
1545
    public function unregister(Meeting $meeting, array $registrants)
1546
    {
1547
        $meetingRegistrants = [];
1548
        foreach ($registrants as $registrant) {
1549
            $meetingRegistrants[] = $registrant->getMeetingRegistrant();
1550
        }
1551
1552
        if ($meeting instanceof Webinar) {
1553
            $meeting->getWebinarSchema()->removeRegistrants($meetingRegistrants);
1554
        } else {
1555
            $meeting->getMeetingInfoGet()->removeRegistrants($meetingRegistrants);
1556
        }
1557
1558
        $em = Database::getManager();
1559
        foreach ($registrants as $registrant) {
1560
            $em->remove($registrant);
1561
        }
1562
        $em->flush();
1563
    }
1564
1565
    /**
1566
     * Updates meeting registrants list. Adds the missing registrants and removes the extra.
1567
     *
1568
     * @param Meeting $meeting
1569
     * @param User[]  $users   list of users to be registered
1570
     *
1571
     * @throws Exception
1572
     */
1573
    private function updateRegistrantList($meeting, $users)
1574
    {
1575
        $usersToAdd = [];
1576
        foreach ($users as $user) {
1577
            $found = false;
1578
            foreach ($meeting->getRegistrants() as $registrant) {
1579
                if ($registrant->getUser() === $user) {
1580
                    $found = true;
1581
                    break;
1582
                }
1583
            }
1584
            if (!$found) {
1585
                $usersToAdd[] = $user;
1586
            }
1587
        }
1588
        $registrantsToRemove = [];
1589
        foreach ($meeting->getRegistrants() as $registrant) {
1590
            $found = false;
1591
            foreach ($users as $user) {
1592
                if ($registrant->getUser() === $user) {
1593
                    $found = true;
1594
                    break;
1595
                }
1596
            }
1597
            if (!$found) {
1598
                $registrantsToRemove[] = $registrant;
1599
            }
1600
        }
1601
        $this->registerUsers($meeting, $usersToAdd);
1602
        $this->unregister($meeting, $registrantsToRemove);
1603
    }
1604
1605
    /**
1606
     * @throws Exception
1607
     * @throws OptimisticLockException
1608
     *
1609
     * @return Registrant
1610
     */
1611
    private function registerUser(Meeting $meeting, User $user, $andFlush = true)
1612
    {
1613
        if (empty($user->getEmail())) {
1614
            throw new Exception($this->get_lang('CannotRegisterWithoutEmailAddress'));
1615
        }
1616
1617
        if ($meeting instanceof Webinar) {
1618
            $meetingRegistrant = WebinarRegistrantSchema::fromEmailAndFirstName(
1619
                $user->getEmail(),
1620
                $user->getFirstname(),
1621
                $user->getLastname()
1622
            );
1623
        } else {
1624
            $meetingRegistrant = MeetingRegistrant::fromEmailAndFirstName(
1625
                $user->getEmail(),
1626
                $user->getFirstname(),
1627
                $user->getLastname()
1628
            );
1629
        }
1630
1631
        $registrantEntity = (new Registrant())
1632
            ->setMeeting($meeting)
1633
            ->setUser($user)
1634
            ->setMeetingRegistrant($meetingRegistrant)
1635
        ;
1636
1637
        if ($meeting instanceof Webinar) {
1638
            $registrantEntity->setCreatedRegistration($meeting->getWebinarSchema()->addRegistrant($meetingRegistrant));
1639
        } else {
1640
            $registrantEntity->setCreatedRegistration($meeting->getMeetingInfoGet()->addRegistrant($meetingRegistrant));
1641
        }
1642
1643
        Database::getManager()->persist($registrantEntity);
1644
1645
        if ($andFlush) {
1646
            Database::getManager()->flush($registrantEntity);
1647
        }
1648
1649
        return $registrantEntity;
1650
    }
1651
1652
    /**
1653
     * Starts a new instant meeting and redirects to its start url.
1654
     *
1655
     * @param string          $topic
1656
     * @param User|null       $user
1657
     * @param Course|null     $course
1658
     * @param CGroupInfo|null $group
1659
     * @param Session|null    $session
1660
     *
1661
     * @throws Exception
1662
     */
1663
    private function startInstantMeeting($topic, $user = null, $course = null, $group = null, $session = null)
1664
    {
1665
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_INSTANT);
1666
        //$meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE;
1667
        $meeting = $this->createMeetingFromMeeting(
1668
            (new Meeting())
1669
                ->setMeetingInfoGet($meetingInfoGet)
1670
                ->setUser($user)
1671
                ->setGroup($group)
1672
                ->setCourse($course)
1673
                ->setSession($session)
1674
        );
1675
        api_location($meeting->getMeetingInfoGet()->start_url);
1676
    }
1677
1678
    /**
1679
     * Creates a meeting on Zoom servers and stores it in the local database.
1680
     *
1681
     * @param Meeting $meeting a new, unsaved meeting with at least a type and a topic
1682
     *
1683
     * @throws Exception
1684
     *
1685
     * @return Meeting
1686
     */
1687
    private function createMeetingFromMeeting($meeting)
1688
    {
1689
        $currentUser = api_get_user_entity(api_get_user_id());
1690
1691
        $meeting->getMeetingInfoGet()->settings->contact_email = $currentUser->getEmail();
1692
        $meeting->getMeetingInfoGet()->settings->contact_name = $currentUser->getFullname();
1693
        $meeting->getMeetingInfoGet()->settings->auto_recording = $this->getRecordingSetting();
1694
        $meeting->getMeetingInfoGet()->settings->registrants_email_notification = false;
1695
1696
        //$meeting->getMeetingInfoGet()->host_email = $currentUser->getEmail();
1697
        //$meeting->getMeetingInfoGet()->settings->alternative_hosts = $currentUser->getEmail();
1698
1699
        // Send create to Zoom.
1700
        $meeting->setMeetingInfoGet(
1701
            $meeting->getMeetingInfoGet()->create(
1702
                $meeting->getAccountEmail()
1703
            )
1704
        );
1705
1706
        Database::getManager()->persist($meeting);
1707
        Database::getManager()->flush();
1708
1709
        return $meeting;
1710
    }
1711
1712
    /**
1713
     * @throws Exception
1714
     *
1715
     * @return Meeting
1716
     */
1717
    private function createGlobalMeeting()
1718
    {
1719
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType(
1720
            $this->get_lang('GlobalMeeting'),
1721
            MeetingInfoGet::TYPE_SCHEDULED
1722
        );
1723
        $meetingInfoGet->start_time = (new DateTime())->format(DATE_ATOM);
1724
        $meetingInfoGet->duration = 60;
1725
        $meetingInfoGet->settings->approval_type =
1726
            ('true' === $this->get('enableParticipantRegistration'))
1727
                ? MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE
1728
                : MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED;
1729
        // $meetingInfoGet->settings->host_video = true;
1730
        $meetingInfoGet->settings->participant_video = true;
1731
        $meetingInfoGet->settings->join_before_host = true;
1732
        $meetingInfoGet->settings->registrants_email_notification = false;
1733
1734
        return $this->createMeetingFromMeeting((new Meeting())->setMeetingInfoGet($meetingInfoGet));
1735
    }
1736
1737
    /**
1738
     * Schedules a meeting and returns it.
1739
     * set $course, $session and $user to null in order to create a global meeting.
1740
     *
1741
     * @param DateTime $startTime meeting local start date-time (configure local timezone on your Zoom account)
1742
     * @param int      $duration  in minutes
1743
     * @param string   $topic     short title of the meeting, required
1744
     * @param string   $agenda    ordre du jour
1745
     * @param string   $password  meeting password
1746
     *
1747
     * @throws Exception
1748
     *
1749
     * @return Meeting meeting
1750
     */
1751
    private function createScheduleMeeting(
1752
        User $user = null,
1753
        Course $course = null,
1754
        CGroupInfo $group = null,
1755
        Session $session = null,
1756
        $startTime,
1757
        $duration,
1758
        $topic,
1759
        $agenda,
1760
        $password,
1761
        bool $signAttendance = false,
1762
        string $reasonToSignAttendance = '',
1763
        string $accountEmail = null
1764
    ) {
1765
        $meetingInfoGet = MeetingInfoGet::fromTopicAndType($topic, MeetingInfoGet::TYPE_SCHEDULED);
1766
        $meetingInfoGet->duration = $duration;
1767
        $meetingInfoGet->start_time = $startTime->format(DATE_ATOM);
1768
        $meetingInfoGet->agenda = $agenda;
1769
        $meetingInfoGet->password = $password;
1770
        $meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_NO_REGISTRATION_REQUIRED;
1771
        if ('true' === $this->get('enableParticipantRegistration')) {
1772
            $meetingInfoGet->settings->approval_type = MeetingSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE;
1773
        }
1774
1775
        return $this->createMeetingFromMeeting(
1776
            (new Meeting())
1777
                ->setMeetingInfoGet($meetingInfoGet)
1778
                ->setUser($user)
1779
                ->setCourse($course)
1780
                ->setGroup($group)
1781
                ->setSession($session)
1782
                ->setSignAttendance($signAttendance)
1783
                ->setReasonToSignAttendance($reasonToSignAttendance)
1784
                ->setAccountEmail($accountEmail)
1785
        );
1786
    }
1787
1788
    /**
1789
     * @throws Exception
1790
     */
1791
    private function createScheduleWebinar(
1792
        ?User $user,
1793
        ?Course $course,
1794
        ?CGroupInfo $group,
1795
        ?Session $session,
1796
        DateTime $startTime,
1797
        $duration,
1798
        $topic,
1799
        $agenda,
1800
        $password,
1801
        bool $signAttendance = false,
1802
        string $reasonToSignAttendance = '',
1803
        string $accountEmail = null
1804
    ): Webinar {
1805
        $webinarSchema = WebinarSchema::fromTopicAndType($topic);
1806
        $webinarSchema->duration = $duration;
1807
        $webinarSchema->start_time = $startTime->format(DATE_ATOM);
1808
        $webinarSchema->agenda = $agenda;
1809
        $webinarSchema->password = $password;
1810
1811
        if ('true' === $this->get('enableParticipantRegistration')) {
1812
            $webinarSchema->settings->approval_type = WebinarSettings::APPROVAL_TYPE_AUTOMATICALLY_APPROVE;
1813
        }
1814
1815
        $webinar = (new Webinar())
1816
            ->setUser($user)
1817
            ->setCourse($course)
1818
            ->setGroup($group)
1819
            ->setSession($session)
1820
            ->setSignAttendance($signAttendance)
1821
            ->setReasonToSignAttendance($reasonToSignAttendance)
1822
            ->setAccountEmail($accountEmail)
1823
        ;
1824
1825
        return $this->createWebinarFromSchema($webinar, $webinarSchema);
1826
    }
1827
1828
    /**
1829
     * Registers all the course users to a course meeting.
1830
     *
1831
     * @param Meeting $meeting
1832
     *
1833
     * @throws OptimisticLockException
1834
     */
1835
    private function registerAllCourseUsers($meeting)
1836
    {
1837
        $this->registerUsers($meeting, $meeting->getRegistrableUsers());
1838
    }
1839
}
1840