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 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 cache_helper;
  21  use SimpleXMLElement;
  22  
  23  /**
  24   * The recording proxy.
  25   *
  26   * This class acts as a proxy between Moodle and the BigBlueButton API server,
  27   * and deals with all requests relating to recordings.
  28   *
  29   * @package   mod_bigbluebuttonbn
  30   * @copyright 2021 onwards, Blindside Networks Inc
  31   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  32   * @author    Laurent David  (laurent [at] call-learning [dt] fr)
  33   * @author    Jesus Federico  (jesus [at] blindsidenetworks [dt] com)
  34   */
  35  class recording_proxy extends proxy_base {
  36  
  37      /**
  38       * Invalidate the MUC cache for the specified recording.
  39       *
  40       * @param string $recordid
  41       */
  42      protected static function invalidate_cache_for_recording(string $recordid): void {
  43          cache_helper::invalidate_by_event('mod_bigbluebuttonbn/recordingchanged', [$recordid]);
  44      }
  45  
  46      /**
  47       * Perform deleteRecordings on BBB.
  48       *
  49       * @param string $recordid a recording id
  50       * @return bool
  51       */
  52      public static function delete_recording(string $recordid): bool {
  53          $result = self::fetch_endpoint_xml('deleteRecordings', ['recordID' => $recordid]);
  54          if (!$result || $result->returncode != 'SUCCESS') {
  55              return false;
  56          }
  57          return true;
  58      }
  59  
  60      /**
  61       * Perform publishRecordings on BBB.
  62       *
  63       * @param string $recordid
  64       * @param string $publish
  65       * @return bool
  66       */
  67      public static function publish_recording(string $recordid, string $publish = 'true'): bool {
  68          $result = self::fetch_endpoint_xml('publishRecordings', [
  69              'recordID' => $recordid,
  70              'publish' => $publish,
  71          ]);
  72  
  73          self::invalidate_cache_for_recording($recordid);
  74  
  75          if (!$result || $result->returncode != 'SUCCESS') {
  76              return false;
  77          }
  78  
  79          return true;
  80      }
  81  
  82      /**
  83       * Perform publishRecordings on BBB.
  84       *
  85       * @param string $recordid
  86       * @param string $protected
  87       * @return bool
  88       */
  89      public static function protect_recording(string $recordid, string $protected = 'true'): bool {
  90          global $CFG;
  91  
  92          // Ignore action if recording_protect_editable is set to false.
  93          if (empty($CFG->bigbluebuttonbn_recording_protect_editable)) {
  94              return false;
  95          }
  96  
  97          $result = self::fetch_endpoint_xml('updateRecordings', [
  98              'recordID' => $recordid,
  99              'protect' => $protected,
 100          ]);
 101  
 102          self::invalidate_cache_for_recording($recordid);
 103  
 104          if (!$result || $result->returncode != 'SUCCESS') {
 105              return false;
 106          }
 107  
 108          return true;
 109      }
 110  
 111      /**
 112       * Perform updateRecordings on BBB.
 113       *
 114       * @param string $recordid a single record identifier
 115       * @param array $params ['key'=>param_key, 'value']
 116       */
 117      public static function update_recording(string $recordid, array $params): bool {
 118          $result = self::fetch_endpoint_xml('updateRecordings', array_merge([
 119              'recordID' => $recordid
 120          ], $params));
 121  
 122          self::invalidate_cache_for_recording($recordid);
 123  
 124          return $result ? $result->returncode == 'SUCCESS' : false;
 125      }
 126  
 127      /**
 128       * Helper function to fetch a single recording from a BigBlueButton server.
 129       *
 130       * @param string $recordingid
 131       * @return null|array
 132       */
 133      public static function fetch_recording(string $recordingid): ?array {
 134          $data = self::fetch_recordings([$recordingid]);
 135  
 136          if (array_key_exists($recordingid, $data)) {
 137              return $data[$recordingid];
 138          }
 139  
 140          return null;
 141      }
 142  
 143      /**
 144       * Check whether the current recording is a protected recording and purge the cache if necessary.
 145       *
 146       * @param string $recordingid
 147       */
 148      public static function purge_protected_recording(string $recordingid): void {
 149          $cache = cache::make('mod_bigbluebuttonbn', 'recordings');
 150  
 151          $recording = $cache->get($recordingid);
 152          if (empty($recording)) {
 153              // This value was not cached to begin with.
 154              return;
 155          }
 156  
 157          $currentfetchcache = cache::make('mod_bigbluebuttonbn', 'currentfetch');
 158          if ($currentfetchcache->has($recordingid)) {
 159              // This item was fetched in the current request.
 160              return;
 161          }
 162  
 163          if (array_key_exists('protected', $recording) && $recording['protected'] === 'true') {
 164              // This item is protected. Purge it from the cache.
 165              $cache->delete($recordingid);
 166              return;
 167          }
 168      }
 169  
 170      /**
 171       * Helper function to fetch recordings from a BigBlueButton server.
 172       *
 173       * We use a cache to store recording indexed by keyids/recordingID.
 174       * @param array $keyids list of recordingids
 175       * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array
 176       *  and sorted by {@see recording_proxy::sort_recordings}
 177       */
 178      public static function fetch_recordings(array $keyids = []): array {
 179          $recordings = [];
 180  
 181          // If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''.
 182          if (empty($keyids)) {
 183              return $recordings;
 184          }
 185          $cache = cache::make('mod_bigbluebuttonbn', 'recordings');
 186          $currentfetchcache = cache::make('mod_bigbluebuttonbn', 'currentfetch');
 187          $recordings = array_filter($cache->get_many($keyids));
 188          $missingkeys = array_diff(array_values($keyids), array_keys($recordings));
 189  
 190          $recordings += self::do_fetch_recordings($missingkeys);
 191          $cache->set_many($recordings);
 192          $currentfetchcache->set_many(array_flip(array_keys($recordings)));
 193          return $recordings;
 194      }
 195  
 196      /**
 197       * Helper function to fetch recordings from a BigBlueButton server.
 198       *
 199       * @param array $keyids list of meetingids
 200       * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array
 201       *  and sorted by {@see recording_proxy::sort_recordings}
 202       */
 203      public static function fetch_recording_by_meeting_id(array $keyids = []): array {
 204          $recordings = [];
 205  
 206          // If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''.
 207          if (empty($keyids)) {
 208              return $recordings;
 209          }
 210          $recordings = self::do_fetch_recordings($keyids, 'meetingID');
 211          return $recordings;
 212      }
 213  
 214      /**
 215       * Helper function to fetch recordings from a BigBlueButton server.
 216       *
 217       * @param array $keyids list of meetingids or recordingids
 218       * @param string $key the param name used for the BBB request (<recordID>|meetingID)
 219       * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array.
 220       *  and sorted {@see recording_proxy::sort_recordings}
 221       */
 222      private static function do_fetch_recordings(array $keyids = [], string $key = 'recordID'): array {
 223          $recordings = [];
 224          $pagesize = 25;
 225          while ($ids = array_splice($keyids, 0, $pagesize)) {
 226              $fetchrecordings = self::fetch_recordings_page($ids, $key);
 227              $recordings += $fetchrecordings;
 228          }
 229          // Sort recordings.
 230          return self::sort_recordings($recordings);
 231      }
 232      /**
 233       * Helper function to fetch a page of recordings from the remote server.
 234       *
 235       * @param array $ids
 236       * @param string $key
 237       * @return array
 238       */
 239      private static function fetch_recordings_page(array $ids, $key = 'recordID'): array {
 240          // The getRecordings call is executed using a method GET (supported by all versions of BBB).
 241          $xml = self::fetch_endpoint_xml('getRecordings', [$key => implode(',', $ids), 'state' => 'any']);
 242  
 243          if (!$xml) {
 244              return [];
 245          }
 246  
 247          if ($xml->returncode != 'SUCCESS') {
 248              return [];
 249          }
 250  
 251          if (!isset($xml->recordings)) {
 252              return [];
 253          }
 254  
 255          $recordings = [];
 256          // If there were recordings already created.
 257          foreach ($xml->recordings->recording as $recordingxml) {
 258              $recording = self::parse_recording($recordingxml);
 259              $recordings[$recording['recordID']] = $recording;
 260              // Check if there are any child.
 261              if (isset($recordingxml->breakoutRooms->breakoutRoom)) {
 262                  $breakoutrooms = [];
 263                  foreach ($recordingxml->breakoutRooms->breakoutRoom as $breakoutroom) {
 264                      $breakoutrooms[] = trim((string) $breakoutroom);
 265                  }
 266                  if ($breakoutrooms) {
 267                      $xml = self::fetch_endpoint_xml('getRecordings', ['recordID' => implode(',', $breakoutrooms)]);
 268                      if ($xml && $xml->returncode == 'SUCCESS' && isset($xml->recordings)) {
 269                          // If there were already created meetings.
 270                          foreach ($xml->recordings->recording as $subrecordingxml) {
 271                              $recording = self::parse_recording($subrecordingxml);
 272                              $recordings[$recording['recordID']] = $recording;
 273                          }
 274                      }
 275                  }
 276              }
 277          }
 278  
 279          return $recordings;
 280      }
 281  
 282      /**
 283       *  Helper function to sort an array of recordings. It compares the startTime in two recording objects.
 284       *
 285       * @param array $recordings
 286       * @return array
 287       */
 288      public static function sort_recordings(array $recordings): array {
 289          global $CFG;
 290  
 291          uasort($recordings, function($a, $b) {
 292              if ($a['startTime'] < $b['startTime']) {
 293                  return -1;
 294              }
 295              if ($a['startTime'] == $b['startTime']) {
 296                  return 0;
 297              }
 298              return 1;
 299          });
 300  
 301          return $recordings;
 302      }
 303  
 304      /**
 305       * Helper function to parse an xml recording object and produce an array in the format used by the plugin.
 306       *
 307       * @param SimpleXMLElement $recording
 308       *
 309       * @return array
 310       */
 311      public static function parse_recording(SimpleXMLElement $recording): array {
 312          // Add formats.
 313          $playbackarray = [];
 314          foreach ($recording->playback->format as $format) {
 315              $playbackarray[(string) $format->type] = [
 316                  'type' => (string) $format->type,
 317                  'url' => trim((string) $format->url), 'length' => (string) $format->length
 318              ];
 319              // Add preview per format when existing.
 320              if ($format->preview) {
 321                  $playbackarray[(string) $format->type]['preview'] =
 322                      self::parse_preview_images($format->preview);
 323              }
 324          }
 325          // Add the metadata to the recordings array.
 326          $metadataarray =
 327              self::parse_recording_meta(get_object_vars($recording->metadata));
 328          $recordingarray = [
 329              'recordID' => (string) $recording->recordID,
 330              'meetingID' => (string) $recording->meetingID,
 331              'meetingName' => (string) $recording->name,
 332              'published' => (string) $recording->published,
 333              'state' => (string) $recording->state,
 334              'startTime' => (string) $recording->startTime,
 335              'endTime' => (string) $recording->endTime,
 336              'playbacks' => $playbackarray
 337          ];
 338          if (isset($recording->protected)) {
 339              $recordingarray['protected'] = (string) $recording->protected;
 340          }
 341          return $recordingarray + $metadataarray;
 342      }
 343  
 344      /**
 345       * Helper function to convert an xml recording metadata object to an array in the format used by the plugin.
 346       *
 347       * @param array $metadata
 348       *
 349       * @return array
 350       */
 351      public static function parse_recording_meta(array $metadata): array {
 352          $metadataarray = [];
 353          foreach ($metadata as $key => $value) {
 354              if (is_object($value)) {
 355                  $value = '';
 356              }
 357              $metadataarray['meta_' . $key] = $value;
 358          }
 359          return $metadataarray;
 360      }
 361  
 362      /**
 363       * Helper function to convert an xml recording preview images to an array in the format used by the plugin.
 364       *
 365       * @param SimpleXMLElement $preview
 366       *
 367       * @return array
 368       */
 369      public static function parse_preview_images(SimpleXMLElement $preview): array {
 370          $imagesarray = [];
 371          foreach ($preview->images->image as $image) {
 372              $imagearray = ['url' => trim((string) $image)];
 373              foreach ($image->attributes() as $attkey => $attvalue) {
 374                  $imagearray[$attkey] = (string) $attvalue;
 375              }
 376              array_push($imagesarray, $imagearray);
 377          }
 378          return $imagesarray;
 379      }
 380  }