Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace mod_bigbluebuttonbn\local\proxy;
18
 
19
use cache;
20
use completion_info;
21
use Exception;
22
use mod_bigbluebuttonbn\completion\custom_completion;
23
use mod_bigbluebuttonbn\instance;
24
use mod_bigbluebuttonbn\local\config;
25
use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception;
26
use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception;
27
use moodle_url;
28
use stdClass;
29
use user_picture;
30
 
31
/**
32
 * The bigbluebutton proxy class.
33
 *
34
 * This class acts as a proxy between Moodle and the BigBlueButton API server,
35
 * and handles all requests relating to the server and meetings.
36
 *
37
 * @package   mod_bigbluebuttonbn
38
 * @copyright 2010 onwards, Blindside Networks Inc
39
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40
 * @author    Jesus Federico  (jesus [at] blindsidenetworks [dt] com)
41
 */
42
class bigbluebutton_proxy extends proxy_base {
43
 
44
    /**
45
     * Minimum poll interval for remote bigbluebutton server in seconds.
46
     */
47
    const MIN_POLL_INTERVAL = 2;
48
 
49
    /**
50
     * Default poll interval for remote bigbluebutton server in seconds.
51
     */
52
    const DEFAULT_POLL_INTERVAL = 5;
53
 
54
    /**
55
     * Builds and returns a url for joining a BigBlueButton meeting.
56
     *
57
     * @param instance $instance
58
     * @param string|null $createtime
59
     *
60
     * @return string
61
     */
62
    public static function get_join_url(
63
        instance $instance,
64
        ?string $createtime
65
    ): string {
66
        return self::internal_get_join_url($instance, $createtime);
67
    }
68
 
69
    /**
70
     * Builds and returns a url for joining a BigBlueButton meeting.
71
     *
72
     * @param instance $instance
73
     * @param string|null $createtime
74
     * @param string $username
75
     * @return string
76
     */
77
    public static function get_guest_join_url(
78
        instance $instance,
79
        ?string $createtime,
80
        string $username
81
    ): string {
82
        return self::internal_get_join_url($instance, $createtime, $username, true);
83
    }
84
 
85
    /**
86
     * Internal helper method to builds and returns a url for joining a BigBlueButton meeting.
87
     *
88
     * @param instance $instance
89
     * @param string|null $jointime = null
90
     * @param string|null $userfullname
91
     * @param bool $isguestjoin
92
     * @return string
93
     */
94
    private static function internal_get_join_url(
95
        instance $instance,
96
        ?string $jointime,
97
        string $userfullname = null,
98
        bool $isguestjoin = false
99
    ): string {
100
        $data = [
101
            'meetingID' => $instance->get_meeting_id(),
102
            'fullName' => $userfullname ?? $instance->get_user_fullname(),
103
            'password' => $instance->get_current_user_password(),
104
            'logoutURL' => $isguestjoin ? $instance->get_guest_access_url()->out(false) : $instance->get_logout_url()->out(false),
105
            'role' => $instance->get_current_user_role()
106
        ];
107
 
108
        if (!$isguestjoin) {
109
            $data['userID'] = $instance->get_user_id();
110
            $data['guest'] = "false";
111
        } else {
112
            $data['guest'] = "true";
113
        }
114
 
115
        if (!is_null($jointime)) {
116
            $data['createTime'] = $jointime;
117
        }
118
        $currentlang = current_language();
119
        if (!empty(trim($currentlang))) {
120
            $data['userdata-bbb_override_default_locale'] = $currentlang;
121
        }
122
        if ($instance->is_profile_picture_enabled()) {
123
            $user = $instance->get_user();
124
            if (!empty($user->picture)) {
125
                $data['avatarURL'] = self::get_avatar_url($user)->out(false);
126
            }
127
        }
128
        return self::action_url('join', $data, [], $instance->get_instance_id());
129
    }
130
 
131
    /**
132
     * Get user avatar URL
133
     *
134
     * @param stdClass $user
135
     * @return moodle_url
136
     */
137
    private static function get_avatar_url(stdClass $user): moodle_url {
138
        global $PAGE;
139
        $userpicture = new user_picture($user);
140
        $userpicture->includetoken = true;
141
        $userpicture->size = 3; // Size f3.
142
        return $userpicture->get_url($PAGE);
143
    }
144
 
145
    /**
146
     * Perform api request on BBB.
147
     *
148
     * @return null|string
149
     */
150
    public static function get_server_version(): ?string {
151
        $cache = cache::make('mod_bigbluebuttonbn', 'serverinfo');
152
        $serverversion = $cache->get('serverversion');
153
 
154
        if (!$serverversion) {
155
            $xml = self::fetch_endpoint_xml('');
156
            if (!$xml || $xml->returncode != 'SUCCESS') {
157
                return null;
158
            }
159
 
160
            if (!isset($xml->version)) {
161
                return null;
162
            }
163
 
164
            $serverversion = (string) $xml->version;
165
            $cache->set('serverversion', $serverversion);
166
        }
167
 
168
        return (double) $serverversion;
169
    }
170
 
171
    /**
172
     * Helper for getting the owner userid of a bigbluebuttonbn instance.
173
     *
174
     * @param stdClass $bigbluebuttonbn BigBlueButtonBN instance
175
     * @return int ownerid (a valid user id or null if not registered/found)
176
     */
177
    public static function get_instance_ownerid(stdClass $bigbluebuttonbn): int {
178
        global $DB;
179
 
180
        $filters = [
181
            'bigbluebuttonbnid' => $bigbluebuttonbn->id,
182
            'log' => 'Add',
183
        ];
184
 
185
        return (int) $DB->get_field('bigbluebuttonbn_logs', 'userid', $filters);
186
    }
187
 
188
    /**
189
     * Helper evaluates if a voicebridge number is unique.
190
     *
191
     * @param int $instance
192
     * @param int $voicebridge
193
     * @return bool
194
     */
195
    public static function is_voicebridge_number_unique(int $instance, int $voicebridge): bool {
196
        global $DB;
197
        if ($voicebridge == 0) {
198
            return true;
199
        }
200
        $select = 'voicebridge = ' . $voicebridge;
201
        if ($instance != 0) {
202
            $select .= ' AND id <>' . $instance;
203
        }
204
        if (!$DB->get_records_select('bigbluebuttonbn', $select)) {
205
            return true;
206
        }
207
        return false;
208
    }
209
 
210
    /**
211
     * Helper function validates a remote resource.
212
     *
213
     * @param string $url
214
     * @return bool
215
     */
216
    public static function is_remote_resource_valid(string $url): bool {
217
        $urlhost = parse_url($url, PHP_URL_HOST);
218
        $serverurlhost = parse_url(\mod_bigbluebuttonbn\local\config::get('server_url'), PHP_URL_HOST);
219
 
220
        if ($urlhost == $serverurlhost) {
221
            // Skip validation when the recording URL host is the same as the configured BBB server.
222
            return true;
223
        }
224
 
225
        $cache = cache::make('mod_bigbluebuttonbn', 'validatedurls');
226
 
227
        if ($cachevalue = $cache->get($urlhost)) {
228
            // Skip validation when the recording URL was already validated.
229
            return $cachevalue == 1;
230
        }
231
 
232
        $curl = new curl();
233
        $curl->head($url);
234
 
235
        $isvalid = false;
236
        if ($info = $curl->get_info()) {
237
            if ($info['http_code'] == 200) {
238
                $isvalid = true;
239
            } else {
240
                debugging(
241
                    "Resources hosted by {$urlhost} are unreachable. Server responded with {$info['http_code']}",
242
                    DEBUG_DEVELOPER
243
                );
244
                $isvalid = false;
245
            }
246
 
247
            // Note: When a cache key is not found, it returns false.
248
            // We need to distinguish between a result not found, and an invalid result.
249
            $cache->set($urlhost, $isvalid ? 1 : 0);
250
        }
251
 
252
        return $isvalid;
253
    }
254
 
255
    /**
256
     * Helper function enqueues one user for being validated as for completion.
257
     *
258
     * @param stdClass $bigbluebuttonbn
259
     * @param int $userid
260
     * @return void
261
     */
262
    public static function enqueue_completion_event(stdClass $bigbluebuttonbn, int $userid): void {
263
        try {
264
            // Create the instance of completion_update_state task.
265
            $task = new \mod_bigbluebuttonbn\task\completion_update_state();
266
            // Add custom data.
267
            $data = [
268
                'bigbluebuttonbn' => $bigbluebuttonbn,
269
                'userid' => $userid,
270
            ];
271
            $task->set_custom_data($data);
272
            // CONTRIB-7457: Task should be executed by a user, maybe Teacher as Student won't have rights for overriding.
273
            // $ task -> set_userid ( $ user -> id );.
274
            // Enqueue it.
275
            \core\task\manager::queue_adhoc_task($task);
276
        } catch (Exception $e) {
277
            mtrace("Error while enqueuing completion_update_state task. " . (string) $e);
278
        }
279
    }
280
 
281
    /**
282
     * Helper function enqueues completion trigger.
283
     *
284
     * @param stdClass $bigbluebuttonbn
285
     * @param int $userid
286
     * @return void
287
     */
288
    public static function update_completion_state(stdClass $bigbluebuttonbn, int $userid) {
289
        global $CFG;
290
        require_once($CFG->libdir . '/completionlib.php');
291
        list($course, $cm) = get_course_and_cm_from_instance($bigbluebuttonbn, 'bigbluebuttonbn');
292
        $completion = new completion_info($course);
293
        if (!$completion->is_enabled($cm)) {
294
            mtrace("Completion not enabled");
295
            return;
296
        }
297
 
298
        $bbbcompletion = new custom_completion($cm, $userid);
299
        if ($bbbcompletion->get_overall_completion_state()) {
300
            mtrace("Completion for userid $userid and bigbluebuttonid {$bigbluebuttonbn->id} updated.");
301
            $completion->update_state($cm, COMPLETION_COMPLETE, $userid, true);
302
        } else {
303
            // Still update state to current value (prevent unwanted caching).
304
            $completion->update_state($cm, COMPLETION_UNKNOWN, $userid);
305
            mtrace("Activity not completed for userid $userid and bigbluebuttonid {$bigbluebuttonbn->id}.");
306
        }
307
    }
308
 
309
    /**
310
     * Helper function returns an array with the profiles (with features per profile) for the different types
311
     * of bigbluebuttonbn instances.
312
     *
313
     * @return array
314
     */
315
    public static function get_instance_type_profiles(): array {
316
        $instanceprofiles = [
317
            instance::TYPE_ALL => [
318
                'id' => instance::TYPE_ALL,
319
                'name' => get_string('instance_type_default', 'bigbluebuttonbn'),
320
                'features' => ['all']
321
            ],
322
            instance::TYPE_ROOM_ONLY => [
323
                'id' => instance::TYPE_ROOM_ONLY,
324
                'name' => get_string('instance_type_room_only', 'bigbluebuttonbn'),
325
                'features' => ['showroom', 'welcomemessage', 'voicebridge', 'waitformoderator', 'userlimit',
326
                    'recording', 'sendnotifications', 'lock', 'preuploadpresentation', 'permissions', 'schedule', 'groups',
327
                    'modstandardelshdr', 'availabilityconditionsheader', 'tagshdr', 'competenciessection',
328
                    'completionattendance', 'completionengagement', 'availabilityconditionsheader']
329
            ],
330
            instance::TYPE_RECORDING_ONLY => [
331
                'id' => instance::TYPE_RECORDING_ONLY,
332
                'name' => get_string('instance_type_recording_only', 'bigbluebuttonbn'),
333
                'features' => ['showrecordings', 'importrecordings', 'availabilityconditionsheader']
334
            ],
335
        ];
336
        return $instanceprofiles;
337
    }
338
 
339
    /**
340
     * Helper function returns an array with the profiles (with features per profile) for the different types
341
     * of bigbluebuttonbn instances that the user is allowed to create.
342
     *
343
     * @param bool $room
344
     * @param bool $recording
345
     *
346
     * @return array
347
     */
348
    public static function get_instance_type_profiles_create_allowed(bool $room, bool $recording): array {
349
        $profiles = self::get_instance_type_profiles();
350
        if (!$room) {
351
            unset($profiles[instance::TYPE_ROOM_ONLY]);
352
            unset($profiles[instance::TYPE_ALL]);
353
        }
354
        if (!$recording) {
355
            unset($profiles[instance::TYPE_RECORDING_ONLY]);
356
            unset($profiles[instance::TYPE_ALL]);
357
        }
358
        return $profiles;
359
    }
360
 
361
    /**
362
     * Helper function returns an array with the profiles (with features per profile) for the different types
363
     * of bigbluebuttonbn instances.
364
     *
365
     * @param array $profiles
366
     *
367
     * @return array
368
     */
369
    public static function get_instance_profiles_array(array $profiles = []): array {
370
        $profilesarray = [];
371
        foreach ($profiles as $key => $profile) {
372
            $profilesarray[$profile['id']] = $profile['name'];
373
        }
374
        return $profilesarray;
375
    }
376
 
377
    /**
378
     * Return the status of an activity [open|not_started|ended].
379
     *
380
     * @param instance $instance
381
     * @return string
382
     */
383
    public static function view_get_activity_status(instance $instance): string {
384
        $now = time();
385
        if (!empty($instance->get_instance_var('openingtime')) && $now < $instance->get_instance_var('openingtime')) {
386
            // The activity has not been opened.
387
            return 'not_started';
388
        }
389
        if (!empty($instance->get_instance_var('closingtime')) && $now > $instance->get_instance_var('closingtime')) {
390
            // The activity has been closed.
391
            return 'ended';
392
        }
393
        // The activity is open.
394
        return 'open';
395
    }
396
 
397
    /**
398
     * Ensure that the remote server was contactable.
399
     *
400
     * @param instance $instance
401
     */
402
    public static function require_working_server(instance $instance): void {
403
        $version = null;
404
        try {
405
            $version = self::get_server_version();
406
        } catch (server_not_available_exception $e) {
407
            self::handle_server_not_available($instance);
408
        }
409
 
410
        if (empty($version)) {
411
            self::handle_server_not_available($instance);
412
        }
413
    }
414
 
415
    /**
416
     * Handle the server not being available.
417
     *
418
     * @param instance $instance
419
     */
420
    public static function handle_server_not_available(instance $instance): void {
421
        \core\notification::add(
422
            self::get_server_not_available_message($instance),
423
            \core\notification::ERROR
424
        );
425
        redirect(self::get_server_not_available_url($instance));
426
    }
427
 
428
    /**
429
     * Get message when server not available
430
     *
431
     * @param instance $instance
432
     * @return string
433
     */
434
    public static function get_server_not_available_message(instance $instance): string {
435
        if ($instance->is_admin()) {
436
            return get_string('view_error_unable_join', 'mod_bigbluebuttonbn');
437
        } else if ($instance->is_moderator()) {
438
            return get_string('view_error_unable_join_teacher', 'mod_bigbluebuttonbn');
439
        } else {
440
            return get_string('view_error_unable_join_student', 'mod_bigbluebuttonbn');
441
        }
442
    }
443
 
444
    /**
445
     * Get URL to the page displaying that the server is not available
446
     *
447
     * @param instance $instance
448
     * @return string
449
     */
450
    public static function get_server_not_available_url(instance $instance): string {
451
        if ($instance->is_admin()) {
452
            return new moodle_url('/admin/settings.php', ['section' => 'modsettingbigbluebuttonbn']);
453
        } else if ($instance->is_moderator()) {
454
            return new moodle_url('/course/view.php', ['id' => $instance->get_course_id()]);
455
        } else {
456
            return new moodle_url('/course/view.php', ['id' => $instance->get_course_id()]);
457
        }
458
    }
459
 
460
    /**
461
     * Create a Meeting
462
     *
463
     * @param array $data
464
     * @param array $metadata
465
     * @param string|null $presentationname
466
     * @param string|null $presentationurl
467
     * @param int|null $instanceid
468
     * @return array
469
     * @throws bigbluebutton_exception
470
     */
471
    public static function create_meeting(
472
        array $data,
473
        array $metadata,
474
        ?string $presentationname = null,
475
        ?string $presentationurl = null,
476
        ?int $instanceid = null
477
    ): array {
478
        $createmeetingurl = self::action_url('create', $data, $metadata, $instanceid);
479
 
480
        $curl = new curl();
481
        if (!is_null($presentationname) && !is_null($presentationurl)) {
482
            $payload = "<?xml version='1.0' encoding='UTF-8'?><modules><module name='presentation'><document url='" .
483
                $presentationurl . "' /></module></modules>";
484
 
485
            $xml = $curl->post($createmeetingurl, $payload);
486
        } else {
487
            $xml = $curl->get($createmeetingurl);
488
        }
489
 
490
        self::assert_returned_xml($xml);
491
 
492
        if (empty($xml->meetingID)) {
493
            throw new bigbluebutton_exception('general_error_cannot_create_meeting');
494
        }
495
 
496
        if ($xml->hasBeenForciblyEnded === 'true') {
497
            throw new bigbluebutton_exception('index_error_forciblyended');
498
        }
499
 
500
        return [
501
            'meetingID' => (string) $xml->meetingID,
502
            'internalMeetingID' => (string) $xml->internalMeetingID,
503
            'attendeePW' => (string) $xml->attendeePW,
504
            'moderatorPW' => (string) $xml->moderatorPW
505
        ];
506
    }
507
 
508
    /**
509
     * Get meeting info for a given meeting id
510
     *
511
     * @param string $meetingid
512
     * @param int|null $instanceid
513
     * @return array
514
     */
515
    public static function get_meeting_info(string $meetingid, ?int $instanceid = null): array {
516
        $xmlinfo = self::fetch_endpoint_xml('getMeetingInfo', ['meetingID' => $meetingid], [], $instanceid);
517
        self::assert_returned_xml($xmlinfo, ['meetingid' => $meetingid]);
518
        return (array) $xmlinfo;
519
    }
520
 
521
    /**
522
     * Perform end meeting on BBB.
523
     *
524
     * @param string $meetingid
525
     * @param string $modpw
526
     * @param int|null $instanceid
527
     */
528
    public static function end_meeting(string $meetingid, string $modpw, ?int $instanceid = null): void {
529
        $xml = self::fetch_endpoint_xml('end', ['meetingID' => $meetingid, 'password' => $modpw], [], $instanceid);
530
        self::assert_returned_xml($xml, ['meetingid' => $meetingid]);
531
    }
532
 
533
    /**
534
     * Helper evaluates if the bigbluebutton server used belongs to blindsidenetworks domain.
535
     *
536
     * @return bool
537
     */
538
    public static function is_bn_server() {
539
        if (config::get('bn_server')) {
540
            return true;
541
        }
542
        $parsedurl = parse_url(config::get('server_url'));
543
        if (!isset($parsedurl['host'])) {
544
            return false;
545
        }
546
        $h = $parsedurl['host'];
547
        $hends = explode('.', $h);
548
        $hendslength = count($hends);
549
        return ($hends[$hendslength - 1] == 'com' && $hends[$hendslength - 2] == 'blindsidenetworks');
550
    }
551
 
552
    /**
553
     * Get the poll interval as it is set in the configuration
554
     *
555
     * If configuration value is under the threshold of {@see self::MIN_POLL_INTERVAL},
556
     * then return the {@see self::MIN_POLL_INTERVAL} value.
557
     *
558
     * @return int the poll interval in seconds
559
     */
560
    public static function get_poll_interval(): int {
561
        $pollinterval = intval(config::get('poll_interval'));
562
        if ($pollinterval < self::MIN_POLL_INTERVAL) {
563
            $pollinterval = self::MIN_POLL_INTERVAL;
564
        }
565
        return $pollinterval;
566
    }
567
}