Passed
Push — 1.11.x ( b4ce57...e435f5 )
by Julito
09:28
created

ZoomPlugin::scheduleMeeting()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 15
c 0
b 0
f 0
nc 2
nop 9
dl 0
loc 28
rs 9.7666

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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