Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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