Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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  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  }