Passed
Push — 1.11.x ( 8c4a90...9feff2 )
by Julito
13:24
created

ZoomPlugin::createGlobalMeeting()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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