Bbb::createMeeting()   F
last analyzed

Complexity

Conditions 33
Paths 6914

Size

Total Lines 176
Code Lines 119

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 33
eloc 119
c 0
b 0
f 0
nc 6914
nop 1
dl 0
loc 176
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\CoreBundle\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
        error_log('BBB API createMeeting');
412
        // Ensure API is available
413
        if (!$this->ensureApi()) {
414
            if ($this->debug) {
415
                error_log('BBB API not initialized in createMeeting().');
416
            }
417
            return false;
418
        }
419
420
        // Normalize input
421
        $params = is_array($params) ? $params : [];
422
423
        // Context defaults
424
        $params['c_id']       = $params['c_id']       ?? api_get_course_int_id();
425
        $params['session_id'] = $params['session_id'] ?? api_get_session_id();
426
427
        if ($this->hasGroupSupport()) {
428
            $params['group_id'] = $params['group_id'] ?? api_get_group_id();
429
        }
430
431
        if ($this->isGlobalConferencePerUserEnabled() && !empty($this->userId)) {
432
            $params['user_id'] = (int) $this->userId;
433
        }
434
435
        // Passwords
436
        $params['attendee_pw']  = $params['attendee_pw']  ?? $this->getUserMeetingPassword();
437
        $params['moderator_pw'] = $params['moderator_pw'] ?? $this->getModMeetingPassword();
438
        $attendeePassword  = (string) $params['attendee_pw'];
439
        $moderatorPassword = (string) $params['moderator_pw'];
440
441
        // Recording and limits
442
        $params['record'] = api_get_course_plugin_setting('bbb', 'big_blue_button_record_and_store') == 1 ? 1 : 0;
443
        $max = api_get_course_plugin_setting('bbb', 'big_blue_button_max_students_allowed');
444
        $max = isset($max) ? (int) $max : -1;
445
446
        // Meeting identifiers
447
        $params['status']     = 1;
448
        $params['remote_id']  = $params['remote_id']  ?? uniqid(true, true);
449
        $params['voice_bridge']= $params['voice_bridge'] ?? rand(10000, 99999);
450
        $params['created_at'] = $params['created_at'] ?? api_get_utc_datetime();
451
        $params['access_url'] = $params['access_url'] ?? $this->accessUrl;
452
453
        // Persist meeting entity
454
        $em = Database::getManager();
455
        $meeting = new ConferenceMeeting();
456
457
        $meeting
458
            ->setCourse(api_get_course_entity($params['c_id']))
459
            ->setSession(api_get_session_entity($params['session_id']))
460
            ->setAccessUrl(api_get_url_entity($params['access_url']))
461
            ->setGroup($this->hasGroupSupport() ? api_get_group_entity($params['group_id']) : null)
462
            ->setUser($this->isGlobalConferencePerUserEnabled() ? api_get_user_entity($params['user_id'] ?? 0) : null)
463
            ->setRemoteId($params['remote_id'])
464
            ->setTitle($params['meeting_name'] ?? $this->getCurrentVideoConferenceName())
465
            ->setAttendeePw($attendeePassword)
466
            ->setModeratorPw($moderatorPassword)
467
            ->setRecord((bool) $params['record'])
468
            ->setStatus($params['status'])
469
            ->setVoiceBridge($params['voice_bridge'])
470
            ->setWelcomeMsg($params['welcome_msg'] ?? null)
471
            ->setVisibility(1)
472
            ->setHasVideoM4v(false)
473
            ->setServiceProvider('bbb');
474
475
        $em->persist($meeting);
476
        $em->flush();
477
478
        $id = $meeting->getId();
479
480
        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

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

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

799
                    /** @scrutinizer ignore-call */ 
800
                    $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...
800
                        api_get_user_id(),
801
                        GroupManager::get_group_properties($groupId)
802
                    );
803
                    if ($isSubscribed) {
804
                        return true;
805
                    }
806
                }
807
            }
808
        }
809
810
        if (!empty($courseInfo)) {
811
            return api_is_course_admin();
812
        }
813
814
        return false;
815
    }
816
817
    /**
818
     * Get information about the given meeting
819
     *
820
     * @param array ...?
821
     *
822
     * @return mixed Array of information on success, false on error
823
     * @assert (array()) === false
824
     */
825
    public function getMeetingInfo($params)
826
    {
827
        try {
828
            // Guard against null API
829
            if (!$this->ensureApi()) {
830
                if ($this->debug) {
831
                    error_log('BBB API not initialized (missing URL or salt).');
832
                }
833
                return false;
834
            }
835
836
            $result = $this->api->getMeetingInfoWithXmlResponseArray($params);
837
            if ($result == null) {
838
                if ($this->debug) {
839
                    error_log("Failed to get any response. Maybe we can't contact the BBB server.");
840
                }
841
                return false;
842
            }
843
844
            return $result;
845
        } catch (\Throwable $e) {
846
            if ($this->debug) {
847
                error_log('BBB getMeetingInfo error: '.$e->getMessage());
848
            }
849
            return false;
850
        }
851
    }
852
853
854
    /**
855
     * Returns per-participant aggregates for a given meeting (durations + metrics).
856
     * - Sums all closed intervals (inAt..outAt) plus any open interval up to "now".
857
     * - Merges JSON metrics across rows:
858
     *   * Sums integer counters and "totals.*_seconds"
859
     *   * Keeps last non-empty scalar for other leaves
860
     */
861
    public function getMeetingParticipantInfo($meetingId, $userId): array
862
    {
863
        $em = Database::getManager();
864
        /** @var ConferenceActivityRepository $repo */
865
        $repo = $em->getRepository(ConferenceActivity::class);
866
867
        $meeting = $em->getRepository(ConferenceMeeting::class)->find((int)$meetingId);
868
        $user    = $em->getRepository(User::class)->find((int)$userId);
869
870
        if (!$meeting || !$user) {
871
            return [];
872
        }
873
874
        $rows = $repo->createQueryBuilder('a')
875
            ->where('a.meeting = :meeting')
876
            ->andWhere('a.participant = :user')
877
            ->setParameter('meeting', $meeting)
878
            ->setParameter('user', $user)
879
            ->orderBy('a.id', 'ASC')
880
            ->getQuery()->getResult();
881
882
        if (!$rows) {
883
            return [];
884
        }
885
886
        $now = new \DateTimeImmutable();
887
        $onlineSeconds = 0;
888
        $lastInAt = null;
889
        $lastOutAt = null;
890
891
        // Merge metrics across rows
892
        $mergedMetrics = [];
893
894
        foreach ($rows as $a) {
895
            $in  = $a->getInAt();
896
            $out = $a->getOutAt();
897
898
            if ($in instanceof \DateTimeInterface) {
899
                $lastInAt = $in;
900
            }
901
            if ($out instanceof \DateTimeInterface) {
902
                $lastOutAt = $out;
903
            }
904
905
            // Duration: if outAt missing or equals inAt (open), count until now
906
            $start = $in instanceof \DateTimeInterface ? \DateTimeImmutable::createFromMutable($in) : null;
907
            $end   = $out instanceof \DateTimeInterface ? \DateTimeImmutable::createFromMutable($out) : null;
908
909
            if ($start) {
910
                $effectiveEnd = ($end && $end > $start) ? $end : $now;
911
                $delta = max(0, $effectiveEnd->getTimestamp() - $start->getTimestamp());
912
                $onlineSeconds += $delta;
913
            }
914
915
            // Merge metrics JSON
916
            if (method_exists($a, 'getMetrics')) {
917
                $metrics = $a->getMetrics() ?? [];
918
                $mergedMetrics = $this->mergeMetrics($mergedMetrics, $metrics);
919
            }
920
        }
921
922
        return [
923
            'meeting_id'      => (int)$meeting->getId(),
924
            'participant_id'  => (int)$user->getId(),
925
            'online_seconds'  => (int)$onlineSeconds,
926
            'in_at_last'      => $lastInAt ? $lastInAt->format('Y-m-d H:i:s') : null,
927
            'out_at_last'     => $lastOutAt ? $lastOutAt->format('Y-m-d H:i:s') : null,
928
            'metrics'         => $mergedMetrics,
929
        ];
930
    }
931
932
    /**
933
     * Merge two metrics arrays:
934
     * - If both leaves are numeric -> sum them
935
     * - If keys start with "totals." and end with "_seconds" and values are numeric -> sum
936
     * - If arrays -> deep merge with same rules
937
     * - Otherwise -> prefer right-side (latest non-empty)
938
     */
939
    private function mergeMetrics(array $left, array $right, string $prefix = ''): array
940
    {
941
        foreach ($right as $k => $rv) {
942
            $path = $prefix === '' ? $k : $prefix.'.'.$k;
943
944
            if (!array_key_exists($k, $left)) {
945
                $left[$k] = $rv;
946
                continue;
947
            }
948
949
            $lv = $left[$k];
950
951
            // Both arrays: deep merge
952
            if (is_array($lv) && is_array($rv)) {
953
                $left[$k] = $this->mergeMetrics($lv, $rv, $path);
954
                continue;
955
            }
956
957
            // Both numeric: sum
958
            if (is_numeric($lv) && is_numeric($rv)) {
959
                $left[$k] = 0 + $lv + $rv;
960
                continue;
961
            }
962
963
            // totals.* seconds heuristic
964
            $isTotalsSeconds = (str_starts_with($path, 'totals.') && str_ends_with($k, '_seconds'));
965
            if ($isTotalsSeconds && is_numeric($lv) && is_numeric($rv)) {
966
                $left[$k] = 0 + $lv + $rv;
967
                continue;
968
            }
969
970
            // Fallback: keep the most recent non-empty
971
            $left[$k] = $rv !== null && $rv !== '' ? $rv : $lv;
972
        }
973
974
        return $left;
975
    }
976
977
    /**
978
     * Save a participant in a meeting room
979
     *
980
     * @param int $meetingId
981
     * @param int $participantId
982
     *
983
     * @return false|int The last inserted ID. Otherwise return false
984
     */
985
    public function saveParticipant(int $meetingId, int $participantId): false|int
986
    {
987
        $em = Database::getManager();
988
989
        /** @var ConferenceActivityRepository $repo */
990
        $repo = $em->getRepository(ConferenceActivity::class);
991
992
        $meeting = $em->getRepository(ConferenceMeeting::class)->find($meetingId);
993
        $user = api_get_user_entity($participantId);
994
995
        if (!$meeting || !$user) {
996
            return false;
997
        }
998
999
        $existing = $repo->createQueryBuilder('a')
1000
            ->where('a.meeting = :meeting')
1001
            ->andWhere('a.participant = :participant')
1002
            ->andWhere('a.close = :open')
1003
            ->setParameter('meeting', $meeting)
1004
            ->setParameter('participant', $user)
1005
            ->setParameter('open', \BbbPlugin::ROOM_OPEN)
1006
            ->getQuery()
1007
            ->getResult();
1008
1009
        foreach ($existing as $activity) {
1010
            if ($activity->getInAt() != $activity->getOutAt()) {
1011
                $activity->setClose(\BbbPlugin::ROOM_CLOSE);
1012
            } else {
1013
                $activity->setOutAt(new \DateTime());
1014
                $activity->setClose(\BbbPlugin::ROOM_CLOSE);
1015
            }
1016
            $em->persist($activity);
1017
        }
1018
1019
        $newActivity = new ConferenceActivity();
1020
        $newActivity->setMeeting($meeting);
1021
        $newActivity->setParticipant($user);
1022
        $newActivity->setInAt(new \DateTime());
1023
        $newActivity->setOutAt(new \DateTime());
1024
        $newActivity->setClose(\BbbPlugin::ROOM_OPEN);
1025
1026
        $em->persist($newActivity);
1027
        $em->flush();
1028
1029
        return $newActivity->getId();
1030
    }
1031
1032
    /**
1033
     * Tells whether the given meeting exists and is running
1034
     * (using course code as name)
1035
     *
1036
     * @param string $meetingName Meeting name (usually the course code)
1037
     *
1038
     * @return bool True if meeting exists, false otherwise
1039
     * @assert ('') === false
1040
     * @assert ('abcdefghijklmnopqrstuvwxyzabcdefghijklmno') === false
1041
     */
1042
    public function meetingExists($meetingName)
1043
    {
1044
        $meetingData = $this->getMeetingByName($meetingName);
1045
1046
        return !empty($meetingData);
1047
    }
1048
1049
    /**
1050
     * @param string $meetingName
1051
     *
1052
     * @return array
1053
     */
1054
    public function getMeetingByName($meetingName)
1055
    {
1056
        if (empty($meetingName)) {
1057
            return [];
1058
        }
1059
1060
        $courseEntity = api_get_course_entity();
1061
        $sessionEntity = api_get_session_entity();
1062
        $accessUrlEntity = api_get_url_entity($this->accessUrl);
1063
1064
        $criteria = [
1065
            'course' => $courseEntity,
1066
            'session' => $sessionEntity,
1067
            'title' => $meetingName,
1068
            'status' => 1,
1069
            'accessUrl' => $accessUrlEntity,
1070
        ];
1071
1072
        if ($this->hasGroupSupport()) {
1073
            $groupEntity = api_get_group_entity(api_get_group_id());
1074
            $criteria['group'] = $groupEntity;
1075
        }
1076
1077
        $meeting = Database::getManager()
1078
            ->getRepository(ConferenceMeeting::class)
1079
            ->findOneBy($criteria);
1080
1081
        if ($this->debug) {
1082
            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

1082
            error_log('meeting_exists './** @scrutinizer ignore-type */ print_r($meeting ? ['id' => $meeting->getId()] : [], 1));
Loading history...
1083
        }
1084
1085
        if (!$meeting) {
1086
            return [];
1087
        }
1088
1089
        return [
1090
            'id' => $meeting->getId(),
1091
            'c_id' => $meeting->getCourse()?->getId(),
1092
            'session_id' => $meeting->getSession()?->getId(),
1093
            'meeting_name' => $meeting->getTitle(),
1094
            'status' => $meeting->getStatus(),
1095
            'access_url' => $meeting->getAccessUrl()?->getId(),
1096
            'group_id' => $meeting->getGroup()?->getIid(),
1097
            'remote_id' => $meeting->getRemoteId(),
1098
            'moderator_pw' => $meeting->getModeratorPw(),
1099
            'attendee_pw' => $meeting->getAttendeePw(),
1100
            'created_at' => $meeting->getCreatedAt()->format('Y-m-d H:i:s'),
1101
            'closed_at' => $meeting->getClosedAt()?->format('Y-m-d H:i:s'),
1102
            'visibility' => $meeting->getVisibility(),
1103
            'video_url' => $meeting->getVideoUrl(),
1104
            'has_video_m4v' => $meeting->isHasVideoM4v(),
1105
            'record' => $meeting->isRecord(),
1106
            'internal_meeting_id' => $meeting->getInternalMeetingId(),
1107
        ];
1108
    }
1109
1110
    /**
1111
     * Gets a list from the database of all meetings attached to a course with the given status
1112
     * @param int $courseId
1113
     * @param int $sessionId
1114
     * @param int $status 0 for closed meetings, 1 for open meetings
1115
     *
1116
     * @return array
1117
     */
1118
    public function getAllMeetingsInCourse($courseId, $sessionId, $status)
1119
    {
1120
        $em = Database::getManager();
1121
        $courseEntity = api_get_course_entity($courseId);
1122
        $sessionEntity = api_get_session_entity($sessionId);
1123
1124
        $meetings = $em->getRepository(ConferenceMeeting::class)->findBy([
1125
            'course' => $courseEntity,
1126
            'session' => $sessionEntity,
1127
            'status' => $status,
1128
        ]);
1129
1130
        $results = [];
1131
        foreach ($meetings as $meeting) {
1132
            $results[] = [
1133
                'id' => $meeting->getId(),
1134
                'c_id' => $meeting->getCourse()?->getId(),
1135
                'session_id' => $meeting->getSession()?->getId(),
1136
                'meeting_name' => $meeting->getTitle(),
1137
                'status' => $meeting->getStatus(),
1138
                'access_url' => $meeting->getAccessUrl()?->getId(),
1139
                'group_id' => $meeting->getGroup()?->getIid(),
1140
                'remote_id' => $meeting->getRemoteId(),
1141
                'moderator_pw' => $meeting->getModeratorPw(),
1142
                'attendee_pw' => $meeting->getAttendeePw(),
1143
                'created_at' => $meeting->getCreatedAt()->format('Y-m-d H:i:s'),
1144
                'closed_at' => $meeting->getClosedAt()?->format('Y-m-d H:i:s'),
1145
                'visibility' => $meeting->getVisibility(),
1146
                'video_url' => $meeting->getVideoUrl(),
1147
                'has_video_m4v' => $meeting->isHasVideoM4v(),
1148
                'record' => $meeting->isRecord(),
1149
                'internal_meeting_id' => $meeting->getInternalMeetingId(),
1150
            ];
1151
        }
1152
1153
        return $results;
1154
    }
1155
1156
    /**
1157
     * Gets all the course meetings saved in the plugin_bbb_meeting table and
1158
     * generate actionable links (join/close/delete/etc)
1159
     *
1160
     * @param int   $courseId
1161
     * @param int   $sessionId
1162
     * @param int   $groupId
1163
     * @param bool  $isAdminReport Optional. Set to true then the report is for admins
1164
     * @param array $dateRange     Optional
1165
     *
1166
     * @return array Array of current open meeting rooms
1167
     * @throws Exception
1168
     */
1169
    public function getMeetings(
1170
        $courseId = 0,
1171
        $sessionId = 0,
1172
        $groupId = 0,
1173
        $isAdminReport = false,
1174
        $dateRange = []
1175
    ) {
1176
        if (!$this->ensureApi()) {
1177
            if ($this->debug) {
1178
                error_log('BBB API not initialized in getMeetings().');
1179
            }
1180
            return [];
1181
        }
1182
1183
        $em = Database::getManager();
1184
        $repo = $em->getRepository(ConferenceMeeting::class);
1185
        $manager = $this->isConferenceManager();
1186
        $isGlobal = $this->isGlobalConference();
1187
        $meetings = [];
1188
1189
        if (!empty($dateRange)) {
1190
            $dateStart = (new \DateTime($dateRange['search_meeting_start']))->setTime(0, 0, 0);
1191
            $dateEnd = (new \DateTime($dateRange['search_meeting_end']))->setTime(23, 59, 59);
1192
            $meetings = $repo->findByDateRange($dateStart, $dateEnd);
1193
        } elseif ($this->isGlobalConference()) {
1194
            $meetings = $repo->findBy([
1195
                'course' => null,
1196
                'user' => api_get_user_entity($this->userId),
1197
                'accessUrl' => api_get_url_entity($this->accessUrl),
1198
            ]);
1199
        } elseif ($this->isGlobalConferencePerUserEnabled()) {
1200
            $meetings = $repo->findBy([
1201
                'course' => api_get_course_entity($courseId),
1202
                'session' => api_get_session_entity($sessionId),
1203
                'user' => api_get_user_entity($this->userId),
1204
                'accessUrl' => api_get_url_entity($this->accessUrl),
1205
            ]);
1206
        } else {
1207
            $criteria = [
1208
                'course' => api_get_course_entity($courseId),
1209
                'session' => api_get_session_entity($sessionId),
1210
                'accessUrl' => api_get_url_entity($this->accessUrl),
1211
            ];
1212
            if ($this->hasGroupSupport() && $groupId) {
1213
                $criteria['group'] = api_get_group_entity($groupId);
1214
            }
1215
            $meetings = $repo->findBy($criteria, ['createdAt' => 'ASC']);
1216
        }
1217
1218
        $result = [];
1219
        foreach ($meetings as $meeting) {
1220
            $meetingArray = $this->convertMeetingToArray($meeting);
1221
            $recordLink = $this->plugin->get_lang('NoRecording');
1222
            $meetingBBB = $this->getMeetingInfo([
1223
                'meetingId' => $meeting->getRemoteId(),
1224
                'password' => $manager ? $meeting->getModeratorPw() : $meeting->getAttendeePw(),
1225
            ]);
1226
1227
            if (!$meetingBBB && $meeting->getId()) {
1228
                $meetingBBB = $this->getMeetingInfo([
1229
                    'meetingId' => $meeting->getId(),
1230
                    'password' => $manager ? $meeting->getModeratorPw() : $meeting->getAttendeePw(),
1231
                ]);
1232
            }
1233
1234
            if (!$meeting->isVisible() && !$manager) {
1235
                continue;
1236
            }
1237
1238
            $meetingBBB['end_url'] = $this->endUrl(['id' => $meeting->getId()]);
1239
            if (isset($meetingBBB['returncode']) && (string) $meetingBBB['returncode'] === 'FAILED') {
1240
                if ($meeting->getStatus() === 1 && $manager) {
1241
                    $this->endMeeting($meeting->getId(), $meeting->getCourse()?->getCode());
1242
                }
1243
            } else {
1244
                $meetingBBB['add_to_calendar_url'] = $this->addToCalendarUrl($meetingArray);
1245
            }
1246
1247
            if ($meeting->isRecord()) {
1248
                $recordings = $this->api->getRecordingsWithXmlResponseArray(['meetingId' => $meeting->getRemoteId()]);
1249
                if (!empty($recordings) && (!isset($recordings['messageKey']) || $recordings['messageKey'] !== 'noRecordings')) {
1250
                    $record = end($recordings);
1251
                    if (isset($record['playbackFormatUrl'])) {
1252
                        $recordLink = Display::url(
1253
                            $this->plugin->get_lang('ViewRecord'),
1254
                            $record['playbackFormatUrl'],
1255
                            ['target' => '_blank', 'class' => 'btn btn--plain']
1256
                        );
1257
                        $this->updateMeetingVideoUrl($meeting->getId(), $record['playbackFormatUrl']);
1258
                    }
1259
                }
1260
            }
1261
1262
            $actionLinks = $this->getActionLinks($meetingArray, $record ?? [], $isGlobal, $isAdminReport);
1263
1264
            $item = array_merge($meetingArray, [
1265
                'go_url' => '',
1266
                'show_links' => $recordLink,
1267
                'action_links' => implode(PHP_EOL, $actionLinks),
1268
                'publish_url' => $this->publishUrl(['id' => $meeting->getId()]),
1269
                'unpublish_url' => $this->unPublishUrl(['id' => $meeting->getId()]),
1270
            ]);
1271
1272
            if ($meeting->getStatus() === 1) {
1273
                $joinParams = [
1274
                    'meetingId' => $meeting->getRemoteId(),
1275
                    'username' => $this->userCompleteName,
1276
                    'password' => $manager ? $meeting->getModeratorPw() : $meeting->getAttendeePw(),
1277
                    'createTime' => '',
1278
                    'userID' => '',
1279
                    'webVoiceConf' => '',
1280
                ];
1281
                $item['go_url'] = $this->protocol.$this->api->getJoinMeetingURL($joinParams);
1282
            }
1283
1284
            $result[] = array_merge($item, $meetingBBB);
1285
        }
1286
1287
        return $result;
1288
    }
1289
1290
    private function convertMeetingToArray(ConferenceMeeting $meeting): array
1291
    {
1292
        return [
1293
            'id' => $meeting->getId(),
1294
            'remote_id' => $meeting->getRemoteId(),
1295
            'internal_meeting_id' => $meeting->getInternalMeetingId(),
1296
            'meeting_name' => $meeting->getTitle(),
1297
            'status' => $meeting->getStatus(),
1298
            'visibility' => $meeting->getVisibility(),
1299
            '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...
1300
            'closed_at' => $meeting->getClosedAt() instanceof \DateTime ? $meeting->getClosedAt()->format('Y-m-d H:i:s') : '',
1301
            'record' => $meeting->isRecord() ? 1 : 0,
1302
            'c_id' => $meeting->getCourse()?->getId() ?? 0,
1303
            'session_id' => $meeting->getSession()?->getId() ?? 0,
1304
            'group_id' => $meeting->getGroup()?->getIid() ?? 0,
1305
            'course' => $meeting->getCourse(),
1306
            'session' => $meeting->getSession(),
1307
            'title' => $meeting->getTitle(),
1308
        ];
1309
    }
1310
1311
    public function getMeetingsLight(
1312
        $courseId = 0,
1313
        $sessionId = 0,
1314
        $groupId = 0,
1315
        $dateRange = []
1316
    ): array {
1317
        $em = Database::getManager();
1318
        $repo = $em->getRepository(ConferenceMeeting::class);
1319
        $meetings = [];
1320
1321
        if (!empty($dateRange)) {
1322
            $dateStart = (new \DateTime($dateRange['search_meeting_start']))->setTime(0, 0, 0);
1323
            $dateEnd = (new \DateTime($dateRange['search_meeting_end']))->setTime(23, 59, 59);
1324
            $meetings = $repo->findByDateRange($dateStart, $dateEnd);
1325
        } else {
1326
            $criteria = [
1327
                'course' => api_get_course_entity($courseId),
1328
                'session' => api_get_session_entity($sessionId),
1329
                'accessUrl' => api_get_url_entity($this->accessUrl),
1330
            ];
1331
            if ($this->hasGroupSupport() && $groupId) {
1332
                $criteria['group'] = api_get_group_entity($groupId);
1333
            }
1334
            $meetings = $repo->findBy($criteria, ['createdAt' => 'DESC']);
1335
        }
1336
1337
        $result = [];
1338
        foreach ($meetings as $meeting) {
1339
            $meetingArray = $this->convertMeetingToArray($meeting);
1340
1341
            $item = array_merge($meetingArray, [
1342
                'go_url' => '',
1343
                'show_links' => $this->plugin->get_lang('NoRecording'),
1344
                'action_links' => '',
1345
                'publish_url' => $this->publishUrl(['id' => $meeting->getId()]),
1346
                'unpublish_url' => $this->unPublishUrl(['id' => $meeting->getId()]),
1347
            ]);
1348
1349
            $result[] = $item;
1350
        }
1351
1352
        return $result;
1353
    }
1354
1355
    /**
1356
     * @param array $meeting
1357
     *
1358
     * @return string
1359
     */
1360
    public function endUrl($meeting)
1361
    {
1362
        if (!isset($meeting['id'])) {
1363
            return '';
1364
        }
1365
1366
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams().'&action=end&id='.$meeting['id'];
1367
    }
1368
1369
    /**
1370
     * Closes a meeting (usually when the user click on the close button from
1371
     * the conferences listing.
1372
     *
1373
     * @param string The internal ID of the meeting (id field for this meeting)
1374
     * @param string $courseCode
1375
     *
1376
     * @return void
1377
     * @assert (0) === false
1378
     */
1379
    public function endMeeting($id, $courseCode = null)
1380
    {
1381
        if (empty($id)) {
1382
            return false;
1383
        }
1384
1385
        $em = Database::getManager();
1386
1387
        /** @var ConferenceMeetingRepository $repo */
1388
        $repo = $em->getRepository(ConferenceMeeting::class);
1389
1390
        $meetingData = $repo->findOneAsArrayById((int) $id);
1391
        if (!$meetingData) {
1392
            return false;
1393
        }
1394
1395
        $manager = $this->isConferenceManager();
1396
        $pass = $manager ? $meetingData['moderatorPw'] : $meetingData['attendeePw'];
1397
1398
        Event::addEvent(
1399
            'bbb_end_meeting',
1400
            'meeting_id',
1401
            (int) $id,
1402
            null,
1403
            api_get_user_id(),
1404
            api_get_course_int_id(),
1405
            api_get_session_id()
1406
        );
1407
1408
        $endParams = [
1409
            'meetingId' => $meetingData['remoteId'],
1410
            'password' => $pass,
1411
        ];
1412
        $this->api->endMeetingWithXmlResponseArray($endParams);
1413
1414
        $repo->closeMeeting((int) $id, new \DateTime());
1415
1416
        /** @var ConferenceActivityRepository $activityRepo */
1417
        $activityRepo = $em->getRepository(ConferenceActivity::class);
1418
1419
        $activities = $activityRepo->findOpenWithSameInAndOutTime((int) $id);
1420
1421
        foreach ($activities as $activity) {
1422
            $activity->setOutAt(new \DateTime());
1423
            $activity->setClose(BbbPlugin::ROOM_CLOSE);
1424
            $em->persist($activity);
1425
        }
1426
1427
        $activityRepo->closeAllByMeetingId((int) $id);
1428
1429
        $em->flush();
1430
1431
        return true;
1432
    }
1433
1434
    /**
1435
     * @param array $meeting
1436
     * @param array $record
1437
     *
1438
     * @return string
1439
     */
1440
    public function addToCalendarUrl($meeting, $record = []): string
1441
    {
1442
        $url = isset($record['playbackFormatUrl']) ? $record['playbackFormatUrl'] : '';
1443
1444
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams(
1445
            ).'&action=add_to_calendar&id='.$meeting['id'].'&start='.api_strtotime($meeting['created_at']).'&url='.$url;
1446
    }
1447
1448
    /**
1449
     * @param int    $meetingId
1450
     * @param string $videoUrl
1451
     *
1452
     * @return bool|int
1453
     */
1454
    public function updateMeetingVideoUrl(int $meetingId, string $videoUrl): void
1455
    {
1456
        $em = Database::getManager();
1457
        /** @var ConferenceMeetingRepository $repo */
1458
        $repo = $em->getRepository(ConferenceMeeting::class);
1459
        $repo->updateVideoUrl($meetingId, $videoUrl);
1460
    }
1461
1462
    /**
1463
     * Force the course, session and/or group IDs
1464
     *
1465
     * @param string $courseCode
1466
     * @param int    $sessionId
1467
     * @param int    $groupId
1468
     */
1469
    public function forceCIdReq($courseCode, $sessionId = 0, $groupId = 0)
1470
    {
1471
        $this->courseCode = $courseCode;
1472
        $this->sessionId = (int) $sessionId;
1473
        $this->groupId = (int) $groupId;
1474
    }
1475
1476
    /**
1477
     * @param array $meetingInfo
1478
     * @param array $recordInfo
1479
     * @param bool  $isGlobal
1480
     * @param bool  $isAdminReport
1481
     *
1482
     * @return array
1483
     */
1484
    private function getActionLinks(
1485
        $meetingInfo,
1486
        $recordInfo,
1487
        $isGlobal = false,
1488
        $isAdminReport = false
1489
    ) {
1490
        $isVisible = $meetingInfo['visibility'] != 0;
1491
        $linkVisibility = $isVisible
1492
            ? Display::url(
1493
                Display::getMdiIcon(StateIcon::ACTIVE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Make invisible')),
1494
                $this->unPublishUrl($meetingInfo)
1495
            )
1496
            : Display::url(
1497
                Display::getMdiIcon(StateIcon::INACTIVE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Make visible')),
1498
                $this->publishUrl($meetingInfo)
1499
            );
1500
1501
        $links = [];
1502
        if ($this->plugin->get('allow_regenerate_recording') === 'true' && $meetingInfo['record'] == 1) {
1503
            if (!empty($recordInfo)) {
1504
                $links[] = Display::url(
1505
                    Display::getMdiIcon(ActionIcon::REFRESH, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('RegenerateRecord')),
1506
                    $this->regenerateRecordUrl($meetingInfo, $recordInfo)
1507
                );
1508
            } else {
1509
                $links[] = Display::url(
1510
                    Display::getMdiIcon(ActionIcon::REFRESH, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('RegenerateRecord')),
1511
                    $this->regenerateRecordUrlFromMeeting($meetingInfo)
1512
                );
1513
            }
1514
        }
1515
1516
        if (empty($recordInfo)) {
1517
            if (!$isAdminReport) {
1518
                if ($meetingInfo['status'] == 0) {
1519
                    $links[] = Display::url(
1520
                        Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')),
1521
                        $this->deleteRecordUrl($meetingInfo)
1522
                    );
1523
                    $links[] = $linkVisibility;
1524
                }
1525
1526
                return $links;
1527
            } else {
1528
                $links[] = Display::url(
1529
                    Display::getMdiIcon(ObjectIcon::HOME, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Go to the course')),
1530
                    $this->getListingUrl($meetingInfo['c_id'], $meetingInfo['session_id'], $meetingInfo['group_id'])
1531
                );
1532
1533
                return $links;
1534
            }
1535
        }
1536
1537
        if (!$isGlobal) {
1538
            $links[] = Display::url(
1539
                Display::getMdiIcon(ObjectIcon::LINK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('UrlMeetingToShare')),
1540
                $this->copyToRecordToLinkTool($meetingInfo)
1541
            );
1542
            $links[] = Display::url(
1543
                Display::getMdiIcon(ObjectIcon::AGENDA, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Add to calendar')),
1544
                $this->addToCalendarUrl($meetingInfo, $recordInfo)
1545
            );
1546
        }
1547
1548
        $hide = $this->plugin->get('disable_download_conference_link') === 'true' ? true : false;
1549
1550
        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...
1551
            if ($meetingInfo['has_video_m4v']) {
1552
                $links[] = Display::url(
1553
                    Display::getMdiIcon(ActionIcon::SAVE_FORM, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Download file')),
1554
                    $recordInfo['playbackFormatUrl'].'/capture.m4v',
1555
                    ['target' => '_blank']
1556
                );
1557
            } else {
1558
                $links[] = Display::url(
1559
                    Display::getMdiIcon(ActionIcon::SAVE_FORM, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Download file')),
1560
                    '#',
1561
                    [
1562
                        'id' => "btn-check-meeting-video-{$meetingInfo['id']}",
1563
                        'class' => 'check-meeting-video',
1564
                        'data-id' => $meetingInfo['id'],
1565
                    ]
1566
                );
1567
            }
1568
        }
1569
1570
1571
        if (!$isAdminReport) {
1572
            $links[] = Display::url(
1573
                Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')),
1574
                $this->deleteRecordUrl($meetingInfo)
1575
            );
1576
            $links[] = $linkVisibility;
1577
        } else {
1578
            $links[] = Display::url(
1579
                Display::getMdiIcon(ObjectIcon::HOME, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Go to the course')),
1580
                $this->getListingUrl($meetingInfo['c_id'], $meetingInfo['session_id'], $meetingInfo['group_id'])
1581
            );
1582
        }
1583
1584
1585
        return $links;
1586
    }
1587
1588
    /**
1589
     * @param array $meeting
1590
     *
1591
     * @return string
1592
     */
1593
    public function unPublishUrl($meeting)
1594
    {
1595
        if (!isset($meeting['id'])) {
1596
            return null;
1597
        }
1598
1599
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams(
1600
            ).'&action=unpublish&id='.$meeting['id'];
1601
    }
1602
1603
    /**
1604
     * @param array $meeting
1605
     *
1606
     * @return string
1607
     */
1608
    public function publishUrl($meeting)
1609
    {
1610
        if (!isset($meeting['id'])) {
1611
            return '';
1612
        }
1613
1614
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams(
1615
            ).'&action=publish&id='.$meeting['id'];
1616
    }
1617
1618
    /**
1619
     * @param array $meeting
1620
     * @param array $recordInfo
1621
     *
1622
     * @return string
1623
     */
1624
    public function regenerateRecordUrl($meeting, $recordInfo)
1625
    {
1626
        if ($this->plugin->get('allow_regenerate_recording') !== 'true') {
1627
            return '';
1628
        }
1629
1630
        if (!isset($meeting['id'])) {
1631
            return '';
1632
        }
1633
1634
        if (empty($recordInfo) || (!empty($recordInfo['recordId']) && !isset($recordInfo['recordId']))) {
1635
            return '';
1636
        }
1637
1638
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams().
1639
            '&action=regenerate_record&id='.$meeting['id'].'&record_id='.$recordInfo['recordId'];
1640
    }
1641
1642
    /**
1643
     * @param array $meeting
1644
     *
1645
     * @return string
1646
     */
1647
    public function regenerateRecordUrlFromMeeting($meeting)
1648
    {
1649
        if ($this->plugin->get('allow_regenerate_recording') !== 'true') {
1650
            return '';
1651
        }
1652
1653
        if (!isset($meeting['id'])) {
1654
            return '';
1655
        }
1656
1657
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams().
1658
            '&action=regenerate_record&id='.$meeting['id'];
1659
    }
1660
1661
    /**
1662
     * @param array $meeting
1663
     *
1664
     * @return string
1665
     */
1666
    public function deleteRecordUrl($meeting)
1667
    {
1668
        if (!isset($meeting['id'])) {
1669
            return '';
1670
        }
1671
1672
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/listing.php?'.$this->getUrlParams(
1673
            ).'&action=delete_record&id='.$meeting['id'];
1674
    }
1675
1676
    /**
1677
     * @param array $meeting
1678
     *
1679
     * @return string
1680
     */
1681
    public function copyToRecordToLinkTool($meeting)
1682
    {
1683
        if (!isset($meeting['id'])) {
1684
            return '';
1685
        }
1686
1687
        return api_get_path(WEB_PLUGIN_PATH).
1688
            'Bbb/listing.php?'.$this->getUrlParams().'&action=copy_record_to_link_tool&id='.$meeting['id'];
1689
    }
1690
1691
    /**
1692
     * Function disabled
1693
     */
1694
    public function publishMeeting($id)
1695
    {
1696
        if (empty($id)) {
1697
            return false;
1698
        }
1699
1700
        $em = Database::getManager();
1701
        /** @var ConferenceMeetingRepository $repo */
1702
        $repo = $em->getRepository(ConferenceMeeting::class);
1703
1704
        $meeting = $repo->find($id);
1705
        if (!$meeting) {
1706
            return false;
1707
        }
1708
1709
        $meeting->setVisibility(1);
1710
        $em->flush();
1711
1712
        return true;
1713
    }
1714
1715
    /**
1716
     * Function disabled
1717
     */
1718
    public function unpublishMeeting($id)
1719
    {
1720
        if (empty($id)) {
1721
            return false;
1722
        }
1723
1724
        $em = Database::getManager();
1725
        /** @var ConferenceMeetingRepository $repo */
1726
        $repo = $em->getRepository(ConferenceMeeting::class);
1727
1728
        $meeting = $repo->find($id);
1729
        if (!$meeting) {
1730
            return false;
1731
        }
1732
1733
        $meeting->setVisibility(0);
1734
        $em->flush();
1735
1736
        return true;
1737
    }
1738
1739
    /**
1740
     * Get users online in the current course room.
1741
     *
1742
     * @return int The number of users currently connected to the videoconference
1743
     * @assert () > -1
1744
     */
1745
    public function getUsersOnlineInCurrentRoom()
1746
    {
1747
        $courseId = api_get_course_int_id();
1748
        $sessionId = api_get_session_id();
1749
1750
        $em = Database::getManager();
1751
        $repo = $em->getRepository(ConferenceMeeting::class);
1752
1753
        $qb = $repo->createQueryBuilder('m')
1754
            ->where('m.status = 1')
1755
            ->andWhere('m.accessUrl = :accessUrl')
1756
            ->setParameter('accessUrl', $this->accessUrl)
1757
            ->setMaxResults(1);
1758
1759
        if ($this->hasGroupSupport()) {
1760
            $groupId = api_get_group_id();
1761
            $qb->andWhere('m.course = :courseId')
1762
                ->andWhere('m.session = :sessionId')
1763
                ->andWhere('m.group = :groupId')
1764
                ->setParameter('courseId', $courseId)
1765
                ->setParameter('sessionId', $sessionId)
1766
                ->setParameter('groupId', $groupId);
1767
        } elseif ($this->isGlobalConferencePerUserEnabled()) {
1768
            $qb->andWhere('m.user = :userId')
1769
                ->setParameter('userId', $this->userId);
1770
        } else {
1771
            $qb->andWhere('m.course = :courseId')
1772
                ->andWhere('m.session = :sessionId')
1773
                ->setParameter('courseId', $courseId)
1774
                ->setParameter('sessionId', $sessionId);
1775
        }
1776
1777
        $meetingData = $qb->getQuery()->getOneOrNullResult();
1778
1779
        if (!$meetingData) {
1780
            return 0;
1781
        }
1782
        $pass = $meetingData->getModeratorPw();
1783
        $info = $this->getMeetingInfo([
1784
            'meetingId' => $meetingData->getRemoteId(),
1785
            'password' => $pass,
1786
        ]);
1787
        if ($info === false) {
1788
            $info = $this->getMeetingInfo([
1789
                'meetingId' => $meetingData->getId(),
1790
                'password' => $pass,
1791
            ]);
1792
        }
1793
1794
        if (!empty($info) && isset($info['participantCount'])) {
1795
            return (int) $info['participantCount'];
1796
        }
1797
1798
        return 0;
1799
    }
1800
1801
    /**
1802
     * @param int    $id
1803
     * @param string $recordId
1804
     *
1805
     * @return bool
1806
     */
1807
    public function regenerateRecording($id, $recordId = '')
1808
    {
1809
        if ($this->plugin->get('allow_regenerate_recording') !== 'true') {
1810
            return false;
1811
        }
1812
1813
        if (empty($id)) {
1814
            return false;
1815
        }
1816
1817
        $em = Database::getManager();
1818
        /** @var ConferenceMeetingRepository $repo */
1819
        $repo = $em->getRepository(ConferenceMeeting::class);
1820
1821
        $meetingData = $repo->findOneAsArrayById((int) $id);
1822
        if (!$meetingData) {
1823
            return false;
1824
        }
1825
1826
        Event::addEvent(
1827
            'bbb_regenerate_record',
1828
            'record_id',
1829
            (int) $recordId,
1830
            null,
1831
            api_get_user_id(),
1832
            api_get_course_int_id(),
1833
            api_get_session_id()
1834
        );
1835
1836
        /** @var ConferenceRecordingRepository $recordingRepo */
1837
        $recordingRepo = $em->getRepository(ConferenceRecordingRepository::class);
1838
        $recordings = $recordingRepo->findByMeetingRemoteId($meetingData['remoteId']);
1839
1840
        if (!empty($recordings) && isset($recordings['messageKey']) && $recordings['messageKey'] === 'noRecordings') {
1841
            if (!empty($meetingData['internalMeetingId'])) {
1842
                return $this->api->generateRecording(['recordId' => $meetingData['internalMeetingId']]);
1843
            }
1844
1845
            return false;
1846
        }
1847
1848
        if (!empty($recordings['records'])) {
1849
            foreach ($recordings['records'] as $record) {
1850
                if ($recordId == $record['recordId']) {
1851
                    return $this->api->generateRecording(['recordId' => $recordId]);
1852
                }
1853
            }
1854
        }
1855
1856
        return false;
1857
    }
1858
1859
    /**
1860
     * Deletes a recording of a meeting
1861
     *
1862
     * @param int $id ID of the recording
1863
     *
1864
     * @return bool
1865
     *
1866
     * @assert () === false
1867
     * @todo Also delete links and agenda items created from this recording
1868
     */
1869
    public function deleteRecording($id)
1870
    {
1871
        if (empty($id)) {
1872
            return false;
1873
        }
1874
1875
        $em = Database::getManager();
1876
1877
        /** @var ConferenceMeetingRepository $meetingRepo */
1878
        $meetingRepo = $em->getRepository(ConferenceMeeting::class);
1879
        $meetingData = $meetingRepo->findOneAsArrayById((int) $id);
1880
        if (!$meetingData) {
1881
            return false;
1882
        }
1883
1884
        Event::addEvent(
1885
            'bbb_delete_record',
1886
            'meeting_id',
1887
            $id,
1888
            null,
1889
            api_get_user_id(),
1890
            api_get_course_int_id(),
1891
            api_get_session_id()
1892
        );
1893
1894
        $delete = false;
1895
        $recordings = [];
1896
1897
        if (!empty($meetingData['remoteId'])) {
1898
            Event::addEvent(
1899
                'bbb_delete_record',
1900
                'remote_id',
1901
                $meetingData['remoteId'],
1902
                null,
1903
                api_get_user_id(),
1904
                api_get_course_int_id(),
1905
                api_get_session_id()
1906
            );
1907
1908
            /** @var ConferenceRecordingRepository $recordingRepo */
1909
            $recordingRepo = $em->getRepository(ConferenceRecording::class);
1910
            $recordings = $recordingRepo->findByMeetingRemoteId($meetingData['remoteId']);
1911
        }
1912
1913
        if (!empty($recordings) && isset($recordings['messageKey']) && $recordings['messageKey'] === 'noRecordings') {
1914
            $delete = true;
1915
        } elseif (!empty($recordings['records'])) {
1916
            $recordsToDelete = [];
1917
            foreach ($recordings['records'] as $record) {
1918
                $recordsToDelete[] = $record['recordId'];
1919
            }
1920
1921
            if (!empty($recordsToDelete)) {
1922
                $recordingParams = ['recordId' => implode(',', $recordsToDelete)];
1923
                Event::addEvent(
1924
                    'bbb_delete_record',
1925
                    'record_id_list',
1926
                    implode(',', $recordsToDelete),
1927
                    null,
1928
                    api_get_user_id(),
1929
                    api_get_course_int_id(),
1930
                    api_get_session_id()
1931
                );
1932
1933
                $result = $this->api->deleteRecordingsWithXmlResponseArray($recordingParams);
1934
1935
                if (!empty($result) && isset($result['deleted']) && $result['deleted'] === 'true') {
1936
                    $delete = true;
1937
                }
1938
            }
1939
        }
1940
1941
        if (!$delete) {
1942
            $delete = true;
1943
        }
1944
1945
        if ($delete) {
1946
            /** @var ConferenceActivityRepository $activityRepo */
1947
            $activityRepo = $em->getRepository(ConferenceActivity::class);
1948
            $activityRepo->closeAllByMeetingId((int) $id);
1949
1950
            $meeting = $meetingRepo->find((int) $id);
1951
            if ($meeting) {
1952
                $em->remove($meeting);
1953
            }
1954
1955
            $em->flush();
1956
        }
1957
1958
        return $delete;
1959
    }
1960
1961
    /**
1962
     * Creates a link in the links tool from the given videoconference recording
1963
     *
1964
     * @param int $id ID of the item in the plugin_bbb_meeting table
1965
     * @param string Hash identifying the recording, as provided by the API
1966
     *
1967
     * @return mixed ID of the newly created link, or false on error
1968
     * @assert (null, null) === false
1969
     * @assert (1, null) === false
1970
     * @assert (null, 'abcdefabcdefabcdefabcdef') === false
1971
     */
1972
    public function copyRecordingToLinkTool($id)
1973
    {
1974
        if (empty($id)) {
1975
            return false;
1976
        }
1977
1978
        $em = Database::getManager();
1979
        /** @var ConferenceMeetingRepository $repo */
1980
        $repo = $em->getRepository(ConferenceMeeting::class);
1981
1982
        $meetingData = $repo->findOneAsArrayById((int) $id);
1983
        if (!$meetingData || empty($meetingData['remoteId'])) {
1984
            return false;
1985
        }
1986
1987
        $records = $this->api->getRecordingsWithXmlResponseArray([
1988
            'meetingId' => $meetingData['remoteId']
1989
        ]);
1990
1991
        if (!empty($records)) {
1992
            if (isset($records['message']) && !empty($records['message'])) {
1993
                if ($records['messageKey'] == 'noRecordings') {
1994
                    return false;
1995
                }
1996
            } else {
1997
                $record = $records[0];
1998
                if (is_array($record) && isset($record['recordId'])) {
1999
                    $url = $record['playbackFormatUrl'];
2000
                    $link = new \Link();
2001
                    $params = [
2002
                        'url' => $url,
2003
                        'title' => $meetingData['title'],
2004
                    ];
2005
                    $id = $link->save($params);
2006
2007
                    return $id;
2008
                }
2009
            }
2010
        }
2011
2012
        return false;
2013
    }
2014
2015
    /**
2016
     * Checks if the video conference server is running.
2017
     * Function currently disabled (always returns 1)
2018
     * @return bool True if server is running, false otherwise
2019
     * @assert () === false
2020
     */
2021
    public function isServerRunning()
2022
    {
2023
        return true;
2024
        //return BigBlueButtonBN::isServerRunning($this->protocol.$this->url);
2025
    }
2026
2027
    /**
2028
     * Checks if the video conference plugin is properly configured
2029
     * @return bool True if plugin has a host and a salt, false otherwise
2030
     * @assert () === false
2031
     */
2032
    public function isServerConfigured()
2033
    {
2034
        $host = $this->plugin->get('host');
2035
2036
        if (empty($host)) {
2037
            return false;
2038
        }
2039
2040
        $salt = $this->plugin->get('salt');
2041
2042
        if (empty($salt)) {
2043
            return false;
2044
        }
2045
2046
        return true;
2047
        //return BigBlueButtonBN::isServerRunning($this->protocol.$this->url);
2048
    }
2049
2050
    /**
2051
     * Get active session in the all platform
2052
     */
2053
    public function getActiveSessionsCount(): int
2054
    {
2055
        $em = Database::getManager();
2056
        $qb = $em->createQueryBuilder();
2057
2058
        $qb->select('COUNT(m.id)')
2059
            ->from(ConferenceMeeting::class, 'm')
2060
            ->where('m.status = :status')
2061
            ->andWhere('m.accessUrl = :accessUrl')
2062
            ->setParameter('status', 1)
2063
            ->setParameter('accessUrl', $this->accessUrl);
2064
2065
        return (int) $qb->getQuery()->getSingleScalarResult();
2066
    }
2067
2068
    /**
2069
     * Get active session in the all platform
2070
     */
2071
    public function getActiveSessions(): array
2072
    {
2073
        $em = Database::getManager();
2074
        $repo = $em->getRepository(ConferenceMeeting::class);
2075
2076
        $qb = $repo->createQueryBuilder('m')
2077
            ->where('m.status = :status')
2078
            ->andWhere('m.accessUrl = :accessUrl')
2079
            ->setParameter('status', 1)
2080
            ->setParameter('accessUrl', $this->accessUrl);
2081
2082
        return $qb->getQuery()->getArrayResult();
2083
    }
2084
2085
    /**
2086
     * @param string $url
2087
     */
2088
    public function redirectToBBB($url)
2089
    {
2090
        if (file_exists(__DIR__.'/../config.vm.php')) {
2091
            // Using VM
2092
            echo Display::url($this->plugin->get_lang('ClickToContinue'), $url);
2093
            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...
2094
        } else {
2095
            // Classic
2096
            header("Location: $url");
2097
            exit;
2098
        }
2099
    }
2100
2101
    /**
2102
     * @return string
2103
     */
2104
    public function getConferenceUrl()
2105
    {
2106
        return api_get_path(WEB_PLUGIN_PATH).'Bbb/start.php?launch=1&'.$this->getUrlParams();
2107
    }
2108
2109
    /**
2110
     * Get the meeting info from DB by its name
2111
     *
2112
     * @param string $name
2113
     *
2114
     * @return array
2115
     */
2116
    public function findMeetingByName(string $name): ?array
2117
    {
2118
        $em = Database::getManager();
2119
        /** @var ConferenceMeetingRepository $repo */
2120
        $repo = $em->getRepository(ConferenceMeeting::class);
2121
2122
        // Selecciona campos que necesitamos, incluyendo remoteId
2123
        $qb = $repo->createQueryBuilder('m')
2124
            ->select('m.id AS id, m.title AS title, m.remoteId AS remote_id, m.createdAt AS created_at')
2125
            ->where('m.title = :name')
2126
            ->andWhere('m.accessUrl = :accessUrl')
2127
            ->setParameter('name', $name)
2128
            ->setParameter('accessUrl', api_get_url_entity($this->accessUrl))
2129
            ->setMaxResults(1);
2130
2131
        $row = $qb->getQuery()->getArrayResult();
2132
        if (!$row) {
2133
            return null;
2134
        }
2135
2136
        // Normalización mínima de fechas a string (opcional)
2137
        if (!empty($row[0]['created_at']) && $row[0]['created_at'] instanceof \DateTimeInterface) {
2138
            $row[0]['created_at'] = $row[0]['created_at']->format('Y-m-d H:i:s');
2139
        }
2140
2141
        return $row[0];
2142
    }
2143
2144
    /**
2145
     * Get the meeting info from DB by its name
2146
     *
2147
     * @param int $id
2148
     *
2149
     * @return array
2150
     */
2151
    public function getMeeting(int $id): ?array
2152
    {
2153
        $em = Database::getManager();
2154
        /** @var ConferenceMeetingRepository $repo */
2155
        $repo = $em->getRepository(ConferenceMeeting::class);
2156
2157
        return $repo->findOneAsArrayById($id);
2158
    }
2159
2160
    /**
2161
     * Get the meeting info.
2162
     *
2163
     * @param int $id
2164
     *
2165
     * @return array
2166
     */
2167
    public function getMeetingByRemoteId(string $id): ?array
2168
    {
2169
        $em = Database::getManager();
2170
        /** @var ConferenceMeetingRepository $repo */
2171
        $repo = $em->getRepository(ConferenceMeeting::class);
2172
2173
        return $repo->findOneByRemoteIdAndAccessUrl($id, $this->accessUrl);
2174
    }
2175
2176
    /**
2177
     * @param int $meetingId
2178
     *
2179
     * @return array
2180
     */
2181
    public function findConnectedMeetingParticipants(int $meetingId): array
2182
    {
2183
        $em = Database::getManager();
2184
        /** @var ConferenceActivityRepository $repo */
2185
        $repo = $em->getRepository(ConferenceActivity::class);
2186
2187
        $activities = $repo->createQueryBuilder('a')
2188
            ->where('a.meeting = :meetingId')
2189
            ->andWhere('a.inAt IS NOT NULL')
2190
            ->setParameter('meetingId', $meetingId)
2191
            ->getQuery()
2192
            ->getResult();
2193
2194
        $participantIds = [];
2195
        $return = [];
2196
2197
        foreach ($activities as $activity) {
2198
            $participant = $activity->getParticipant();
2199
            $participantId = $participant?->getId();
2200
2201
            if (!$participantId || in_array($participantId, $participantIds, true)) {
2202
                continue;
2203
            }
2204
            $participantIds[] = $participantId;
2205
2206
            // Reuse aggregator for each user
2207
            $agg = $this->getMeetingParticipantInfo($meetingId, $participantId);
2208
2209
            $return[] = [
2210
                'id'             => $activity->getId(),
2211
                'meeting_id'     => $meetingId,
2212
                'participant'    => api_get_user_entity($participantId),
2213
                'in_at'          => $activity->getInAt()?->format('Y-m-d H:i:s'),
2214
                'out_at'         => $activity->getOutAt()?->format('Y-m-d H:i:s'),
2215
                'online_seconds' => $agg['online_seconds'] ?? 0,
2216
                'metrics'        => $agg['metrics'] ?? [],
2217
            ];
2218
        }
2219
2220
        return $return;
2221
    }
2222
2223
    /**
2224
     * Check if the meeting has a capture.m4v video file. If exists then the has_video_m4v field is updated
2225
     *
2226
     * @param int $meetingId
2227
     *
2228
     * @return bool
2229
     */
2230
    public function checkDirectMeetingVideoUrl(int $meetingId): bool
2231
    {
2232
        $em = Database::getManager();
2233
        /** @var ConferenceMeetingRepository $repo */
2234
        $repo = $em->getRepository(ConferenceMeeting::class);
2235
2236
        $meetingInfo = $repo->findOneAsArrayById($meetingId);
2237
2238
        if (empty($meetingInfo) || !isset($meetingInfo['videoUrl'])) {
2239
            return false;
2240
        }
2241
2242
        $hasCapture = SocialManager::verifyUrl($meetingInfo['videoUrl'].'/capture.m4v');
2243
2244
        if ($hasCapture) {
2245
            $qb = $em->createQueryBuilder();
2246
            $qb->update(ConferenceMeeting::class, 'm')
2247
                ->set('m.hasVideoM4v', ':value')
2248
                ->where('m.id = :id')
2249
                ->setParameter('value', true)
2250
                ->setParameter('id', $meetingId)
2251
                ->getQuery()
2252
                ->execute();
2253
2254
            return true;
2255
        }
2256
2257
        return false;
2258
    }
2259
2260
    public static function showGlobalConferenceLink($userInfo)
2261
    {
2262
        if (empty($userInfo)) {
2263
            return false;
2264
        }
2265
2266
        $setting = api_get_plugin_setting('bbb', 'enable_global_conference');
2267
        $settingLink = api_get_plugin_setting('bbb', 'enable_global_conference_link');
2268
2269
        if ($setting === 'true' && $settingLink === 'true') {
2270
            $allowedRoles = api_get_plugin_setting('bbb', 'global_conference_allow_roles');
2271
            $allowedRoles = self::normalizeSettingToArray($allowedRoles);
2272
2273
            if (api_is_platform_admin()) {
2274
                $userInfo['status'] = PLATFORM_ADMIN;
2275
            }
2276
2277
            if (!empty($allowedRoles)) {
2278
                $needle = (string) $userInfo['status'];
2279
                $haystack = array_map('strval', $allowedRoles);
2280
2281
                if (!in_array($needle, $haystack, true)) {
2282
                    return false;
2283
                }
2284
            }
2285
2286
            return true;
2287
        }
2288
2289
        return false;
2290
    }
2291
2292
    /**
2293
     * Build the callback URL for BBB webhooks, signed with HMAC using the plugin salt.
2294
     */
2295
    private function buildWebhookCallbackUrl(?string $meetingId = null): string
2296
    {
2297
        $base = rtrim(api_get_path(WEB_PLUGIN_PATH), '/').'/Bbb/webhook.php';
2298
2299
        $au  = (int) $this->accessUrl;              // current access_url_id
2300
        $mid = (string) ($meetingId ?? '');         // meetingID (empty if global)
2301
2302
        $algo   = $this->plugin->webhooksHashAlgo(); // 'sha256' | 'sha1'
2303
        $secret = (string) $this->salt;
2304
2305
        // Stable signature without timestamp (MUST match webhook.php)
2306
        $payload = $au.'|'.$mid;
2307
        $sig     = hash_hmac($algo, $payload, $secret);
2308
2309
        $qs = [
2310
            'au'  => $au,
2311
            'mid' => $mid,
2312
            'sig' => $sig,
2313
        ];
2314
2315
        $url = $base.'?'.http_build_query($qs);
2316
2317
        error_log('[BBB hooks] callbackURL='.$url);
2318
2319
        return $url;
2320
    }
2321
2322
    /**
2323
     * Call to the BBB hooks API (GET with sha1(call+query+salt) checksum).
2324
     * Returns a normalized array. Uses cURL with timeouts and TLS verification.
2325
     */
2326
    private function bbbHooksRequest(string $call, array $params): array
2327
    {
2328
        try {
2329
            $baseRaw = $this->urlWithProtocol ?: '';
2330
            if ($baseRaw === '') {
2331
                return ['returncode' => 'FAILED', 'messageKey' => 'missingServerUrl'];
2332
            }
2333
2334
            $base = rtrim($baseRaw, '/').'/';
2335
            $apiBase = $base . 'api/';
2336
2337
            $query    = http_build_query($params);
2338
            $checksum = sha1($call . $query . $this->salt);
2339
            $url      = $apiBase . $call . '?' . $query . '&checksum=' . $checksum;
2340
2341
            error_log('[BBB hooks] CALL='.$call.' URL='.$url);
2342
2343
            $ch = curl_init($url);
2344
            curl_setopt_array($ch, [
2345
                CURLOPT_RETURNTRANSFER => true,
2346
                CURLOPT_TIMEOUT        => 8,
2347
                CURLOPT_CONNECTTIMEOUT => 3,
2348
                CURLOPT_FOLLOWLOCATION => false,
2349
                CURLOPT_SSL_VERIFYPEER => true,
2350
                CURLOPT_SSL_VERIFYHOST => 2,
2351
                CURLOPT_USERAGENT      => 'Chamilo-BBB-Webhooks/1.0',
2352
            ]);
2353
            $body = curl_exec($ch);
2354
            $err  = curl_error($ch);
2355
            $info = curl_getinfo($ch);
2356
            curl_close($ch);
2357
2358
            error_log('[BBB hooks] RAW='.substr($body, 0, 2000));
2359
2360
            if ($body === false || !$body) {
2361
                $this->dlog('bbbHooksRequest: empty body', ['curl_err' => $err]);
2362
                return ['returncode' => 'FAILED', 'messageKey' => 'noResponse', 'error' => $err ?: ''];
2363
            }
2364
2365
            $xml = @simplexml_load_string($body);
2366
            if (!$xml) {
2367
                $this->dlog('bbbHooksRequest: invalid XML body', ['body_sample' => substr($body, 0, 200)]);
2368
                return ['returncode' => 'FAILED', 'messageKey' => 'invalidXML'];
2369
            }
2370
2371
            $out = ['returncode' => (string)($xml->returncode ?? '')];
2372
            if (isset($xml->messageKey)) { $out['messageKey'] = (string)$xml->messageKey; }
2373
            if (isset($xml->message))    { $out['message']    = (string)$xml->message; }
2374
2375
            if ($call === 'hooks/list' && isset($xml->hooks)) {
2376
                $out['hooks'] = [];
2377
                foreach (($xml->hooks->hook ?? []) as $h) {
2378
                    $out['hooks'][] = [
2379
                        'id'          => isset($h->id) ? (string)$h->id : '',
2380
                        'callbackURL' => isset($h->callbackURL) ? (string)$h->callbackURL : '',
2381
                        'meetingID'   => isset($h->meetingID) ? (string)$h->meetingID : '',
2382
                    ];
2383
                }
2384
            }
2385
            if ($call === 'hooks/create' && isset($xml->hookID)) {
2386
                $out['hookID'] = (string)$xml->hookID;
2387
            }
2388
2389
            $this->dlog('bbbHooksRequest: parsed response', $out);
2390
            return $out;
2391
2392
        } catch (\Throwable $e) {
2393
            $this->dlog('bbbHooksRequest: exception', ['error' => $e->getMessage()]);
2394
            return ['returncode' => 'FAILED', 'messageKey' => 'exception'];
2395
        }
2396
    }
2397
2398
    /**
2399
     * Registers one webhook per meeting (idempotent).
2400
     * If a hook with that meeting ID already exists, don't create another one.
2401
     */
2402
    private function registerWebhookForMeeting(string $meetingId): void
2403
    {
2404
        if ($meetingId === '') {
2405
            $this->dlog('registerWebhookForMeeting: empty meetingId');
2406
            return;
2407
        }
2408
2409
        $this->dlog('registerWebhookForMeeting: start', ['meetingId' => $meetingId]);
2410
2411
        $list = $this->bbbHooksRequest('hooks/list', []);
2412
        if (($list['returncode'] ?? '') === 'SUCCESS') {
2413
            foreach (($list['hooks'] ?? []) as $h) {
2414
                if (($h['meetingID'] ?? '') === $meetingId) {
2415
                    $this->dlog('registerWebhookForMeeting: already registered', $h);
2416
                    return;
2417
                }
2418
            }
2419
        } else {
2420
            $this->dlog('registerWebhookForMeeting: hooks/list failed', $list);
2421
        }
2422
2423
        $callback = $this->buildWebhookCallbackUrl($meetingId);
2424
        $params = ['callbackURL' => $callback, 'meetingID' => $meetingId];
2425
2426
        $events = trim((string)$this->plugin->webhooksEventFilter());
2427
        if ($events !== '') {
2428
            $events = implode(',', array_map('trim', explode(',', strtolower($events))));
2429
            $params['events'] = $events;
2430
        }
2431
2432
        $this->dlog('registerWebhookForMeeting: creating hook', $params);
2433
        $res = $this->bbbHooksRequest('hooks/create', $params);
2434
2435
        if (($res['returncode'] ?? '') !== 'SUCCESS') {
2436
            $this->dlog('registerWebhookForMeeting: create failed', $res);
2437
        } else {
2438
            $this->dlog('registerWebhookForMeeting: create success', $res);
2439
        }
2440
    }
2441
2442
    /** Ensure webhooks when joining an already-existing meeting */
2443
    public function ensureWebhooksOnJoin(string $remoteId): void
2444
    {
2445
        try {
2446
            if (!$this->plugin->webhooksEnabled()) {
2447
                if ($this->debug) { error_log('[BBB] ensureWebhooksOnJoin: webhooks disabled'); }
2448
                return;
2449
            }
2450
            $scope = $this->plugin->webhooksScope(); // 'per_meeting' | 'global'
2451
            if ($this->debug) { error_log('[BBB] ensureWebhooksOnJoin: scope='.$scope.' remoteId='.$remoteId); }
2452
2453
            if ($scope === 'per_meeting') {
2454
                if ($remoteId === '') {
2455
                    if ($this->debug) { error_log('[BBB] ensureWebhooksOnJoin: empty remoteId'); }
2456
                    return;
2457
                }
2458
                // private helper already logs internals
2459
                $this->registerWebhookForMeeting($remoteId);
2460
            } else {
2461
                $this->ensureGlobalWebhook();
2462
            }
2463
        } catch (\Throwable $e) {
2464
            if ($this->debug) { error_log('[BBB] ensureWebhooksOnJoin: exception '.$e->getMessage()); }
2465
        }
2466
    }
2467
2468
    /**
2469
     * Ensures a single global webhook (without a meeting ID).
2470
     * Query hooks/list and create it only if it doesn't exist with the same callbackURL.
2471
     */
2472
    private function ensureGlobalWebhook(): void
2473
    {
2474
        $callback = $this->buildWebhookCallbackUrl(null);
2475
        $this->dlog('ensureGlobalWebhook: start', ['callback' => $callback]);
2476
2477
        $list = $this->bbbHooksRequest('hooks/list', []);
2478
        if (($list['returncode'] ?? '') !== 'SUCCESS') {
2479
            $this->dlog('ensureGlobalWebhook: hooks/list failed', $list);
2480
            return;
2481
        }
2482
2483
        foreach (($list['hooks'] ?? []) as $h) {
2484
            if (($h['callbackURL'] ?? '') === $callback && ($h['meetingID'] ?? '') === '') {
2485
                $this->dlog('ensureGlobalWebhook: global hook already exists', $h);
2486
                return;
2487
            }
2488
        }
2489
2490
        $params = ['callbackURL' => $callback];
2491
        $events = trim((string)$this->plugin->webhooksEventFilter());
2492
        if ($events !== '') {
2493
            $events = implode(',', array_map('trim', explode(',', strtolower($events))));
2494
            $params['events'] = $events;
2495
        }
2496
2497
        $this->dlog('ensureGlobalWebhook: creating', $params);
2498
        $res = $this->bbbHooksRequest('hooks/create', $params);
2499
        if (($res['returncode'] ?? '') !== 'SUCCESS') {
2500
            $this->dlog('ensureGlobalWebhook: create failed', $res);
2501
        } else {
2502
            $this->dlog('ensureGlobalWebhook: create success', $res);
2503
        }
2504
    }
2505
2506
    public function ensureGlobalHook(): void
2507
    {
2508
        try {
2509
            error_log('[BBB hooks] ensureGlobalHook() called');
2510
            $this->ensureGlobalWebhook();
2511
            error_log('[BBB hooks] ensureGlobalHook() done');
2512
        } catch (\Throwable $e) {
2513
            error_log('[BBB hooks] ensureGlobalHook() error: '.$e->getMessage());
2514
        }
2515
    }
2516
2517
    public function ensureHookForMeeting(string $remoteId): void
2518
    {
2519
        try {
2520
            if ($remoteId === '') {
2521
                error_log('[BBB hooks] ensureHookForMeeting(): empty remoteId');
2522
                return;
2523
            }
2524
            error_log('[BBB hooks] ensureHookForMeeting('.$remoteId.') called');
2525
            $this->registerWebhookForMeeting($remoteId);
2526
            error_log('[BBB hooks] ensureHookForMeeting('.$remoteId.') done');
2527
        } catch (\Throwable $e) {
2528
            error_log('[BBB hooks] ensureHookForMeeting('.$remoteId.') error: '.$e->getMessage());
2529
        }
2530
    }
2531
2532
    private function dlog(string $msg, array $ctx = []): void
2533
    {
2534
        if (!$this->debug) { return; }
2535
        if (!empty($ctx)) {
2536
            $msg .= ' | ' . json_encode($ctx, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
2537
        }
2538
        error_log('[BBB] ' . $msg);
2539
    }
2540
2541
}
2542