Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

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