Passed
Pull Request — 1.11.x (#4697)
by Angel Fernando Quiroz
11:38 queued 03:22
created

ZoomPlugin::currentUserCanJoinGlobalMeeting()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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