Passed
Pull Request — master (#6739)
by
unknown
08:30
created

Bbb::isGlobalConferenceEnabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\ConferenceActivity;
6
use Chamilo\CoreBundle\Entity\ConferenceMeeting;
7
use Chamilo\CoreBundle\Entity\ConferenceRecording;
8
use Chamilo\CoreBundle\Enums\ActionIcon;
9
use Chamilo\CoreBundle\Enums\ObjectIcon;
10
use Chamilo\CoreBundle\Enums\StateIcon;
11
use Chamilo\CoreBundle\Repository\ConferenceActivityRepository;
12
use Chamilo\CoreBundle\Repository\ConferenceMeetingRepository;
13
use Chamilo\CoreBundle\Repository\ConferenceRecordingRepository;
14
use Chamilo\UserBundle\Entity\User;
15
16
/**
17
 * Class Bbb
18
 * This script initiates a video conference session, calling the BigBlueButton
19
 * API BigBlueButton-Chamilo connector class
20
 */
21
class Bbb
22
{
23
    public $url;
24
    public $salt;
25
    public $api;
26
    public $userCompleteName = '';
27
    public $protocol = 'http://';
28
    public $debug = false;
29
    public $logoutUrl = '';
30
    public $pluginEnabled = false;
31
    public $enableGlobalConference = false;
32
    public $enableGlobalConferencePerUser = false;
33
    public $isGlobalConference = false;
34
    public $groupSupport = false;
35
    public $userSupport = false;
36
    public $accessUrl = 1;
37
    public $userId = 0;
38
    public $plugin;
39
    private $courseCode;
40
    private $courseId;
41
    private $sessionId;
42
    private $groupId;
43
    private $maxUsersLimit;
44
    private $urlWithProtocol = '';
45
46
    /**
47
     * Constructor (generates a connection to the API and the Chamilo settings
48
     * required for the connection to the video conference server)
49
     *
50
     * @param string $host
51
     * @param string $salt
52
     * @param bool   $isGlobalConference
53
     * @param int    $isGlobalPerUser
54
     */
55
    public function __construct(
56
        $host = '',
57
        $salt = '',
58
        $isGlobalConference = false,
59
        $isGlobalPerUser = 0
60
    ) {
61
        $this->courseCode = api_get_course_id();
62
        $this->courseId = api_get_course_int_id();
63
        $this->sessionId = api_get_session_id();
64
        $this->groupId = api_get_group_id();
65
66
        // Initialize video server settings from global settings
67
        $this->plugin = BbbPlugin::create();
68
        $bbbPluginEnabled = $this->plugin->get('tool_enable');
69
70
        $bbb_host = !empty($host) ? $host : $this->plugin->get('host');
71
        $bbb_salt = !empty($salt) ? $salt : $this->plugin->get('salt');
72
73
        $this->table = Database::get_main_table('conference_meeting');
74
        $this->enableGlobalConference = $this->plugin->get('enable_global_conference') === 'true';
75
        $this->isGlobalConference = (bool) $isGlobalConference;
76
77
        $columns = Database::listTableColumns($this->table);
78
        $this->groupSupport = isset($columns['group_id']) ? true : false;
79
        $this->userSupport = isset($columns['user_id']) ? true : false;
80
        $this->accessUrl = api_get_current_access_url_id();
81
82
        $this->enableGlobalConferencePerUser = false;
83
        if ($this->userSupport && !empty($isGlobalPerUser)) {
84
            $this->enableGlobalConferencePerUser = $this->plugin->get('enable_global_conference_per_user') === 'true';
85
            $this->userId = $isGlobalPerUser;
86
        }
87
88
        if ($this->groupSupport) {
89
            // Plugin check
90
            $this->groupSupport = $this->plugin->get('enable_conference_in_course_groups') === 'true' ? true : false;
91
            if ($this->groupSupport) {
92
                // Platform check
93
                $bbbSetting = api_get_plugin_setting('bbb', 'enable_conference_in_course_groups') === 'true';
94
                if ($bbbSetting) {
95
                    // Course check
96
                    $courseInfo = api_get_course_info();
97
                    if ($courseInfo) {
98
                        $this->groupSupport = api_get_course_plugin_setting(
99
                                'bbb',
100
                                'bbb_enable_conference_in_groups',
101
                                $courseInfo
102
                            ) === '1';
103
                    }
104
                }
105
            }
106
        }
107
        $this->maxUsersLimit = $this->plugin->get('max_users_limit');
108
109
        if ($bbbPluginEnabled === 'true') {
110
            $userInfo = api_get_user_info();
111
            if (empty($userInfo) && !empty($isGlobalPerUser)) {
112
                // If we are following a link to a global "per user" conference
113
                // then generate a random guest name to join the conference
114
                // because there is no part of the process where we give a name
115
                //$this->userCompleteName = 'Guest'.rand(1000, 9999);
116
            } else {
117
                $this->userCompleteName = $userInfo['complete_name'];
118
            }
119
120
            if (api_is_anonymous()) {
121
                $this->userCompleteName = get_lang('Guest').'_'.rand(1000, 9999);
122
            }
123
124
            $this->salt = $bbb_salt;
125
            if (!empty($bbb_host)) {
126
                if (substr($bbb_host, -1, 1) !== '/') {
127
                    $bbb_host .= '/';
128
                }
129
                $this->url = $bbb_host;
130
                if (!preg_match('#/bigbluebutton/$#', $bbb_host)) {
131
                    $this->url = $bbb_host.'bigbluebutton/';
132
                }
133
            }
134
            $info = parse_url($bbb_host);
135
136
            if (isset($info['scheme'])) {
137
                $this->protocol = $info['scheme'].'://';
138
                $this->url = str_replace($this->protocol, '', $this->url);
139
                $urlWithProtocol = $bbb_host;
140
            } else {
141
                // We assume it's an http, if user wants to use https, the host *must* include the protocol.
142
                $this->protocol = 'http://';
143
                $urlWithProtocol = $this->protocol.$bbb_host;
144
            }
145
146
            // Normalize final URL with protocol to the /bigbluebutton/ endpoint
147
            if (!str_ends_with($urlWithProtocol, '/bigbluebutton/')) {
148
                $urlWithProtocol = rtrim($urlWithProtocol, '/').'/bigbluebutton/';
149
            }
150
            $this->urlWithProtocol = $urlWithProtocol;
151
152
            // Define constants safely (optional legacy compatibility)
153
            if (!defined('CONFIG_SERVER_BASE_URL')) {
154
                define('CONFIG_SERVER_BASE_URL', $this->url);
155
            }
156
            if (!defined('CONFIG_SERVER_PROTOCOL')) {
157
                define('CONFIG_SERVER_PROTOCOL', $this->protocol);
158
            }
159
            if (!defined('CONFIG_SERVER_URL_WITH_PROTOCOL')) {
160
                define('CONFIG_SERVER_URL_WITH_PROTOCOL', $this->urlWithProtocol);
161
            }
162
            if (!defined('CONFIG_SECURITY_SALT')) {
163
                // IMPORTANT: use $this->salt, not $this->securitySalt
164
                define('CONFIG_SECURITY_SALT', (string) $this->salt);
165
            }
166
167
            // Initialize API object only if we have both URL and salt
168
            if (!empty($this->urlWithProtocol) && !empty($this->salt)) {
169
                $this->api = new BigBlueButtonBN($this->urlWithProtocol, $this->salt);
170
                $this->pluginEnabled = true;
171
            } else {
172
                $this->api = null;
173
                $this->pluginEnabled = false;
174
            }
175
176
            $this->logoutUrl = $this->getListingUrl();
177
        }
178
    }
179
180
    private function ensureApi(): bool
181
    {
182
        if ($this->api instanceof BigBlueButtonBN) {
183
            return true;
184
        }
185
        $url = $this->urlWithProtocol ?: (defined('CONFIG_SERVER_URL_WITH_PROTOCOL') ? constant('CONFIG_SERVER_URL_WITH_PROTOCOL') : '');
186
        $salt = $this->salt ?: (defined('CONFIG_SECURITY_SALT') ? constant('CONFIG_SECURITY_SALT') : '');
187
188
        if ($url && $salt) {
189
            $this->api = new BigBlueButtonBN($url, $salt);
190
            return $this->api instanceof BigBlueButtonBN;
191
        }
192
193
        return false;
194
    }
195
196
    /**
197
     * @param int $courseId  Optional. Course ID.
198
     * @param int $sessionId Optional. Session ID.
199
     * @param int $groupId   Optional. Group ID.
200
     *
201
     * @return string
202
     */
203
    public function getListingUrl($courseId = 0, $sessionId = 0, $groupId = 0)
204
    {
205
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'
206
            .$this->getUrlParams($courseId, $sessionId, $groupId);
207
    }
208
209
    /**
210
     * @param int $courseId  Optional. Course ID.
211
     * @param int $sessionId Optional. Session ID.
212
     * @param int $groupId   Optional. Group ID.
213
     *
214
     * @return string
215
     */
216
    public function getUrlParams($courseId = 0, $sessionId = 0, $groupId = 0)
217
    {
218
        if (empty($this->courseId) && !$courseId) {
219
            if ($this->isGlobalConferencePerUserEnabled()) {
220
                return 'global=1&user_id='.$this->userId;
221
            }
222
223
            if ($this->isGlobalConference()) {
224
                return 'global=1';
225
            }
226
227
            return '';
228
        }
229
230
        $defaultCourseId = (int) $this->courseId;
231
        if (!empty($courseId)) {
232
            $defaultCourseId = (int) $courseId;
233
        }
234
235
        return http_build_query(
236
            [
237
                'cid' => $defaultCourseId,
238
                'sid' => (int) $sessionId ?: $this->sessionId,
239
                'gid' => (int) $groupId ?: $this->groupId,
240
            ]
241
        );
242
    }
243
244
    /**
245
     * @return bool
246
     */
247
    public function isGlobalConferencePerUserEnabled()
248
    {
249
        return $this->enableGlobalConferencePerUser;
250
    }
251
252
    /**
253
     * @return bool
254
     */
255
    public function isGlobalConference()
256
    {
257
        if ($this->isGlobalConferenceEnabled() === false) {
258
            return false;
259
        }
260
261
        return (bool) $this->isGlobalConference;
262
    }
263
264
    /**
265
     * @return bool
266
     */
267
    public function isGlobalConferenceEnabled()
268
    {
269
        return $this->enableGlobalConference;
270
    }
271
272
    /**
273
     * @param array $userInfo
274
     *
275
     * @return bool
276
     */
277
    private static function normalizeSettingToArray($value): array
278
    {
279
        if (is_array($value)) {
280
            return $value;
281
        }
282
        if (is_string($value)) {
283
            $value = trim($value);
284
            if ($value === '') {
285
                return [];
286
            }
287
            $decoded = json_decode($value, true);
288
            if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
289
                return $decoded;
290
            }
291
            return array_values(
292
                array_filter(
293
                    array_map('trim', explode(',', $value)),
294
                    'strlen'
295
                )
296
            );
297
        }
298
        return [];
299
    }
300
301
    /**
302
     * Gets the global limit of users in a video-conference room.
303
     * This value can be overridden by course-specific values
304
     * @return  int Maximum number of users set globally
305
     */
306
    public function getMaxUsersLimit()
307
    {
308
        $limit = $this->maxUsersLimit;
309
        if ($limit <= 0) {
310
            $limit = 0;
311
        }
312
        $courseLimit = 0;
313
        $sessionLimit = 0;
314
315
        // Check course extra field
316
        if (!empty($this->courseId)) {
317
            $extraField = new ExtraField('course');
318
            $fieldIdList = $extraField->get_all(
319
                array('variable = ?' => 'plugin_bbb_course_users_limit')
320
            );
321
322
            if (!empty($fieldIdList)) {
323
                $fieldId = $fieldIdList[0]['id'] ?? null;
324
                if ($fieldId) {
325
                    $extraValue = new ExtraFieldValue('course');
326
                    $value = $extraValue->get_values_by_handler_and_field_id($this->courseId, $fieldId);
327
                    if (!empty($value['value'])) {
328
                        $courseLimit = (int) $value['value'];
329
                    }
330
                }
331
            }
332
        }
333
334
        // Check session extra field
335
        if (!empty($this->sessionId)) {
336
            $extraField = new ExtraField('session');
337
            $fieldIdList = $extraField->get_all(
338
                array('variable = ?' => 'plugin_bbb_session_users_limit')
339
            );
340
341
            if (!empty($fieldIdList)) {
342
                $fieldId = $fieldIdList[0]['id'] ?? null;
343
                if ($fieldId) {
344
                    $extraValue = new ExtraFieldValue('session');
345
                    $value = $extraValue->get_values_by_handler_and_field_id($this->sessionId, $fieldId);
346
                    if (!empty($value['value'])) {
347
                        $sessionLimit = (int) $value['value'];
348
                    }
349
                }
350
            }
351
        }
352
353
        if (!empty($sessionLimit)) {
354
            return $sessionLimit;
355
        } elseif (!empty($courseLimit)) {
356
            return $courseLimit;
357
        }
358
359
        return (int) $limit;
360
    }
361
362
    /**
363
     * Sets the global limit of users in a video-conference room.
364
     *
365
     * @param int Maximum number of users (globally)
366
     */
367
    public function setMaxUsersLimit($max)
368
    {
369
        if ($max < 0) {
370
            $max = 0;
371
        }
372
        $this->maxUsersLimit = (int) $max;
373
    }
374
375
    /**
376
     * See this file in you BBB to set up default values
377
     *
378
     * @param array $params Array of parameters that will be completed if not containing all expected variables
379
     *
380
     * /var/lib/tomcat6/webapps/bigbluebutton/WEB-INF/classes/bigbluebutton.properties
381
     *
382
     * More record information:
383
     * http://code.google.com/p/bigbluebutton/wiki/RecordPlaybackSpecification
384
     *
385
     * Default maximum number of users a meeting can have.
386
     * Doesn't get enforced yet but is the default value when the create
387
     * API doesn't pass a value.
388
     * defaultMaxUsers=20
389
     *
390
     * Default duration of the meeting in minutes.
391
     * Current default is 0 (meeting doesn't end).
392
     * defaultMeetingDuration=0
393
     *
394
     * Remove the meeting from memory when the end API is called.
395
     * This allows 3rd-party apps to recycle the meeting right-away
396
     * instead of waiting for the meeting to expire (see below).
397
     * removeMeetingWhenEnded=false
398
     *
399
     * The number of minutes before the system removes the meeting from memory.
400
     * defaultMeetingExpireDuration=1
401
     *
402
     * The number of minutes the system waits when a meeting is created and when
403
     * a user joins. If after this period, a user hasn't joined, the meeting is
404
     * removed from memory.
405
     * defaultMeetingCreateJoinDuration=5
406
     *
407
     * @return mixed
408
     */
409
    public function createMeeting($params)
410
    {
411
        // Ensure API is available
412
        if (!$this->ensureApi()) {
413
            if ($this->debug) {
414
                error_log('BBB API not initialized in createMeeting().');
415
            }
416
            return false;
417
        }
418
419
        // Normalize input
420
        $params = is_array($params) ? $params : [];
421
422
        // Context defaults
423
        $params['c_id']       = $params['c_id']       ?? api_get_course_int_id();
424
        $params['session_id'] = $params['session_id'] ?? api_get_session_id();
425
426
        if ($this->hasGroupSupport()) {
427
            $params['group_id'] = $params['group_id'] ?? api_get_group_id();
428
        }
429
430
        if ($this->isGlobalConferencePerUserEnabled() && !empty($this->userId)) {
431
            $params['user_id'] = (int) $this->userId;
432
        }
433
434
        // Passwords
435
        $params['attendee_pw']  = $params['attendee_pw']  ?? $this->getUserMeetingPassword();
436
        $params['moderator_pw'] = $params['moderator_pw'] ?? $this->getModMeetingPassword();
437
        $attendeePassword  = (string) $params['attendee_pw'];
438
        $moderatorPassword = (string) $params['moderator_pw'];
439
440
        // Recording and limits
441
        $params['record'] = api_get_course_plugin_setting('bbb', 'big_blue_button_record_and_store') == 1 ? 1 : 0;
442
        $max = api_get_course_plugin_setting('bbb', 'big_blue_button_max_students_allowed');
443
        $max = isset($max) ? (int) $max : -1;
444
445
        // Meeting identifiers
446
        $params['status']     = 1;
447
        $params['remote_id']  = $params['remote_id']  ?? uniqid(true, true);
448
        $params['voice_bridge']= $params['voice_bridge'] ?? rand(10000, 99999);
449
        $params['created_at'] = $params['created_at'] ?? api_get_utc_datetime();
450
        $params['access_url'] = $params['access_url'] ?? $this->accessUrl;
451
452
        // Persist meeting entity
453
        $em = Database::getManager();
454
        $meeting = new ConferenceMeeting();
455
456
        $meeting
457
            ->setCourse(api_get_course_entity($params['c_id']))
458
            ->setSession(api_get_session_entity($params['session_id']))
459
            ->setAccessUrl(api_get_url_entity($params['access_url']))
460
            ->setGroup($this->hasGroupSupport() ? api_get_group_entity($params['group_id']) : null)
461
            ->setUser($this->isGlobalConferencePerUserEnabled() ? api_get_user_entity($params['user_id'] ?? 0) : null)
462
            ->setRemoteId($params['remote_id'])
463
            ->setTitle($params['meeting_name'] ?? $this->getCurrentVideoConferenceName())
464
            ->setAttendeePw($attendeePassword)
465
            ->setModeratorPw($moderatorPassword)
466
            ->setRecord((bool) $params['record'])
467
            ->setStatus($params['status'])
468
            ->setVoiceBridge($params['voice_bridge'])
469
            ->setWelcomeMsg($params['welcome_msg'] ?? null)
470
            ->setVisibility(1)
471
            ->setHasVideoM4v(false)
472
            ->setServiceProvider('bbb');
473
474
        $em->persist($meeting);
475
        $em->flush();
476
477
        $id = $meeting->getId();
478
479
        Event::addEvent(
0 ignored issues
show
Bug introduced by
The method addEvent() does not exist on Event. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

479
        Event::/** @scrutinizer ignore-call */ 
480
               addEvent(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
480
            'bbb_create_meeting',
481
            'meeting_id',
482
            $id,
483
            null,
484
            api_get_user_id(),
485
            api_get_course_int_id(),
486
            api_get_session_id()
487
        );
488
489
        // Compute BBB params
490
        $meetingName = $meeting->getTitle();
491
        $record      = $meeting->isRecord() ? 'true' : 'false';
492
        $duration    = 300;
493
        $meetingDuration = (int) $this->plugin->get('meeting_duration');
494
        if (!empty($meetingDuration)) {
495
            $duration = $meetingDuration;
496
        }
497
498
        // Normalize optional "documents" to avoid undefined index warnings
499
        $documents = [];
500
        if (!empty($params['documents']) && is_array($params['documents'])) {
501
            foreach ($params['documents'] as $doc) {
502
                if (!is_array($doc)) {
503
                    continue;
504
                }
505
                $url = $doc['url'] ?? null;
506
                if (!$url) {
507
                    continue;
508
                }
509
                $documents[] = [
510
                    'url'          => (string) $url,
511
                    'filename'     => $doc['filename'] ?? (basename(parse_url($url, PHP_URL_PATH) ?: 'document')),
512
                    'downloadable' => array_key_exists('downloadable', $doc) ? (bool) $doc['downloadable'] : true,
513
                    'removable'    => array_key_exists('removable', $doc) ? (bool) $doc['removable'] : true,
514
                ];
515
            }
516
        }
517
518
        $bbbParams = [
519
            'meetingId'       => $meeting->getRemoteId(),
520
            'meetingName'     => $meetingName,
521
            'attendeePw'      => $attendeePassword,
522
            'moderatorPw'     => $moderatorPassword,
523
            'welcomeMsg'      => $meeting->getWelcomeMsg(),
524
            'dialNumber'      => '',
525
            'voiceBridge'     => $meeting->getVoiceBridge(),
526
            'webVoice'        => '',
527
            'logoutUrl'       => $this->logoutUrl . '&action=logout&remote_id=' . $meeting->getRemoteId(),
528
            'maxParticipants' => $max,
529
            'record'          => $record,
530
            'duration'        => $duration,
531
            'documents'       => $documents, // safe default (empty array) if none provided
532
        ];
533
534
        // Try to create meeting until success (kept as in original logic)
535
        $status = false;
536
        while ($status === false) {
537
            $result = $this->api->createMeetingWithXmlResponseArray($bbbParams);
538
539
            if ((string)($result['returncode'] ?? '') === 'SUCCESS') {
540
                if ($this->plugin->get('allow_regenerate_recording') === 'true' && !empty($result['internalMeetingID'])) {
541
                    $meeting->setInternalMeetingId($result['internalMeetingID']);
542
                    $em->flush();
543
                }
544
545
                // Register webhook if enabled (no DB tables, pure BBB hooks API)
546
                try {
547
                    if ($this->plugin->webhooksEnabled()) {
548
                        $scope = $this->plugin->webhooksScope(); // 'per_meeting' | 'global'
549
                        if ($scope === 'per_meeting') {
550
                            $this->registerWebhookForMeeting($meeting->getRemoteId());
551
                        } else {
552
                            // idempotent: checks existing hooks/list before creating
553
                            $this->ensureGlobalWebhook();
554
                        }
555
                    }
556
                } catch (\Throwable $e) {
557
                    if ($this->debug) {
558
                        error_log('BBB webhook registration failed: '.$e->getMessage());
559
                    }
560
                    // Do not block meeting start on webhook issues
561
                }
562
563
                return $this->joinMeeting($meetingName, true);
564
            }
565
566
            if ((string)($result['returncode'] ?? '') === 'FAILED') {
567
                if ((int)($result['httpCode'] ?? 0) === 413) {
568
                    if ($this->debug) {
569
                        error_log('BBB createMeeting failed (413): payload too large');
570
                    }
571
                } else if ($this->debug) {
572
                    error_log('BBB createMeeting failed: '.json_encode($result));
573
                }
574
                break;
575
            }
576
577
            if ($this->debug) {
578
                error_log('BBB createMeeting unexpected response: '.print_r($result, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($result, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

578
                error_log('BBB createMeeting unexpected response: './** @scrutinizer ignore-type */ print_r($result, true));
Loading history...
579
            }
580
            break;
581
        }
582
583
        return false;
584
    }
585
586
    /**
587
     * @return bool
588
     */
589
    public function hasGroupSupport()
590
    {
591
        return $this->groupSupport;
592
    }
593
594
    /**
595
     * Gets the password for a specific meeting for the current user
596
     *
597
     * @param string $courseCode
598
     *
599
     * @return string A moderator password if user is teacher, or the course code otherwise
600
     *
601
     */
602
    public function getUserMeetingPassword($courseCode = null)
603
    {
604
        if ($this->isGlobalConferencePerUserEnabled()) {
605
            return 'url_'.$this->userId.'_'.api_get_current_access_url_id();
606
        }
607
608
        if ($this->isGlobalConference()) {
609
            return 'url_'.api_get_current_access_url_id();
610
        }
611
612
        return empty($courseCode) ? api_get_course_id() : $courseCode;
613
    }
614
615
    /**
616
     * Generated a moderator password for the meeting.
617
     *
618
     * @param string $courseCode
619
     *
620
     * @return string A password for the moderation of the videoconference
621
     */
622
    public function getModMeetingPassword($courseCode = null)
623
    {
624
        if ($this->isGlobalConferencePerUserEnabled()) {
625
            return 'url_'.$this->userId.'_'.api_get_current_access_url_id().'_mod';
626
        }
627
628
        if ($this->isGlobalConference()) {
629
            return 'url_'.api_get_current_access_url_id().'_mod';
630
        }
631
632
        $courseCode = empty($courseCode) ? api_get_course_id() : $courseCode;
633
634
        return $courseCode.'mod';
635
    }
636
637
    /**
638
     * @return string
639
     */
640
    public function getCurrentVideoConferenceName()
641
    {
642
        if ($this->isGlobalConferencePerUserEnabled()) {
643
            return 'url_'.$this->userId.'_'.api_get_current_access_url_id();
644
        }
645
646
        if ($this->isGlobalConference()) {
647
            return 'url_'.api_get_current_access_url_id();
648
        }
649
650
        if ($this->hasGroupSupport()) {
651
            return api_get_course_id().'-'.api_get_session_id().'-'.api_get_group_id();
652
        }
653
654
        return api_get_course_id().'-'.api_get_session_id();
655
    }
656
657
    /**
658
     * Returns a meeting "join" URL
659
     *
660
     * @param string The name of the meeting (usually the course code)
661
     *
662
     * @return mixed The URL to join the meeting, or false on error
663
     * @todo implement moderator pass
664
     * @assert ('') === false
665
     * @assert ('abcdefghijklmnopqrstuvwxyzabcdefghijklmno') === false
666
     */
667
    public function joinMeeting($meetingName)
668
    {
669
        if (!$this->ensureApi()) {
670
            if ($this->debug) {
671
                error_log('BBB API not initialized in joinMeeting().');
672
            }
673
            return false;
674
        }
675
        if ($this->debug) {
676
            error_log("joinMeeting: $meetingName");
677
        }
678
679
        if (empty($meetingName)) {
680
            return false;
681
        }
682
683
        $manager = $this->isConferenceManager();
684
        $pass = $manager ? $this->getModMeetingPassword() : $this->getUserMeetingPassword();
685
686
        $meetingData = Database::getManager()
687
            ->getRepository(ConferenceMeeting::class)
688
            ->findOneBy([
689
                'title' => $meetingName,
690
                'status' => 1,
691
                'accessUrl' => api_get_url_entity($this->accessUrl),
692
            ]);
693
694
        if (empty($meetingData)) {
695
            if ($this->debug) {
696
                error_log("meeting does not exist: $meetingName");
697
            }
698
699
            return false;
700
        }
701
702
        $params = [
703
            'meetingId' => $meetingData->getRemoteId(),
704
            'password' => $this->getModMeetingPassword(),
705
        ];
706
707
        $meetingInfoExists = false;
708
        $meetingIsRunningInfo = $this->getMeetingInfo($params);
709
        if ($this->debug) {
710
            error_log('Searching meeting with params:');
711
            error_log(print_r($params, 1));
712
            error_log('Result:');
713
            error_log(print_r($meetingIsRunningInfo, 1));
714
        }
715
716
        if ($meetingIsRunningInfo === false) {
717
            $params['meetingId'] = $meetingData->getId();
718
            $meetingIsRunningInfo = $this->getMeetingInfo($params);
719
            if ($this->debug) {
720
                error_log('Searching meetingId with params:');
721
                error_log(print_r($params, 1));
722
                error_log('Result:');
723
                error_log(print_r($meetingIsRunningInfo, 1));
724
            }
725
        }
726
727
        if (
728
            strval($meetingIsRunningInfo['returncode']) === 'SUCCESS' &&
729
            isset($meetingIsRunningInfo['meetingName']) &&
730
            !empty($meetingIsRunningInfo['meetingName'])
731
        ) {
732
            $meetingInfoExists = true;
733
        }
734
735
        if ($this->debug) {
736
            error_log("meeting is running: " . intval($meetingInfoExists));
737
        }
738
739
        if ($meetingInfoExists) {
740
            $joinParams = [
741
                'meetingId' => $meetingData->getRemoteId(),
742
                'username' => $this->userCompleteName,
743
                'password' => $pass,
744
                'userID' => api_get_user_id(),
745
                'moderatorPw' => $this->getModMeetingPassword(),
746
                'userID'      => api_get_user_id(),
747
                'webVoiceConf' => '',
748
            ];
749
            $url = $this->api->getJoinMeetingURL($joinParams);
750
            if (preg_match('#^https?://#i', $url)) {
751
                return $url;
752
            }
753
754
            return $this->protocol . $url;
755
        }
756
757
        return false;
758
    }
759
760
761
    /**
762
     * Checks whether a user is teacher in the current course
763
     * @return bool True if the user can be considered a teacher in this course, false otherwise
764
     */
765
    public function isConferenceManager()
766
    {
767
        if (api_is_coach() || api_is_platform_admin(false, true)) {
768
            return true;
769
        }
770
771
        if ($this->isGlobalConferencePerUserEnabled()) {
772
            $currentUserId = api_get_user_id();
773
            if ($this->userId === $currentUserId) {
774
                return true;
775
            } else {
776
                return false;
777
            }
778
        }
779
780
        $courseInfo = api_get_course_info();
781
        $groupId = api_get_group_id();
782
        if (!empty($groupId) && !empty($courseInfo)) {
783
            $groupEnabled = api_get_course_plugin_setting('bbb', 'bbb_enable_conference_in_groups') === '1';
784
            if ($groupEnabled) {
785
                $studentCanStartConference = api_get_course_plugin_setting(
786
                        'bbb',
787
                        'big_blue_button_students_start_conference_in_groups'
788
                    ) === '1';
789
790
                if ($studentCanStartConference) {
791
                    $isSubscribed = GroupManager::is_user_in_group(
0 ignored issues
show
Bug introduced by
The method is_user_in_group() does not exist on GroupManager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

791
                    /** @scrutinizer ignore-call */ 
792
                    $isSubscribed = GroupManager::is_user_in_group(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
792
                        api_get_user_id(),
793
                        GroupManager::get_group_properties($groupId)
794
                    );
795
                    if ($isSubscribed) {
796
                        return true;
797
                    }
798
                }
799
            }
800
        }
801
802
        if (!empty($courseInfo)) {
803
            return api_is_course_admin();
804
        }
805
806
        return false;
807
    }
808
809
    /**
810
     * Get information about the given meeting
811
     *
812
     * @param array ...?
813
     *
814
     * @return mixed Array of information on success, false on error
815
     * @assert (array()) === false
816
     */
817
    public function getMeetingInfo($params)
818
    {
819
        try {
820
            // Guard against null API
821
            if (!$this->ensureApi()) {
822
                if ($this->debug) {
823
                    error_log('BBB API not initialized (missing URL or salt).');
824
                }
825
                return false;
826
            }
827
828
            $result = $this->api->getMeetingInfoWithXmlResponseArray($params);
829
            if ($result == null) {
830
                if ($this->debug) {
831
                    error_log("Failed to get any response. Maybe we can't contact the BBB server.");
832
                }
833
                return false;
834
            }
835
836
            return $result;
837
        } catch (\Throwable $e) {
838
            if ($this->debug) {
839
                error_log('BBB getMeetingInfo error: '.$e->getMessage());
840
            }
841
            return false;
842
        }
843
    }
844
845
846
    /**
847
     * @param int $meetingId
848
     * @param int $userId
849
     *
850
     * @return array
851
     */
852
    public function getMeetingParticipantInfo($meetingId, $userId): array
853
    {
854
        $em = Database::getManager();
855
        /** @var ConferenceActivityRepository $repo */
856
        $repo = $em->getRepository(ConferenceActivity::class);
857
858
        $activity = $repo->createQueryBuilder('a')
859
            ->join('a.meeting', 'm')
860
            ->join('a.participant', 'u')
861
            ->where('m.id = :meetingId')
862
            ->andWhere('u.id = :userId')
863
            ->setParameter('meetingId', $meetingId)
864
            ->setParameter('userId', $userId)
865
            ->setMaxResults(1)
866
            ->getQuery()
867
            ->getOneOrNullResult();
868
869
        if (!$activity) {
870
            return [];
871
        }
872
873
        return [
874
            'id' => $activity->getId(),
875
            'meeting_id' => $activity->getMeeting()?->getId(),
876
            'participant_id' => $activity->getParticipant()?->getId(),
877
            'in_at' => $activity->getInAt()?->format('Y-m-d H:i:s'),
878
            'out_at' => $activity->getOutAt()?->format('Y-m-d H:i:s'),
879
            'close' => $activity->isClose(),
880
            'type' => $activity->getType(),
881
            'event' => $activity->getEvent(),
882
            'activity_data' => $activity->getActivityData(),
883
            'signature_file' => $activity->getSignatureFile(),
884
            'signed_at' => $activity->getSignedAt()?->format('Y-m-d H:i:s'),
885
        ];
886
    }
887
888
    /**
889
     * Save a participant in a meeting room
890
     *
891
     * @param int $meetingId
892
     * @param int $participantId
893
     *
894
     * @return false|int The last inserted ID. Otherwise return false
895
     */
896
    public function saveParticipant(int $meetingId, int $participantId): false|int
897
    {
898
        $em = Database::getManager();
899
900
        /** @var ConferenceActivityRepository $repo */
901
        $repo = $em->getRepository(ConferenceActivity::class);
902
903
        $meeting = $em->getRepository(ConferenceMeeting::class)->find($meetingId);
904
        $user = api_get_user_entity($participantId);
905
906
        if (!$meeting || !$user) {
907
            return false;
908
        }
909
910
        $existing = $repo->createQueryBuilder('a')
911
            ->where('a.meeting = :meeting')
912
            ->andWhere('a.participant = :participant')
913
            ->andWhere('a.close = :open')
914
            ->setParameter('meeting', $meeting)
915
            ->setParameter('participant', $user)
916
            ->setParameter('open', \BbbPlugin::ROOM_OPEN)
917
            ->getQuery()
918
            ->getResult();
919
920
        foreach ($existing as $activity) {
921
            if ($activity->getInAt() != $activity->getOutAt()) {
922
                $activity->setClose(\BbbPlugin::ROOM_CLOSE);
923
            } else {
924
                $activity->setOutAt(new \DateTime());
925
                $activity->setClose(\BbbPlugin::ROOM_CLOSE);
926
            }
927
            $em->persist($activity);
928
        }
929
930
        $newActivity = new ConferenceActivity();
931
        $newActivity->setMeeting($meeting);
932
        $newActivity->setParticipant($user);
933
        $newActivity->setInAt(new \DateTime());
934
        $newActivity->setOutAt(new \DateTime());
935
        $newActivity->setClose(\BbbPlugin::ROOM_OPEN);
936
937
        $em->persist($newActivity);
938
        $em->flush();
939
940
        return $newActivity->getId();
941
    }
942
943
    /**
944
     * Tells whether the given meeting exists and is running
945
     * (using course code as name)
946
     *
947
     * @param string $meetingName Meeting name (usually the course code)
948
     *
949
     * @return bool True if meeting exists, false otherwise
950
     * @assert ('') === false
951
     * @assert ('abcdefghijklmnopqrstuvwxyzabcdefghijklmno') === false
952
     */
953
    public function meetingExists($meetingName)
954
    {
955
        $meetingData = $this->getMeetingByName($meetingName);
956
957
        return !empty($meetingData);
958
    }
959
960
    /**
961
     * @param string $meetingName
962
     *
963
     * @return array
964
     */
965
    public function getMeetingByName($meetingName)
966
    {
967
        if (empty($meetingName)) {
968
            return [];
969
        }
970
971
        $courseEntity = api_get_course_entity();
972
        $sessionEntity = api_get_session_entity();
973
        $accessUrlEntity = api_get_url_entity($this->accessUrl);
974
975
        $criteria = [
976
            'course' => $courseEntity,
977
            'session' => $sessionEntity,
978
            'title' => $meetingName,
979
            'status' => 1,
980
            'accessUrl' => $accessUrlEntity,
981
        ];
982
983
        if ($this->hasGroupSupport()) {
984
            $groupEntity = api_get_group_entity(api_get_group_id());
985
            $criteria['group'] = $groupEntity;
986
        }
987
988
        $meeting = Database::getManager()
989
            ->getRepository(ConferenceMeeting::class)
990
            ->findOneBy($criteria);
991
992
        if ($this->debug) {
993
            error_log('meeting_exists '.print_r($meeting ? ['id' => $meeting->getId()] : [], 1));
0 ignored issues
show
Bug introduced by
Are you sure print_r($meeting ? array...>getId()) : array(), 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

993
            error_log('meeting_exists './** @scrutinizer ignore-type */ print_r($meeting ? ['id' => $meeting->getId()] : [], 1));
Loading history...
994
        }
995
996
        if (!$meeting) {
997
            return [];
998
        }
999
1000
        return [
1001
            'id' => $meeting->getId(),
1002
            'c_id' => $meeting->getCourse()?->getId(),
1003
            'session_id' => $meeting->getSession()?->getId(),
1004
            'meeting_name' => $meeting->getTitle(),
1005
            'status' => $meeting->getStatus(),
1006
            'access_url' => $meeting->getAccessUrl()?->getId(),
1007
            'group_id' => $meeting->getGroup()?->getIid(),
1008
            'remote_id' => $meeting->getRemoteId(),
1009
            'moderator_pw' => $meeting->getModeratorPw(),
1010
            'attendee_pw' => $meeting->getAttendeePw(),
1011
            'created_at' => $meeting->getCreatedAt()->format('Y-m-d H:i:s'),
1012
            'closed_at' => $meeting->getClosedAt()?->format('Y-m-d H:i:s'),
1013
            'visibility' => $meeting->getVisibility(),
1014
            'video_url' => $meeting->getVideoUrl(),
1015
            'has_video_m4v' => $meeting->isHasVideoM4v(),
1016
            'record' => $meeting->isRecord(),
1017
            'internal_meeting_id' => $meeting->getInternalMeetingId(),
1018
        ];
1019
    }
1020
1021
    /**
1022
     * Gets a list from the database of all meetings attached to a course with the given status
1023
     * @param int $courseId
1024
     * @param int $sessionId
1025
     * @param int $status 0 for closed meetings, 1 for open meetings
1026
     *
1027
     * @return array
1028
     */
1029
    public function getAllMeetingsInCourse($courseId, $sessionId, $status)
1030
    {
1031
        $em = Database::getManager();
1032
        $courseEntity = api_get_course_entity($courseId);
1033
        $sessionEntity = api_get_session_entity($sessionId);
1034
1035
        $meetings = $em->getRepository(ConferenceMeeting::class)->findBy([
1036
            'course' => $courseEntity,
1037
            'session' => $sessionEntity,
1038
            'status' => $status,
1039
        ]);
1040
1041
        $results = [];
1042
        foreach ($meetings as $meeting) {
1043
            $results[] = [
1044
                'id' => $meeting->getId(),
1045
                'c_id' => $meeting->getCourse()?->getId(),
1046
                'session_id' => $meeting->getSession()?->getId(),
1047
                'meeting_name' => $meeting->getTitle(),
1048
                'status' => $meeting->getStatus(),
1049
                'access_url' => $meeting->getAccessUrl()?->getId(),
1050
                'group_id' => $meeting->getGroup()?->getIid(),
1051
                'remote_id' => $meeting->getRemoteId(),
1052
                'moderator_pw' => $meeting->getModeratorPw(),
1053
                'attendee_pw' => $meeting->getAttendeePw(),
1054
                'created_at' => $meeting->getCreatedAt()->format('Y-m-d H:i:s'),
1055
                'closed_at' => $meeting->getClosedAt()?->format('Y-m-d H:i:s'),
1056
                'visibility' => $meeting->getVisibility(),
1057
                'video_url' => $meeting->getVideoUrl(),
1058
                'has_video_m4v' => $meeting->isHasVideoM4v(),
1059
                'record' => $meeting->isRecord(),
1060
                'internal_meeting_id' => $meeting->getInternalMeetingId(),
1061
            ];
1062
        }
1063
1064
        return $results;
1065
    }
1066
1067
    /**
1068
     * Gets all the course meetings saved in the plugin_bbb_meeting table and
1069
     * generate actionable links (join/close/delete/etc)
1070
     *
1071
     * @param int   $courseId
1072
     * @param int   $sessionId
1073
     * @param int   $groupId
1074
     * @param bool  $isAdminReport Optional. Set to true then the report is for admins
1075
     * @param array $dateRange     Optional
1076
     *
1077
     * @return array Array of current open meeting rooms
1078
     * @throws Exception
1079
     */
1080
    public function getMeetings(
1081
        $courseId = 0,
1082
        $sessionId = 0,
1083
        $groupId = 0,
1084
        $isAdminReport = false,
1085
        $dateRange = []
1086
    ) {
1087
        if (!$this->ensureApi()) {
1088
            if ($this->debug) {
1089
                error_log('BBB API not initialized in getMeetings().');
1090
            }
1091
            return [];
1092
        }
1093
1094
        $em = Database::getManager();
1095
        $repo = $em->getRepository(ConferenceMeeting::class);
1096
        $manager = $this->isConferenceManager();
1097
        $isGlobal = $this->isGlobalConference();
1098
        $meetings = [];
1099
1100
        if (!empty($dateRange)) {
1101
            $dateStart = (new \DateTime($dateRange['search_meeting_start']))->setTime(0, 0, 0);
1102
            $dateEnd = (new \DateTime($dateRange['search_meeting_end']))->setTime(23, 59, 59);
1103
            $meetings = $repo->findByDateRange($dateStart, $dateEnd);
1104
        } elseif ($this->isGlobalConference()) {
1105
            $meetings = $repo->findBy([
1106
                'course' => null,
1107
                'user' => api_get_user_entity($this->userId),
1108
                'accessUrl' => api_get_url_entity($this->accessUrl),
1109
            ]);
1110
        } elseif ($this->isGlobalConferencePerUserEnabled()) {
1111
            $meetings = $repo->findBy([
1112
                'course' => api_get_course_entity($courseId),
1113
                'session' => api_get_session_entity($sessionId),
1114
                'user' => api_get_user_entity($this->userId),
1115
                'accessUrl' => api_get_url_entity($this->accessUrl),
1116
            ]);
1117
        } else {
1118
            $criteria = [
1119
                'course' => api_get_course_entity($courseId),
1120
                'session' => api_get_session_entity($sessionId),
1121
                'accessUrl' => api_get_url_entity($this->accessUrl),
1122
            ];
1123
            if ($this->hasGroupSupport() && $groupId) {
1124
                $criteria['group'] = api_get_group_entity($groupId);
1125
            }
1126
            $meetings = $repo->findBy($criteria, ['createdAt' => 'ASC']);
1127
        }
1128
1129
        $result = [];
1130
        foreach ($meetings as $meeting) {
1131
            $meetingArray = $this->convertMeetingToArray($meeting);
1132
            $recordLink = $this->plugin->get_lang('NoRecording');
1133
            $meetingBBB = $this->getMeetingInfo([
1134
                'meetingId' => $meeting->getRemoteId(),
1135
                'password' => $manager ? $meeting->getModeratorPw() : $meeting->getAttendeePw(),
1136
            ]);
1137
1138
            if (!$meetingBBB && $meeting->getId()) {
1139
                $meetingBBB = $this->getMeetingInfo([
1140
                    'meetingId' => $meeting->getId(),
1141
                    'password' => $manager ? $meeting->getModeratorPw() : $meeting->getAttendeePw(),
1142
                ]);
1143
            }
1144
1145
            if (!$meeting->isVisible() && !$manager) {
1146
                continue;
1147
            }
1148
1149
            $meetingBBB['end_url'] = $this->endUrl(['id' => $meeting->getId()]);
1150
            if (isset($meetingBBB['returncode']) && (string) $meetingBBB['returncode'] === 'FAILED') {
1151
                if ($meeting->getStatus() === 1 && $manager) {
1152
                    $this->endMeeting($meeting->getId(), $meeting->getCourse()?->getCode());
1153
                }
1154
            } else {
1155
                $meetingBBB['add_to_calendar_url'] = $this->addToCalendarUrl($meetingArray);
1156
            }
1157
1158
            if ($meeting->isRecord()) {
1159
                $recordings = $this->api->getRecordingsWithXmlResponseArray(['meetingId' => $meeting->getRemoteId()]);
1160
                if (!empty($recordings) && (!isset($recordings['messageKey']) || $recordings['messageKey'] !== 'noRecordings')) {
1161
                    $record = end($recordings);
1162
                    if (isset($record['playbackFormatUrl'])) {
1163
                        $recordLink = Display::url(
1164
                            $this->plugin->get_lang('ViewRecord'),
1165
                            $record['playbackFormatUrl'],
1166
                            ['target' => '_blank', 'class' => 'btn btn--plain']
1167
                        );
1168
                        $this->updateMeetingVideoUrl($meeting->getId(), $record['playbackFormatUrl']);
1169
                    }
1170
                }
1171
            }
1172
1173
            $actionLinks = $this->getActionLinks($meetingArray, $record ?? [], $isGlobal, $isAdminReport);
1174
1175
            $item = array_merge($meetingArray, [
1176
                'go_url' => '',
1177
                'show_links' => $recordLink,
1178
                'action_links' => implode(PHP_EOL, $actionLinks),
1179
                'publish_url' => $this->publishUrl(['id' => $meeting->getId()]),
1180
                'unpublish_url' => $this->unPublishUrl(['id' => $meeting->getId()]),
1181
            ]);
1182
1183
            if ($meeting->getStatus() === 1) {
1184
                $joinParams = [
1185
                    'meetingId' => $meeting->getRemoteId(),
1186
                    'username' => $this->userCompleteName,
1187
                    'password' => $manager ? $meeting->getModeratorPw() : $meeting->getAttendeePw(),
1188
                    'createTime' => '',
1189
                    'userID' => '',
1190
                    'webVoiceConf' => '',
1191
                ];
1192
                $item['go_url'] = $this->protocol.$this->api->getJoinMeetingURL($joinParams);
1193
            }
1194
1195
            $result[] = array_merge($item, $meetingBBB);
1196
        }
1197
1198
        return $result;
1199
    }
1200
1201
    private function convertMeetingToArray(ConferenceMeeting $meeting): array
1202
    {
1203
        return [
1204
            'id' => $meeting->getId(),
1205
            'remote_id' => $meeting->getRemoteId(),
1206
            'internal_meeting_id' => $meeting->getInternalMeetingId(),
1207
            'meeting_name' => $meeting->getTitle(),
1208
            'status' => $meeting->getStatus(),
1209
            'visibility' => $meeting->getVisibility(),
1210
            'created_at' => $meeting->getCreatedAt() instanceof \DateTime ? $meeting->getCreatedAt()->format('Y-m-d H:i:s') : '',
0 ignored issues
show
introduced by
$meeting->getCreatedAt() is always a sub-type of DateTime.
Loading history...
1211
            'closed_at' => $meeting->getClosedAt() instanceof \DateTime ? $meeting->getClosedAt()->format('Y-m-d H:i:s') : '',
1212
            'record' => $meeting->isRecord() ? 1 : 0,
1213
            'c_id' => $meeting->getCourse()?->getId() ?? 0,
1214
            'session_id' => $meeting->getSession()?->getId() ?? 0,
1215
            'group_id' => $meeting->getGroup()?->getIid() ?? 0,
1216
            'course' => $meeting->getCourse(),
1217
            'session' => $meeting->getSession(),
1218
            'title' => $meeting->getTitle(),
1219
        ];
1220
    }
1221
1222
    public function getMeetingsLight(
1223
        $courseId = 0,
1224
        $sessionId = 0,
1225
        $groupId = 0,
1226
        $dateRange = []
1227
    ): array {
1228
        $em = Database::getManager();
1229
        $repo = $em->getRepository(ConferenceMeeting::class);
1230
        $meetings = [];
1231
1232
        if (!empty($dateRange)) {
1233
            $dateStart = (new \DateTime($dateRange['search_meeting_start']))->setTime(0, 0, 0);
1234
            $dateEnd = (new \DateTime($dateRange['search_meeting_end']))->setTime(23, 59, 59);
1235
            $meetings = $repo->findByDateRange($dateStart, $dateEnd);
1236
        } else {
1237
            $criteria = [
1238
                'course' => api_get_course_entity($courseId),
1239
                'session' => api_get_session_entity($sessionId),
1240
                'accessUrl' => api_get_url_entity($this->accessUrl),
1241
            ];
1242
            if ($this->hasGroupSupport() && $groupId) {
1243
                $criteria['group'] = api_get_group_entity($groupId);
1244
            }
1245
            $meetings = $repo->findBy($criteria, ['createdAt' => 'DESC']);
1246
        }
1247
1248
        $result = [];
1249
        foreach ($meetings as $meeting) {
1250
            $meetingArray = $this->convertMeetingToArray($meeting);
1251
1252
            $item = array_merge($meetingArray, [
1253
                'go_url' => '',
1254
                'show_links' => $this->plugin->get_lang('NoRecording'),
1255
                'action_links' => '',
1256
                'publish_url' => $this->publishUrl(['id' => $meeting->getId()]),
1257
                'unpublish_url' => $this->unPublishUrl(['id' => $meeting->getId()]),
1258
            ]);
1259
1260
            $result[] = $item;
1261
        }
1262
1263
        return $result;
1264
    }
1265
1266
    /**
1267
     * @param array $meeting
1268
     *
1269
     * @return string
1270
     */
1271
    public function endUrl($meeting)
1272
    {
1273
        if (!isset($meeting['id'])) {
1274
            return '';
1275
        }
1276
1277
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams().'&action=end&id='.$meeting['id'];
1278
    }
1279
1280
    /**
1281
     * Closes a meeting (usually when the user click on the close button from
1282
     * the conferences listing.
1283
     *
1284
     * @param string The internal ID of the meeting (id field for this meeting)
1285
     * @param string $courseCode
1286
     *
1287
     * @return void
1288
     * @assert (0) === false
1289
     */
1290
    public function endMeeting($id, $courseCode = null)
1291
    {
1292
        if (empty($id)) {
1293
            return false;
1294
        }
1295
1296
        $em = Database::getManager();
1297
1298
        /** @var ConferenceMeetingRepository $repo */
1299
        $repo = $em->getRepository(ConferenceMeeting::class);
1300
1301
        $meetingData = $repo->findOneAsArrayById((int) $id);
1302
        if (!$meetingData) {
1303
            return false;
1304
        }
1305
1306
        $manager = $this->isConferenceManager();
1307
        $pass = $manager ? $meetingData['moderatorPw'] : $meetingData['attendeePw'];
1308
1309
        Event::addEvent(
1310
            'bbb_end_meeting',
1311
            'meeting_id',
1312
            (int) $id,
1313
            null,
1314
            api_get_user_id(),
1315
            api_get_course_int_id(),
1316
            api_get_session_id()
1317
        );
1318
1319
        $endParams = [
1320
            'meetingId' => $meetingData['remoteId'],
1321
            'password' => $pass,
1322
        ];
1323
        $this->api->endMeetingWithXmlResponseArray($endParams);
1324
1325
        $repo->closeMeeting((int) $id, new \DateTime());
1326
1327
        /** @var ConferenceActivityRepository $activityRepo */
1328
        $activityRepo = $em->getRepository(ConferenceActivity::class);
1329
1330
        $activities = $activityRepo->findOpenWithSameInAndOutTime((int) $id);
1331
1332
        foreach ($activities as $activity) {
1333
            $activity->setOutAt(new \DateTime());
1334
            $activity->setClose(BbbPlugin::ROOM_CLOSE);
1335
            $em->persist($activity);
1336
        }
1337
1338
        $activityRepo->closeAllByMeetingId((int) $id);
1339
1340
        $em->flush();
1341
1342
        return true;
1343
    }
1344
1345
    /**
1346
     * @param array $meeting
1347
     * @param array $record
1348
     *
1349
     * @return string
1350
     */
1351
    public function addToCalendarUrl($meeting, $record = []): string
1352
    {
1353
        $url = isset($record['playbackFormatUrl']) ? $record['playbackFormatUrl'] : '';
1354
1355
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams(
1356
            ).'&action=add_to_calendar&id='.$meeting['id'].'&start='.api_strtotime($meeting['created_at']).'&url='.$url;
1357
    }
1358
1359
    /**
1360
     * @param int    $meetingId
1361
     * @param string $videoUrl
1362
     *
1363
     * @return bool|int
1364
     */
1365
    public function updateMeetingVideoUrl(int $meetingId, string $videoUrl): void
1366
    {
1367
        $em = Database::getManager();
1368
        /** @var ConferenceMeetingRepository $repo */
1369
        $repo = $em->getRepository(ConferenceMeeting::class);
1370
        $repo->updateVideoUrl($meetingId, $videoUrl);
1371
    }
1372
1373
    /**
1374
     * Force the course, session and/or group IDs
1375
     *
1376
     * @param string $courseCode
1377
     * @param int    $sessionId
1378
     * @param int    $groupId
1379
     */
1380
    public function forceCIdReq($courseCode, $sessionId = 0, $groupId = 0)
1381
    {
1382
        $this->courseCode = $courseCode;
1383
        $this->sessionId = (int) $sessionId;
1384
        $this->groupId = (int) $groupId;
1385
    }
1386
1387
    /**
1388
     * @param array $meetingInfo
1389
     * @param array $recordInfo
1390
     * @param bool  $isGlobal
1391
     * @param bool  $isAdminReport
1392
     *
1393
     * @return array
1394
     */
1395
    private function getActionLinks(
1396
        $meetingInfo,
1397
        $recordInfo,
1398
        $isGlobal = false,
1399
        $isAdminReport = false
1400
    ) {
1401
        $isVisible = $meetingInfo['visibility'] != 0;
1402
        $linkVisibility = $isVisible
1403
            ? Display::url(
1404
                Display::getMdiIcon(StateIcon::ACTIVE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Make invisible')),
1405
                $this->unPublishUrl($meetingInfo)
1406
            )
1407
            : Display::url(
1408
                Display::getMdiIcon(StateIcon::INACTIVE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Make visible')),
1409
                $this->publishUrl($meetingInfo)
1410
            );
1411
1412
        $links = [];
1413
        if ($this->plugin->get('allow_regenerate_recording') === 'true' && $meetingInfo['record'] == 1) {
1414
            if (!empty($recordInfo)) {
1415
                $links[] = Display::url(
1416
                    Display::getMdiIcon(ActionIcon::REFRESH, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('RegenerateRecord')),
1417
                    $this->regenerateRecordUrl($meetingInfo, $recordInfo)
1418
                );
1419
            } else {
1420
                $links[] = Display::url(
1421
                    Display::getMdiIcon(ActionIcon::REFRESH, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('RegenerateRecord')),
1422
                    $this->regenerateRecordUrlFromMeeting($meetingInfo)
1423
                );
1424
            }
1425
        }
1426
1427
        if (empty($recordInfo)) {
1428
            if (!$isAdminReport) {
1429
                if ($meetingInfo['status'] == 0) {
1430
                    $links[] = Display::url(
1431
                        Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')),
1432
                        $this->deleteRecordUrl($meetingInfo)
1433
                    );
1434
                    $links[] = $linkVisibility;
1435
                }
1436
1437
                return $links;
1438
            } else {
1439
                $links[] = Display::url(
1440
                    Display::getMdiIcon(ObjectIcon::HOME, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Go to the course')),
1441
                    $this->getListingUrl($meetingInfo['c_id'], $meetingInfo['session_id'], $meetingInfo['group_id'])
1442
                );
1443
1444
                return $links;
1445
            }
1446
        }
1447
1448
        if (!$isGlobal) {
1449
            $links[] = Display::url(
1450
                Display::getMdiIcon(ObjectIcon::LINK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('UrlMeetingToShare')),
1451
                $this->copyToRecordToLinkTool($meetingInfo)
1452
            );
1453
            $links[] = Display::url(
1454
                Display::getMdiIcon(ObjectIcon::AGENDA, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Add to calendar')),
1455
                $this->addToCalendarUrl($meetingInfo, $recordInfo)
1456
            );
1457
        }
1458
1459
        $hide = $this->plugin->get('disable_download_conference_link') === 'true' ? true : false;
1460
1461
        if ($hide == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1462
            if ($meetingInfo['has_video_m4v']) {
1463
                $links[] = Display::url(
1464
                    Display::getMdiIcon(ActionIcon::SAVE_FORM, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Download file')),
1465
                    $recordInfo['playbackFormatUrl'].'/capture.m4v',
1466
                    ['target' => '_blank']
1467
                );
1468
            } else {
1469
                $links[] = Display::url(
1470
                    Display::getMdiIcon(ActionIcon::SAVE_FORM, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Download file')),
1471
                    '#',
1472
                    [
1473
                        'id' => "btn-check-meeting-video-{$meetingInfo['id']}",
1474
                        'class' => 'check-meeting-video',
1475
                        'data-id' => $meetingInfo['id'],
1476
                    ]
1477
                );
1478
            }
1479
        }
1480
1481
1482
        if (!$isAdminReport) {
1483
            $links[] = Display::url(
1484
                Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')),
1485
                $this->deleteRecordUrl($meetingInfo)
1486
            );
1487
            $links[] = $linkVisibility;
1488
        } else {
1489
            $links[] = Display::url(
1490
                Display::getMdiIcon(ObjectIcon::HOME, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Go to the course')),
1491
                $this->getListingUrl($meetingInfo['c_id'], $meetingInfo['session_id'], $meetingInfo['group_id'])
1492
            );
1493
        }
1494
1495
1496
        return $links;
1497
    }
1498
1499
    /**
1500
     * @param array $meeting
1501
     *
1502
     * @return string
1503
     */
1504
    public function unPublishUrl($meeting)
1505
    {
1506
        if (!isset($meeting['id'])) {
1507
            return null;
1508
        }
1509
1510
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams(
1511
            ).'&action=unpublish&id='.$meeting['id'];
1512
    }
1513
1514
    /**
1515
     * @param array $meeting
1516
     *
1517
     * @return string
1518
     */
1519
    public function publishUrl($meeting)
1520
    {
1521
        if (!isset($meeting['id'])) {
1522
            return '';
1523
        }
1524
1525
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams(
1526
            ).'&action=publish&id='.$meeting['id'];
1527
    }
1528
1529
    /**
1530
     * @param array $meeting
1531
     * @param array $recordInfo
1532
     *
1533
     * @return string
1534
     */
1535
    public function regenerateRecordUrl($meeting, $recordInfo)
1536
    {
1537
        if ($this->plugin->get('allow_regenerate_recording') !== 'true') {
1538
            return '';
1539
        }
1540
1541
        if (!isset($meeting['id'])) {
1542
            return '';
1543
        }
1544
1545
        if (empty($recordInfo) || (!empty($recordInfo['recordId']) && !isset($recordInfo['recordId']))) {
1546
            return '';
1547
        }
1548
1549
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams().
1550
            '&action=regenerate_record&id='.$meeting['id'].'&record_id='.$recordInfo['recordId'];
1551
    }
1552
1553
    /**
1554
     * @param array $meeting
1555
     *
1556
     * @return string
1557
     */
1558
    public function regenerateRecordUrlFromMeeting($meeting)
1559
    {
1560
        if ($this->plugin->get('allow_regenerate_recording') !== 'true') {
1561
            return '';
1562
        }
1563
1564
        if (!isset($meeting['id'])) {
1565
            return '';
1566
        }
1567
1568
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams().
1569
            '&action=regenerate_record&id='.$meeting['id'];
1570
    }
1571
1572
    /**
1573
     * @param array $meeting
1574
     *
1575
     * @return string
1576
     */
1577
    public function deleteRecordUrl($meeting)
1578
    {
1579
        if (!isset($meeting['id'])) {
1580
            return '';
1581
        }
1582
1583
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams(
1584
            ).'&action=delete_record&id='.$meeting['id'];
1585
    }
1586
1587
    /**
1588
     * @param array $meeting
1589
     *
1590
     * @return string
1591
     */
1592
    public function copyToRecordToLinkTool($meeting)
1593
    {
1594
        if (!isset($meeting['id'])) {
1595
            return '';
1596
        }
1597
1598
        return api_get_path(WEB_PLUGIN_PATH).
1599
            'Bbb/listing.php?'.$this->getUrlParams().'&action=copy_record_to_link_tool&id='.$meeting['id'];
1600
    }
1601
1602
    /**
1603
     * Function disabled
1604
     */
1605
    public function publishMeeting($id)
1606
    {
1607
        if (empty($id)) {
1608
            return false;
1609
        }
1610
1611
        $em = Database::getManager();
1612
        /** @var ConferenceMeetingRepository $repo */
1613
        $repo = $em->getRepository(ConferenceMeeting::class);
1614
1615
        $meeting = $repo->find($id);
1616
        if (!$meeting) {
1617
            return false;
1618
        }
1619
1620
        $meeting->setVisibility(1);
1621
        $em->flush();
1622
1623
        return true;
1624
    }
1625
1626
    /**
1627
     * Function disabled
1628
     */
1629
    public function unpublishMeeting($id)
1630
    {
1631
        if (empty($id)) {
1632
            return false;
1633
        }
1634
1635
        $em = Database::getManager();
1636
        /** @var ConferenceMeetingRepository $repo */
1637
        $repo = $em->getRepository(ConferenceMeeting::class);
1638
1639
        $meeting = $repo->find($id);
1640
        if (!$meeting) {
1641
            return false;
1642
        }
1643
1644
        $meeting->setVisibility(0);
1645
        $em->flush();
1646
1647
        return true;
1648
    }
1649
1650
    /**
1651
     * Get users online in the current course room.
1652
     *
1653
     * @return int The number of users currently connected to the videoconference
1654
     * @assert () > -1
1655
     */
1656
    public function getUsersOnlineInCurrentRoom()
1657
    {
1658
        $courseId = api_get_course_int_id();
1659
        $sessionId = api_get_session_id();
1660
1661
        $em = Database::getManager();
1662
        $repo = $em->getRepository(ConferenceMeeting::class);
1663
1664
        $qb = $repo->createQueryBuilder('m')
1665
            ->where('m.status = 1')
1666
            ->andWhere('m.accessUrl = :accessUrl')
1667
            ->setParameter('accessUrl', $this->accessUrl)
1668
            ->setMaxResults(1);
1669
1670
        if ($this->hasGroupSupport()) {
1671
            $groupId = api_get_group_id();
1672
            $qb->andWhere('m.course = :courseId')
1673
                ->andWhere('m.session = :sessionId')
1674
                ->andWhere('m.group = :groupId')
1675
                ->setParameter('courseId', $courseId)
1676
                ->setParameter('sessionId', $sessionId)
1677
                ->setParameter('groupId', $groupId);
1678
        } elseif ($this->isGlobalConferencePerUserEnabled()) {
1679
            $qb->andWhere('m.user = :userId')
1680
                ->setParameter('userId', $this->userId);
1681
        } else {
1682
            $qb->andWhere('m.course = :courseId')
1683
                ->andWhere('m.session = :sessionId')
1684
                ->setParameter('courseId', $courseId)
1685
                ->setParameter('sessionId', $sessionId);
1686
        }
1687
1688
        $meetingData = $qb->getQuery()->getOneOrNullResult();
1689
1690
        if (!$meetingData) {
1691
            return 0;
1692
        }
1693
        $pass = $meetingData->getModeratorPw();
1694
        $info = $this->getMeetingInfo([
1695
            'meetingId' => $meetingData->getRemoteId(),
1696
            'password' => $pass,
1697
        ]);
1698
        if ($info === false) {
1699
            $info = $this->getMeetingInfo([
1700
                'meetingId' => $meetingData->getId(),
1701
                'password' => $pass,
1702
            ]);
1703
        }
1704
1705
        if (!empty($info) && isset($info['participantCount'])) {
1706
            return (int) $info['participantCount'];
1707
        }
1708
1709
        return 0;
1710
    }
1711
1712
    /**
1713
     * @param int    $id
1714
     * @param string $recordId
1715
     *
1716
     * @return bool
1717
     */
1718
    public function regenerateRecording($id, $recordId = '')
1719
    {
1720
        if ($this->plugin->get('allow_regenerate_recording') !== 'true') {
1721
            return false;
1722
        }
1723
1724
        if (empty($id)) {
1725
            return false;
1726
        }
1727
1728
        $em = Database::getManager();
1729
        /** @var ConferenceMeetingRepository $repo */
1730
        $repo = $em->getRepository(ConferenceMeeting::class);
1731
1732
        $meetingData = $repo->findOneAsArrayById((int) $id);
1733
        if (!$meetingData) {
1734
            return false;
1735
        }
1736
1737
        Event::addEvent(
1738
            'bbb_regenerate_record',
1739
            'record_id',
1740
            (int) $recordId,
1741
            null,
1742
            api_get_user_id(),
1743
            api_get_course_int_id(),
1744
            api_get_session_id()
1745
        );
1746
1747
        /** @var ConferenceRecordingRepository $recordingRepo */
1748
        $recordingRepo = $em->getRepository(ConferenceRecordingRepository::class);
1749
        $recordings = $recordingRepo->findByMeetingRemoteId($meetingData['remoteId']);
1750
1751
        if (!empty($recordings) && isset($recordings['messageKey']) && $recordings['messageKey'] === 'noRecordings') {
1752
            if (!empty($meetingData['internalMeetingId'])) {
1753
                return $this->api->generateRecording(['recordId' => $meetingData['internalMeetingId']]);
1754
            }
1755
1756
            return false;
1757
        }
1758
1759
        if (!empty($recordings['records'])) {
1760
            foreach ($recordings['records'] as $record) {
1761
                if ($recordId == $record['recordId']) {
1762
                    return $this->api->generateRecording(['recordId' => $recordId]);
1763
                }
1764
            }
1765
        }
1766
1767
        return false;
1768
    }
1769
1770
    /**
1771
     * Deletes a recording of a meeting
1772
     *
1773
     * @param int $id ID of the recording
1774
     *
1775
     * @return bool
1776
     *
1777
     * @assert () === false
1778
     * @todo Also delete links and agenda items created from this recording
1779
     */
1780
    public function deleteRecording($id)
1781
    {
1782
        if (empty($id)) {
1783
            return false;
1784
        }
1785
1786
        $em = Database::getManager();
1787
1788
        /** @var ConferenceMeetingRepository $meetingRepo */
1789
        $meetingRepo = $em->getRepository(ConferenceMeeting::class);
1790
        $meetingData = $meetingRepo->findOneAsArrayById((int) $id);
1791
        if (!$meetingData) {
1792
            return false;
1793
        }
1794
1795
        Event::addEvent(
1796
            'bbb_delete_record',
1797
            'meeting_id',
1798
            $id,
1799
            null,
1800
            api_get_user_id(),
1801
            api_get_course_int_id(),
1802
            api_get_session_id()
1803
        );
1804
1805
        $delete = false;
1806
        $recordings = [];
1807
1808
        if (!empty($meetingData['remoteId'])) {
1809
            Event::addEvent(
1810
                'bbb_delete_record',
1811
                'remote_id',
1812
                $meetingData['remoteId'],
1813
                null,
1814
                api_get_user_id(),
1815
                api_get_course_int_id(),
1816
                api_get_session_id()
1817
            );
1818
1819
            /** @var ConferenceRecordingRepository $recordingRepo */
1820
            $recordingRepo = $em->getRepository(ConferenceRecording::class);
1821
            $recordings = $recordingRepo->findByMeetingRemoteId($meetingData['remoteId']);
1822
        }
1823
1824
        if (!empty($recordings) && isset($recordings['messageKey']) && $recordings['messageKey'] === 'noRecordings') {
1825
            $delete = true;
1826
        } elseif (!empty($recordings['records'])) {
1827
            $recordsToDelete = [];
1828
            foreach ($recordings['records'] as $record) {
1829
                $recordsToDelete[] = $record['recordId'];
1830
            }
1831
1832
            if (!empty($recordsToDelete)) {
1833
                $recordingParams = ['recordId' => implode(',', $recordsToDelete)];
1834
                Event::addEvent(
1835
                    'bbb_delete_record',
1836
                    'record_id_list',
1837
                    implode(',', $recordsToDelete),
1838
                    null,
1839
                    api_get_user_id(),
1840
                    api_get_course_int_id(),
1841
                    api_get_session_id()
1842
                );
1843
1844
                $result = $this->api->deleteRecordingsWithXmlResponseArray($recordingParams);
1845
1846
                if (!empty($result) && isset($result['deleted']) && $result['deleted'] === 'true') {
1847
                    $delete = true;
1848
                }
1849
            }
1850
        }
1851
1852
        if (!$delete) {
1853
            $delete = true;
1854
        }
1855
1856
        if ($delete) {
1857
            /** @var ConferenceActivityRepository $activityRepo */
1858
            $activityRepo = $em->getRepository(ConferenceActivity::class);
1859
            $activityRepo->closeAllByMeetingId((int) $id);
1860
1861
            $meeting = $meetingRepo->find((int) $id);
1862
            if ($meeting) {
1863
                $em->remove($meeting);
1864
            }
1865
1866
            $em->flush();
1867
        }
1868
1869
        return $delete;
1870
    }
1871
1872
    /**
1873
     * Creates a link in the links tool from the given videoconference recording
1874
     *
1875
     * @param int $id ID of the item in the plugin_bbb_meeting table
1876
     * @param string Hash identifying the recording, as provided by the API
1877
     *
1878
     * @return mixed ID of the newly created link, or false on error
1879
     * @assert (null, null) === false
1880
     * @assert (1, null) === false
1881
     * @assert (null, 'abcdefabcdefabcdefabcdef') === false
1882
     */
1883
    public function copyRecordingToLinkTool($id)
1884
    {
1885
        if (empty($id)) {
1886
            return false;
1887
        }
1888
1889
        $em = Database::getManager();
1890
        /** @var ConferenceMeetingRepository $repo */
1891
        $repo = $em->getRepository(ConferenceMeeting::class);
1892
1893
        $meetingData = $repo->findOneAsArrayById((int) $id);
1894
        if (!$meetingData || empty($meetingData['remoteId'])) {
1895
            return false;
1896
        }
1897
1898
        $records = $this->api->getRecordingsWithXmlResponseArray([
1899
            'meetingId' => $meetingData['remoteId']
1900
        ]);
1901
1902
        if (!empty($records)) {
1903
            if (isset($records['message']) && !empty($records['message'])) {
1904
                if ($records['messageKey'] == 'noRecordings') {
1905
                    return false;
1906
                }
1907
            } else {
1908
                $record = $records[0];
1909
                if (is_array($record) && isset($record['recordId'])) {
1910
                    $url = $record['playbackFormatUrl'];
1911
                    $link = new \Link();
1912
                    $params = [
1913
                        'url' => $url,
1914
                        'title' => $meetingData['title'],
1915
                    ];
1916
                    $id = $link->save($params);
1917
1918
                    return $id;
1919
                }
1920
            }
1921
        }
1922
1923
        return false;
1924
    }
1925
1926
    /**
1927
     * Checks if the video conference server is running.
1928
     * Function currently disabled (always returns 1)
1929
     * @return bool True if server is running, false otherwise
1930
     * @assert () === false
1931
     */
1932
    public function isServerRunning()
1933
    {
1934
        return true;
1935
        //return BigBlueButtonBN::isServerRunning($this->protocol.$this->url);
1936
    }
1937
1938
    /**
1939
     * Checks if the video conference plugin is properly configured
1940
     * @return bool True if plugin has a host and a salt, false otherwise
1941
     * @assert () === false
1942
     */
1943
    public function isServerConfigured()
1944
    {
1945
        $host = $this->plugin->get('host');
1946
1947
        if (empty($host)) {
1948
            return false;
1949
        }
1950
1951
        $salt = $this->plugin->get('salt');
1952
1953
        if (empty($salt)) {
1954
            return false;
1955
        }
1956
1957
        return true;
1958
        //return BigBlueButtonBN::isServerRunning($this->protocol.$this->url);
1959
    }
1960
1961
    /**
1962
     * Get active session in the all platform
1963
     */
1964
    public function getActiveSessionsCount(): int
1965
    {
1966
        $em = Database::getManager();
1967
        $qb = $em->createQueryBuilder();
1968
1969
        $qb->select('COUNT(m.id)')
1970
            ->from(ConferenceMeeting::class, 'm')
1971
            ->where('m.status = :status')
1972
            ->andWhere('m.accessUrl = :accessUrl')
1973
            ->setParameter('status', 1)
1974
            ->setParameter('accessUrl', $this->accessUrl);
1975
1976
        return (int) $qb->getQuery()->getSingleScalarResult();
1977
    }
1978
1979
    /**
1980
     * Get active session in the all platform
1981
     */
1982
    public function getActiveSessions(): array
1983
    {
1984
        $em = Database::getManager();
1985
        $repo = $em->getRepository(ConferenceMeeting::class);
1986
1987
        $qb = $repo->createQueryBuilder('m')
1988
            ->where('m.status = :status')
1989
            ->andWhere('m.accessUrl = :accessUrl')
1990
            ->setParameter('status', 1)
1991
            ->setParameter('accessUrl', $this->accessUrl);
1992
1993
        return $qb->getQuery()->getArrayResult();
1994
    }
1995
1996
    /**
1997
     * @param string $url
1998
     */
1999
    public function redirectToBBB($url)
2000
    {
2001
        if (file_exists(__DIR__.'/../config.vm.php')) {
2002
            // Using VM
2003
            echo Display::url($this->plugin->get_lang('ClickToContinue'), $url);
2004
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
2005
        } else {
2006
            // Classic
2007
            header("Location: $url");
2008
            exit;
2009
        }
2010
    }
2011
2012
    /**
2013
     * @return string
2014
     */
2015
    public function getConferenceUrl()
2016
    {
2017
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/start.php?launch=1&'.$this->getUrlParams();
2018
    }
2019
2020
    /**
2021
     * Get the meeting info from DB by its name
2022
     *
2023
     * @param string $name
2024
     *
2025
     * @return array
2026
     */
2027
    public function findMeetingByName(string $name): ?array
2028
    {
2029
        $em = Database::getManager();
2030
        /** @var ConferenceMeetingRepository $repo */
2031
        $repo = $em->getRepository(ConferenceMeeting::class);
2032
2033
        $qb = $repo->createQueryBuilder('m')
2034
            ->where('m.title = :name')
2035
            ->setParameter('name', $name)
2036
            ->setMaxResults(1);
2037
2038
        $result = $qb->getQuery()->getArrayResult();
2039
2040
        return $result[0] ?? null;
2041
    }
2042
2043
    /**
2044
     * Get the meeting info from DB by its name
2045
     *
2046
     * @param int $id
2047
     *
2048
     * @return array
2049
     */
2050
    public function getMeeting(int $id): ?array
2051
    {
2052
        $em = Database::getManager();
2053
        /** @var ConferenceMeetingRepository $repo */
2054
        $repo = $em->getRepository(ConferenceMeeting::class);
2055
2056
        return $repo->findOneAsArrayById($id);
2057
    }
2058
2059
    /**
2060
     * Get the meeting info.
2061
     *
2062
     * @param int $id
2063
     *
2064
     * @return array
2065
     */
2066
    public function getMeetingByRemoteId(string $id): ?array
2067
    {
2068
        $em = Database::getManager();
2069
        /** @var ConferenceMeetingRepository $repo */
2070
        $repo = $em->getRepository(ConferenceMeeting::class);
2071
2072
        return $repo->findOneByRemoteIdAndAccessUrl($id, $this->accessUrl);
2073
    }
2074
2075
    /**
2076
     * @param int $meetingId
2077
     *
2078
     * @return array
2079
     */
2080
    public function findConnectedMeetingParticipants(int $meetingId): array
2081
    {
2082
        $em = Database::getManager();
2083
        /** @var ConferenceActivityRepository $repo */
2084
        $repo = $em->getRepository(ConferenceActivity::class);
2085
2086
        $activities = $repo->createQueryBuilder('a')
2087
            ->where('a.meeting = :meetingId')
2088
            ->andWhere('a.inAt IS NOT NULL')
2089
            ->setParameter('meetingId', $meetingId)
2090
            ->getQuery()
2091
            ->getResult();
2092
2093
        $participantIds = [];
2094
        $return = [];
2095
2096
        foreach ($activities as $activity) {
2097
            $participant = $activity->getParticipant();
2098
            $participantId = $participant?->getId();
2099
2100
            if (!$participantId || in_array($participantId, $participantIds)) {
2101
                continue;
2102
            }
2103
2104
            $participantIds[] = $participantId;
2105
2106
            $return[] = [
2107
                'id' => $activity->getId(),
2108
                'meeting_id' => $meetingId,
2109
                'participant' => api_get_user_entity($participantId),
2110
                'in_at' => $activity->getInAt()?->format('Y-m-d H:i:s'),
2111
                'out_at' => $activity->getOutAt()?->format('Y-m-d H:i:s'),
2112
            ];
2113
        }
2114
2115
        return $return;
2116
    }
2117
2118
    /**
2119
     * Check if the meeting has a capture.m4v video file. If exists then the has_video_m4v field is updated
2120
     *
2121
     * @param int $meetingId
2122
     *
2123
     * @return bool
2124
     */
2125
    public function checkDirectMeetingVideoUrl(int $meetingId): bool
2126
    {
2127
        $em = Database::getManager();
2128
        /** @var ConferenceMeetingRepository $repo */
2129
        $repo = $em->getRepository(ConferenceMeeting::class);
2130
2131
        $meetingInfo = $repo->findOneAsArrayById($meetingId);
2132
2133
        if (empty($meetingInfo) || !isset($meetingInfo['videoUrl'])) {
2134
            return false;
2135
        }
2136
2137
        $hasCapture = SocialManager::verifyUrl($meetingInfo['videoUrl'].'/capture.m4v');
2138
2139
        if ($hasCapture) {
2140
            $qb = $em->createQueryBuilder();
2141
            $qb->update(ConferenceMeeting::class, 'm')
2142
                ->set('m.hasVideoM4v', ':value')
2143
                ->where('m.id = :id')
2144
                ->setParameter('value', true)
2145
                ->setParameter('id', $meetingId)
2146
                ->getQuery()
2147
                ->execute();
2148
2149
            return true;
2150
        }
2151
2152
        return false;
2153
    }
2154
2155
    public static function showGlobalConferenceLink($userInfo)
2156
    {
2157
        if (empty($userInfo)) {
2158
            return false;
2159
        }
2160
2161
        $setting = api_get_plugin_setting('bbb', 'enable_global_conference');
2162
        $settingLink = api_get_plugin_setting('bbb', 'enable_global_conference_link');
2163
2164
        if ($setting === 'true' && $settingLink === 'true') {
2165
            $allowedRoles = api_get_plugin_setting('bbb', 'global_conference_allow_roles');
2166
            $allowedRoles = self::normalizeSettingToArray($allowedRoles);
2167
2168
            if (api_is_platform_admin()) {
2169
                $userInfo['status'] = PLATFORM_ADMIN;
2170
            }
2171
2172
            if (!empty($allowedRoles)) {
2173
                $needle = (string) $userInfo['status'];
2174
                $haystack = array_map('strval', $allowedRoles);
2175
2176
                if (!in_array($needle, $haystack, true)) {
2177
                    return false;
2178
                }
2179
            }
2180
2181
            return true;
2182
        }
2183
2184
        return false;
2185
    }
2186
2187
    /**
2188
     * Build the callback URL for BBB webhooks, signed with HMAC using the plugin salt.
2189
     * No DB tables involved.
2190
     */
2191
    private function buildWebhookCallbackUrl(?string $meetingId = null): string
2192
    {
2193
        // Plugin public endpoint to implement in the next step (receiver)
2194
        $base = rtrim(api_get_path(WEB_PLUGIN_PATH), '/').'/Bbb/webhook.php';
2195
2196
        $au  = (int) $this->accessUrl;
2197
        $mid = (string) ($meetingId ?? '');
2198
        $ts  = time();
2199
2200
        // Reuse plugin salt as HMAC secret (no new setting)
2201
        $algo = $this->plugin->webhooksHashAlgo(); // 'sha256' | 'sha1'
2202
        $secret = (string) $this->salt;
2203
2204
        $payload = implode('|', [$au, $mid, (string)$ts]);
2205
        $sig = hash_hmac($algo, $payload, $secret);
2206
2207
        $qs = [
2208
            'au'  => $au,
2209
            'mid' => $mid,
2210
            'ts'  => $ts,
2211
            'sig' => $sig,
2212
        ];
2213
2214
        return $base.'?'.http_build_query($qs);
2215
    }
2216
2217
    /**
2218
     * Low-level BBB hooks API caller (GET with BBB checksum: sha1(call+query+salt)).
2219
     * Returns a normalized array. Never throws.
2220
     */
2221
    private function bbbHooksRequest(string $call, array $params): array
2222
    {
2223
        try {
2224
            $base = rtrim($this->urlWithProtocol ?: '', '/'); // ends with /bigbluebutton/
2225
            if ($base === '') {
2226
                return ['returncode' => 'FAILED', 'messageKey' => 'missingServerUrl'];
2227
            }
2228
2229
            // BBB expects checksum = sha1(call + querystring + salt)
2230
            $query = http_build_query($params);
2231
            $checksum = sha1($call.$query.$this->salt);
2232
            $url = $base.'api/'.$call.'?'.$query.'&checksum='.$checksum;
2233
2234
            $xml = @simplexml_load_file($url);
2235
            if (!$xml) {
2236
                return ['returncode' => 'FAILED', 'messageKey' => 'noResponse'];
2237
            }
2238
2239
            $out = ['returncode' => (string)($xml->returncode ?? '')];
2240
            if (isset($xml->messageKey)) { $out['messageKey'] = (string)$xml->messageKey; }
2241
            if (isset($xml->message))    { $out['message']    = (string)$xml->message; }
2242
2243
            if ($call === 'hooks/list' && isset($xml->hooks)) {
2244
                $out['hooks'] = [];
2245
                foreach (($xml->hooks->hook ?? []) as $h) {
2246
                    $out['hooks'][] = [
2247
                        'id'          => isset($h->id) ? (string)$h->id : '',
2248
                        'callbackURL' => isset($h->callbackURL) ? (string)$h->callbackURL : '',
2249
                        'meetingID'   => isset($h->meetingID) ? (string)$h->meetingID : '',
2250
                    ];
2251
                }
2252
            }
2253
            if ($call === 'hooks/create' && isset($xml->hookID)) {
2254
                $out['hookID'] = (string)$xml->hookID;
2255
            }
2256
2257
            return $out;
2258
        } catch (\Throwable $e) {
2259
            if ($this->debug) {
2260
                error_log('bbbHooksRequest error: '.$e->getMessage());
2261
            }
2262
            return ['returncode' => 'FAILED', 'messageKey' => 'exception'];
2263
        }
2264
    }
2265
2266
    /**
2267
     * Register a per-meeting webhook (idempotent enough for our flow).
2268
     * Will silently no-op if BBB webhooks plugin isn’t installed.
2269
     */
2270
    private function registerWebhookForMeeting(string $meetingId): void
2271
    {
2272
        if ($meetingId === '') {
2273
            return;
2274
        }
2275
2276
        $params = [
2277
            'callbackURL' => $this->buildWebhookCallbackUrl($meetingId),
2278
            'meetingID'   => $meetingId,
2279
        ];
2280
2281
        // Optional CSV filter like: user-joined,user-left,webcam-started,webcam-stopped
2282
        $events = trim((string)$this->plugin->webhooksEventFilter());
2283
        if ($events !== '') {
2284
            $params['events'] = $events;
2285
        }
2286
2287
        $res = $this->bbbHooksRequest('hooks/create', $params);
2288
        if (($res['returncode'] ?? '') !== 'SUCCESS' && $this->debug) {
2289
            error_log('BBB hooks/create (per_meeting) failed: '.json_encode($res));
2290
        }
2291
    }
2292
2293
    /**
2294
     * Ensure a single global webhook subscription exists (no meetingID).
2295
     * Uses hooks/list to avoid duplicates (no DB table needed).
2296
     */
2297
    private function ensureGlobalWebhook(): void
2298
    {
2299
        $callback = $this->buildWebhookCallbackUrl(null);
2300
2301
        $list = $this->bbbHooksRequest('hooks/list', []);
2302
        if (($list['returncode'] ?? '') !== 'SUCCESS') {
2303
            if ($this->debug) {
2304
                error_log('BBB hooks/list failed or unavailable (global).');
2305
            }
2306
            return; // webhooks plugin might not be installed
2307
        }
2308
2309
        foreach (($list['hooks'] ?? []) as $h) {
2310
            if (($h['callbackURL'] ?? '') === $callback && ($h['meetingID'] ?? '') === '') {
2311
                return; // already registered
2312
            }
2313
        }
2314
2315
        $params = ['callbackURL' => $callback];
2316
        $events = trim((string)$this->plugin->webhooksEventFilter());
2317
        if ($events !== '') {
2318
            $params['events'] = $events;
2319
        }
2320
2321
        $res = $this->bbbHooksRequest('hooks/create', $params);
2322
        if (($res['returncode'] ?? '') !== 'SUCCESS' && $this->debug) {
2323
            error_log('BBB hooks/create (global) failed: '.json_encode($res));
2324
        }
2325
    }
2326
}
2327