Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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,
1441 ariadna 97
        ?string $userfullname = null,
1 efrain 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',
1441 ariadna 328
                    'completionattendance', 'completionengagement', 'activitycompletionheader', 'modstandardgrade',
329
                ],
1 efrain 330
            ],
331
            instance::TYPE_RECORDING_ONLY => [
332
                'id' => instance::TYPE_RECORDING_ONLY,
333
                'name' => get_string('instance_type_recording_only', 'bigbluebuttonbn'),
334
                'features' => ['showrecordings', 'importrecordings', 'availabilityconditionsheader']
335
            ],
336
        ];
337
        return $instanceprofiles;
338
    }
339
 
340
    /**
341
     * Helper function returns an array with the profiles (with features per profile) for the different types
342
     * of bigbluebuttonbn instances that the user is allowed to create.
343
     *
344
     * @param bool $room
345
     * @param bool $recording
346
     *
347
     * @return array
348
     */
349
    public static function get_instance_type_profiles_create_allowed(bool $room, bool $recording): array {
350
        $profiles = self::get_instance_type_profiles();
351
        if (!$room) {
352
            unset($profiles[instance::TYPE_ROOM_ONLY]);
353
            unset($profiles[instance::TYPE_ALL]);
354
        }
355
        if (!$recording) {
356
            unset($profiles[instance::TYPE_RECORDING_ONLY]);
357
            unset($profiles[instance::TYPE_ALL]);
358
        }
359
        return $profiles;
360
    }
361
 
362
    /**
363
     * Helper function returns an array with the profiles (with features per profile) for the different types
364
     * of bigbluebuttonbn instances.
365
     *
366
     * @param array $profiles
367
     *
368
     * @return array
369
     */
370
    public static function get_instance_profiles_array(array $profiles = []): array {
371
        $profilesarray = [];
372
        foreach ($profiles as $key => $profile) {
373
            $profilesarray[$profile['id']] = $profile['name'];
374
        }
375
        return $profilesarray;
376
    }
377
 
378
    /**
379
     * Return the status of an activity [open|not_started|ended].
380
     *
381
     * @param instance $instance
382
     * @return string
383
     */
384
    public static function view_get_activity_status(instance $instance): string {
385
        $now = time();
386
        if (!empty($instance->get_instance_var('openingtime')) && $now < $instance->get_instance_var('openingtime')) {
387
            // The activity has not been opened.
388
            return 'not_started';
389
        }
390
        if (!empty($instance->get_instance_var('closingtime')) && $now > $instance->get_instance_var('closingtime')) {
391
            // The activity has been closed.
392
            return 'ended';
393
        }
394
        // The activity is open.
395
        return 'open';
396
    }
397
 
398
    /**
399
     * Ensure that the remote server was contactable.
400
     *
401
     * @param instance $instance
402
     */
403
    public static function require_working_server(instance $instance): void {
404
        $version = null;
405
        try {
406
            $version = self::get_server_version();
407
        } catch (server_not_available_exception $e) {
408
            self::handle_server_not_available($instance);
409
        }
410
 
411
        if (empty($version)) {
412
            self::handle_server_not_available($instance);
413
        }
414
    }
415
 
416
    /**
417
     * Handle the server not being available.
418
     *
419
     * @param instance $instance
420
     */
421
    public static function handle_server_not_available(instance $instance): void {
422
        \core\notification::add(
423
            self::get_server_not_available_message($instance),
424
            \core\notification::ERROR
425
        );
426
        redirect(self::get_server_not_available_url($instance));
427
    }
428
 
429
    /**
430
     * Get message when server not available
431
     *
432
     * @param instance $instance
433
     * @return string
434
     */
435
    public static function get_server_not_available_message(instance $instance): string {
436
        if ($instance->is_admin()) {
437
            return get_string('view_error_unable_join', 'mod_bigbluebuttonbn');
438
        } else if ($instance->is_moderator()) {
439
            return get_string('view_error_unable_join_teacher', 'mod_bigbluebuttonbn');
440
        } else {
441
            return get_string('view_error_unable_join_student', 'mod_bigbluebuttonbn');
442
        }
443
    }
444
 
445
    /**
446
     * Get URL to the page displaying that the server is not available
447
     *
448
     * @param instance $instance
449
     * @return string
450
     */
451
    public static function get_server_not_available_url(instance $instance): string {
452
        if ($instance->is_admin()) {
453
            return new moodle_url('/admin/settings.php', ['section' => 'modsettingbigbluebuttonbn']);
454
        } else if ($instance->is_moderator()) {
455
            return new moodle_url('/course/view.php', ['id' => $instance->get_course_id()]);
456
        } else {
457
            return new moodle_url('/course/view.php', ['id' => $instance->get_course_id()]);
458
        }
459
    }
460
 
461
    /**
462
     * Create a Meeting
463
     *
464
     * @param array $data
465
     * @param array $metadata
466
     * @param string|null $presentationname
467
     * @param string|null $presentationurl
468
     * @param int|null $instanceid
469
     * @return array
470
     * @throws bigbluebutton_exception
471
     */
472
    public static function create_meeting(
473
        array $data,
474
        array $metadata,
475
        ?string $presentationname = null,
476
        ?string $presentationurl = null,
477
        ?int $instanceid = null
478
    ): array {
479
        $createmeetingurl = self::action_url('create', $data, $metadata, $instanceid);
480
 
481
        $curl = new curl();
482
        if (!is_null($presentationname) && !is_null($presentationurl)) {
483
            $payload = "<?xml version='1.0' encoding='UTF-8'?><modules><module name='presentation'><document url='" .
484
                $presentationurl . "' /></module></modules>";
485
 
486
            $xml = $curl->post($createmeetingurl, $payload);
487
        } else {
488
            $xml = $curl->get($createmeetingurl);
489
        }
490
 
491
        self::assert_returned_xml($xml);
492
 
493
        if (empty($xml->meetingID)) {
494
            throw new bigbluebutton_exception('general_error_cannot_create_meeting');
495
        }
496
 
497
        if ($xml->hasBeenForciblyEnded === 'true') {
498
            throw new bigbluebutton_exception('index_error_forciblyended');
499
        }
500
 
501
        return [
502
            'meetingID' => (string) $xml->meetingID,
503
            'internalMeetingID' => (string) $xml->internalMeetingID,
504
            'attendeePW' => (string) $xml->attendeePW,
505
            'moderatorPW' => (string) $xml->moderatorPW
506
        ];
507
    }
508
 
509
    /**
510
     * Get meeting info for a given meeting id
511
     *
512
     * @param string $meetingid
513
     * @param int|null $instanceid
514
     * @return array
515
     */
516
    public static function get_meeting_info(string $meetingid, ?int $instanceid = null): array {
517
        $xmlinfo = self::fetch_endpoint_xml('getMeetingInfo', ['meetingID' => $meetingid], [], $instanceid);
518
        self::assert_returned_xml($xmlinfo, ['meetingid' => $meetingid]);
519
        return (array) $xmlinfo;
520
    }
521
 
522
    /**
523
     * Perform end meeting on BBB.
524
     *
525
     * @param string $meetingid
526
     * @param string $modpw
527
     * @param int|null $instanceid
528
     */
529
    public static function end_meeting(string $meetingid, string $modpw, ?int $instanceid = null): void {
530
        $xml = self::fetch_endpoint_xml('end', ['meetingID' => $meetingid, 'password' => $modpw], [], $instanceid);
531
        self::assert_returned_xml($xml, ['meetingid' => $meetingid]);
532
    }
533
 
534
    /**
535
     * Helper evaluates if the bigbluebutton server used belongs to blindsidenetworks domain.
536
     *
537
     * @return bool
538
     */
539
    public static function is_bn_server() {
540
        if (config::get('bn_server')) {
541
            return true;
542
        }
543
        $parsedurl = parse_url(config::get('server_url'));
544
        if (!isset($parsedurl['host'])) {
545
            return false;
546
        }
547
        $h = $parsedurl['host'];
548
        $hends = explode('.', $h);
549
        $hendslength = count($hends);
550
        return ($hends[$hendslength - 1] == 'com' && $hends[$hendslength - 2] == 'blindsidenetworks');
551
    }
552
 
553
    /**
554
     * Get the poll interval as it is set in the configuration
555
     *
556
     * If configuration value is under the threshold of {@see self::MIN_POLL_INTERVAL},
557
     * then return the {@see self::MIN_POLL_INTERVAL} value.
558
     *
559
     * @return int the poll interval in seconds
560
     */
561
    public static function get_poll_interval(): int {
562
        $pollinterval = intval(config::get('poll_interval'));
563
        if ($pollinterval < self::MIN_POLL_INTERVAL) {
564
            $pollinterval = self::MIN_POLL_INTERVAL;
565
        }
566
        return $pollinterval;
567
    }
568
}