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 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;
  18  
  19  use cache;
  20  use context;
  21  use context_course;
  22  use context_module;
  23  use core\persistent;
  24  use mod_bigbluebuttonbn\local\proxy\recording_proxy;
  25  use moodle_url;
  26  use stdClass;
  27  
  28  /**
  29   * The recording entity.
  30   *
  31   * This is utility class that defines a single recording, and provides methods for their local handling locally, and
  32   * communication with the bigbluebutton server.
  33   *
  34   * @package mod_bigbluebuttonbn
  35   * @copyright 2021 onwards, Blindside Networks Inc
  36   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class recording extends persistent {
  39      /** The table name. */
  40      const TABLE = 'bigbluebuttonbn_recordings';
  41  
  42      /** @var int Defines that the activity used to create the recording no longer exists */
  43      public const RECORDING_HEADLESS = 1;
  44  
  45      /** @var int Defines that the recording is not the original but an imported one */
  46      public const RECORDING_IMPORTED = 1;
  47  
  48      /** @var int Defines that the list should include imported recordings */
  49      public const INCLUDE_IMPORTED_RECORDINGS = true;
  50  
  51      /** @var int A meeting set to be recorded still awaits for a recording update */
  52      public const RECORDING_STATUS_AWAITING = 0;
  53  
  54      /** @var int A meeting set to be recorded was not recorded and dismissed by BBB */
  55      public const RECORDING_STATUS_DISMISSED = 1;
  56  
  57      /** @var int A meeting set to be recorded has a recording processed */
  58      public const RECORDING_STATUS_PROCESSED = 2;
  59  
  60      /** @var int A meeting set to be recorded received notification callback from BBB */
  61      public const RECORDING_STATUS_NOTIFIED = 3;
  62  
  63      /** @var int A meeting set to be recorded was processed and set back to an awaiting state */
  64      public const RECORDING_STATUS_RESET = 4;
  65  
  66      /** @var int A meeting set to be recorded was deleted from bigbluebutton */
  67      public const RECORDING_STATUS_DELETED = 5;
  68  
  69      /** @var bool Whether metadata been changed so the remote information needs to be updated ? */
  70      protected $metadatachanged = false;
  71  
  72      /** @var int A refresh period for recordings, defaults to 300s (5mins) */
  73      public const RECORDING_REFRESH_DEFAULT_PERIOD = 300;
  74  
  75      /** @var int A time limit for recordings to be dismissed, defaults to 30d (30days) */
  76      public const RECORDING_TIME_LIMIT_DAYS = 30;
  77  
  78      /** @var array A cached copy of the metadata */
  79      protected $metadata = null;
  80  
  81      /** @var instance A cached copy of the instance */
  82      protected $instance;
  83  
  84      /**
  85       * Create an instance of this class.
  86       *
  87       * @param int $id If set, this is the id of an existing record, used to load the data.
  88       * @param stdClass|null $record If set will be passed to from_record
  89       * @param null|array $metadata
  90       */
  91      public function __construct($id = 0, stdClass $record = null, ?array $metadata = null) {
  92          if ($record) {
  93              $record->headless = $record->headless ?? false;
  94              $record->imported = $record->imported ?? false;
  95              $record->groupid = $record->groupid ?? 0;
  96              $record->status = $record->status ?? self::RECORDING_STATUS_AWAITING;
  97          }
  98          parent::__construct($id, $record);
  99  
 100          if ($metadata) {
 101              $this->metadata = $metadata;
 102          }
 103      }
 104  
 105      /**
 106       * Helper function to retrieve recordings from the BigBlueButton.
 107       *
 108       * @param instance $instance
 109       * @param bool $includeimported
 110       * @param bool $onlyimported
 111       *
 112       * @return recording[] containing the recordings indexed by recordID, each recording is also a
 113       * non sequential associative array itself that corresponds to the actual recording in BBB
 114       */
 115      public static function get_recordings_for_instance(
 116          instance $instance,
 117          bool $includeimported = false,
 118          bool $onlyimported = false
 119      ): array {
 120          [$selects, $params] = self::get_basic_select_from_parameters(false, $includeimported, $onlyimported);
 121          $selects[] = "bigbluebuttonbnid = :bbbid";
 122          $params['bbbid'] = $instance->get_instance_id();
 123          $groupmode = groups_get_activity_groupmode($instance->get_cm());
 124          $context = $instance->get_context();
 125          if ($groupmode) {
 126              [$groupselects, $groupparams] = self::get_select_for_group(
 127                  $groupmode,
 128                  $context,
 129                  $instance->get_course_id(),
 130                  $instance->get_group_id(),
 131                  $instance->get_cm()->groupingid
 132              );
 133              if ($groupselects) {
 134                  $selects[] = $groupselects;
 135                  $params = array_merge_recursive($params, $groupparams);
 136              }
 137          }
 138  
 139          $recordings = self::fetch_records($selects, $params);
 140          foreach ($recordings as $recording) {
 141              $recording->instance = $instance;
 142          }
 143  
 144          return $recordings;
 145      }
 146  
 147      /**
 148       * Helper function to retrieve recordings from a given course.
 149       *
 150       * @param int $courseid id for a course record or null
 151       * @param array $excludedinstanceid exclude recordings from instance ids
 152       * @param bool $includeimported
 153       * @param bool $onlyimported
 154       * @param bool $includedeleted
 155       * @param bool $onlydeleted
 156       *
 157       * @return recording[] containing the recordings indexed by recordID, each recording is also a
 158       * non sequential associative array itself that corresponds to the actual recording in BBB
 159       */
 160      public static function get_recordings_for_course(
 161          int $courseid,
 162          array $excludedinstanceid = [],
 163          bool $includeimported = false,
 164          bool $onlyimported = false,
 165          bool $includedeleted = false,
 166          bool $onlydeleted = false
 167      ): array {
 168          global $DB;
 169  
 170          [$selects, $params] = self::get_basic_select_from_parameters(
 171              $includedeleted,
 172              $includeimported,
 173              $onlyimported,
 174              $onlydeleted
 175          );
 176          if ($courseid) {
 177              $selects[] = "courseid = :courseid";
 178              $params['courseid'] = $courseid;
 179              $course = $DB->get_record('course', ['id' => $courseid]);
 180              $groupmode = groups_get_course_groupmode($course);
 181              $context = context_course::instance($courseid);
 182          } else {
 183              $context = \context_system::instance();
 184              $groupmode = NOGROUPS;
 185          }
 186  
 187          if ($groupmode) {
 188              [$groupselects, $groupparams] = self::get_select_for_group($groupmode, $context, $course->id);
 189              if ($groupselects) {
 190                  $selects[] = $groupselects;
 191                  $params = array_merge($params, $groupparams);
 192              }
 193          }
 194  
 195          if ($excludedinstanceid) {
 196              [$sqlexcluded, $paramexcluded] = $DB->get_in_or_equal($excludedinstanceid, SQL_PARAMS_NAMED, 'param', false);
 197              $selects[] = "bigbluebuttonbnid {$sqlexcluded}";
 198              $params = array_merge($params, $paramexcluded);
 199          }
 200  
 201          return self::fetch_records($selects, $params);
 202      }
 203  
 204      /**
 205       * Get select for given group mode and context
 206       *
 207       * @param int $groupmode
 208       * @param context $context
 209       * @param int $courseid
 210       * @param int $groupid
 211       * @param int $groupingid
 212       * @return array
 213       */
 214      protected static function get_select_for_group($groupmode, $context, $courseid, $groupid = 0, $groupingid = 0): array {
 215          global $DB, $USER;
 216  
 217          $selects = [];
 218          $params = [];
 219          if ($groupmode) {
 220              $accessallgroups = has_capability('moodle/site:accessallgroups', $context) || $groupmode == VISIBLEGROUPS;
 221              if ($accessallgroups) {
 222                  if ($context instanceof context_module) {
 223                      $allowedgroups = groups_get_all_groups($courseid, 0, $groupingid);
 224                  } else {
 225                      $allowedgroups = groups_get_all_groups($courseid);
 226                  }
 227              } else {
 228                  if ($context instanceof context_module) {
 229                      $allowedgroups = groups_get_all_groups($courseid, $USER->id, $groupingid);
 230                  } else {
 231                      $allowedgroups = groups_get_all_groups($courseid, $USER->id);
 232                  }
 233              }
 234              $allowedgroupsid = array_map(function ($g) {
 235                  return $g->id;
 236              }, $allowedgroups);
 237              if ($groupid || empty($allowedgroups)) {
 238                  $selects[] = "groupid = :groupid";
 239                  $params['groupid'] = ($groupid && in_array($groupid, $allowedgroupsid)) ?
 240                      $groupid : 0;
 241              } else {
 242                  if ($accessallgroups) {
 243                      $allowedgroupsid[] = 0;
 244                  }
 245                  list($groupselects, $groupparams) = $DB->get_in_or_equal($allowedgroupsid, SQL_PARAMS_NAMED);
 246                  $selects[] = 'groupid ' . $groupselects;
 247                  $params = array_merge_recursive($params, $groupparams);
 248              }
 249          }
 250          return [
 251              implode(" AND ", $selects),
 252              $params,
 253          ];
 254      }
 255  
 256      /**
 257       * Get basic sql select from given parameters
 258       *
 259       * @param bool $includedeleted
 260       * @param bool $includeimported
 261       * @param bool $onlyimported
 262       * @param bool $onlydeleted
 263       * @return array
 264       */
 265      protected static function get_basic_select_from_parameters(
 266          bool $includedeleted = false,
 267          bool $includeimported = false,
 268          bool $onlyimported = false,
 269          bool $onlydeleted = false
 270      ): array {
 271          $selects = [];
 272          $params = [];
 273  
 274          // Start with the filters.
 275          if ($onlydeleted) {
 276              // Only headless recordings when only deleted is set.
 277              $selects[] = "headless = :headless";
 278              $params['headless'] = self::RECORDING_HEADLESS;
 279          } else if (!$includedeleted) {
 280              // Exclude headless recordings unless includedeleted.
 281              $selects[] = "headless != :headless";
 282              $params['headless'] = self::RECORDING_HEADLESS;
 283          }
 284  
 285          if (!$includeimported) {
 286              // Exclude imported recordings unless includedeleted.
 287              $selects[] = "imported != :imported";
 288              $params['imported'] = self::RECORDING_IMPORTED;
 289          } else if ($onlyimported) {
 290              // Exclude non-imported recordings.
 291              $selects[] = "imported = :imported";
 292              $params['imported'] = self::RECORDING_IMPORTED;
 293          }
 294  
 295          // Now get only recordings that have been validated by recording ready callback.
 296          $selects[] = "status IN (:status_processed, :status_notified)";
 297          $params['status_processed'] = self::RECORDING_STATUS_PROCESSED;
 298          $params['status_notified'] = self::RECORDING_STATUS_NOTIFIED;
 299          return [$selects, $params];
 300      }
 301  
 302      /**
 303       * Return the definition of the properties of this model.
 304       *
 305       * @return array
 306       */
 307      protected static function define_properties() {
 308          return [
 309              'courseid' => [
 310                  'type' => PARAM_INT,
 311              ],
 312              'bigbluebuttonbnid' => [
 313                  'type' => PARAM_INT,
 314              ],
 315              'groupid' => [
 316                  'type' => PARAM_INT,
 317                  'null' => NULL_ALLOWED,
 318              ],
 319              'recordingid' => [
 320                  'type' => PARAM_RAW,
 321              ],
 322              'headless' => [
 323                  'type' => PARAM_BOOL,
 324              ],
 325              'imported' => [
 326                  'type' => PARAM_BOOL,
 327              ],
 328              'status' => [
 329                  'type' => PARAM_INT,
 330              ],
 331              'importeddata' => [
 332                  'type' => PARAM_RAW,
 333                  'null' => NULL_ALLOWED,
 334                  'default' => ''
 335              ],
 336              'name' => [
 337                  'type' => PARAM_TEXT,
 338                  'null' => NULL_ALLOWED,
 339                  'default' => null
 340              ],
 341              'description' => [
 342                  'type' => PARAM_TEXT,
 343                  'null' => NULL_ALLOWED,
 344                  'default' => 0
 345              ],
 346              'protected' => [
 347                  'type' => PARAM_BOOL,
 348                  'null' => NULL_ALLOWED,
 349                  'default' => null
 350              ],
 351              'starttime' => [
 352                  'type' => PARAM_INT,
 353                  'null' => NULL_ALLOWED,
 354                  'default' => null
 355              ],
 356              'endtime' => [
 357                  'type' => PARAM_INT,
 358                  'null' => NULL_ALLOWED,
 359                  'default' => null
 360              ],
 361              'published' => [
 362                  'type' => PARAM_BOOL,
 363                  'null' => NULL_ALLOWED,
 364                  'default' => null
 365              ],
 366              'playbacks' => [
 367                  'type' => PARAM_RAW,
 368                  'null' => NULL_ALLOWED,
 369                  'default' => null
 370              ],
 371          ];
 372      }
 373  
 374      /**
 375       * Get the instance that this recording relates to.
 376       *
 377       * @return instance
 378       */
 379      public function get_instance(): instance {
 380          if ($this->instance === null) {
 381              $this->instance = instance::get_from_instanceid($this->get('bigbluebuttonbnid'));
 382          }
 383  
 384          return $this->instance;
 385      }
 386  
 387      /**
 388       * Before doing the database update, let's check if we need to update metadata
 389       *
 390       * @return void
 391       */
 392      protected function before_update() {
 393          // We update if the remote metadata has been changed locally.
 394          if ($this->metadatachanged && !$this->get('imported')) {
 395              $metadata = $this->fetch_metadata();
 396              if ($metadata) {
 397                  recording_proxy::update_recording(
 398                      $this->get('recordingid'),
 399                      $metadata
 400                  );
 401              }
 402              $this->metadatachanged = false;
 403          }
 404      }
 405  
 406      /**
 407       * Create a new imported recording from current recording
 408       *
 409       * @param instance $targetinstance
 410       * @return recording
 411       */
 412      public function create_imported_recording(instance $targetinstance) {
 413          $recordingrec = $this->to_record();
 414          $remotedata = $this->fetch_metadata();
 415          unset($recordingrec->id);
 416          $recordingrec->bigbluebuttonbnid = $targetinstance->get_instance_id();
 417          $recordingrec->courseid = $targetinstance->get_course_id();
 418          $recordingrec->groupid = 0; // The recording is available to everyone.
 419          $recordingrec->importeddata = json_encode($remotedata);
 420          $recordingrec->imported = true;
 421          $recordingrec->headless = false;
 422          $importedrecording = new self(0, $recordingrec);
 423          $importedrecording->create();
 424          return $importedrecording;
 425      }
 426  
 427      /**
 428       * Delete the recording in the BBB button
 429       *
 430       * @return void
 431       */
 432      protected function before_delete() {
 433          $recordid = $this->get('recordingid');
 434          if ($recordid && !$this->get('imported')) {
 435              recording_proxy::delete_recording($recordid);
 436              // Delete in cache if needed.
 437              $cachedrecordings = cache::make('mod_bigbluebuttonbn', 'recordings');
 438              $cachedrecordings->delete($recordid);
 439          }
 440      }
 441  
 442      /**
 443       * Set name
 444       *
 445       * @param string $value
 446       */
 447      protected function set_name($value) {
 448          $this->metadata_set('name', trim($value));
 449      }
 450  
 451      /**
 452       * Set Description
 453       *
 454       * @param string $value
 455       */
 456      protected function set_description($value) {
 457          $this->metadata_set('description', trim($value));
 458      }
 459  
 460      /**
 461       * Recording is protected
 462       *
 463       * @param bool $value
 464       */
 465      protected function set_protected($value) {
 466          $realvalue = $value ? "true" : "false";
 467          $this->metadata_set('protected', $realvalue);
 468          recording_proxy::protect_recording($this->get('recordingid'), $realvalue);
 469      }
 470  
 471      /**
 472       * Recording starttime
 473       *
 474       * @param int $value
 475       */
 476      protected function set_starttime($value) {
 477          $this->metadata_set('starttime', $value);
 478      }
 479  
 480      /**
 481       * Recording endtime
 482       *
 483       * @param int $value
 484       */
 485      protected function set_endtime($value) {
 486          $this->metadata_set('endtime', $value);
 487      }
 488  
 489      /**
 490       * Recording is published
 491       *
 492       * @param bool $value
 493       */
 494      protected function set_published($value) {
 495          $realvalue = $value ? "true" : "false";
 496          $this->metadata_set('published', $realvalue);
 497          // Now set this flag onto the remote bbb server.
 498          recording_proxy::publish_recording($this->get('recordingid'), $realvalue);
 499      }
 500  
 501      /**
 502       * Update recording status
 503       *
 504       * @param bool $value
 505       */
 506      protected function set_status($value) {
 507          $this->raw_set('status', $value);
 508          $this->update();
 509      }
 510  
 511      /**
 512       * POSSIBLE_REMOTE_META_SOURCE match a field type and its metadataname (historical and current).
 513       */
 514      const POSSIBLE_REMOTE_META_SOURCE = [
 515          'description' => ['meta_bbb-recording-description', 'meta_contextactivitydescription'],
 516          'name' => ['meta_bbb-recording-name', 'meta_contextactivity', 'meetingName'],
 517          'playbacks' => ['playbacks'],
 518          'starttime' => ['startTime'],
 519          'endtime' => ['endTime'],
 520          'published' => ['published'],
 521          'protected' => ['protected'],
 522          'tags' => ['meta_bbb-recording-tags']
 523      ];
 524  
 525      /**
 526       * Get the real metadata name for the possible source.
 527       *
 528       * @param string $sourcetype the name of the source we look for (name, description...)
 529       * @param array $metadata current metadata
 530       */
 531      protected function get_possible_meta_name_for_source($sourcetype, $metadata): string {
 532          $possiblesource = self::POSSIBLE_REMOTE_META_SOURCE[$sourcetype];
 533          $possiblesourcename = $possiblesource[0];
 534          foreach ($possiblesource as $possiblesname) {
 535              if (isset($meta[$possiblesname])) {
 536                  $possiblesourcename = $possiblesname;
 537              }
 538          }
 539          return $possiblesourcename;
 540      }
 541  
 542      /**
 543       * Convert string (metadata) to json object
 544       *
 545       * @return mixed|null
 546       */
 547      protected function remote_meta_convert() {
 548          $remotemeta = $this->raw_get('importeddata');
 549          return json_decode($remotemeta, true);
 550      }
 551  
 552      /**
 553       * Description is stored in the metadata, so we sometimes needs to do some conversion.
 554       */
 555      protected function get_description() {
 556          return trim($this->metadata_get('description'));
 557      }
 558  
 559      /**
 560       * Name is stored in the metadata
 561       */
 562      protected function get_name() {
 563          return trim($this->metadata_get('name'));
 564      }
 565  
 566      /**
 567       * List of playbacks for this recording.
 568       *
 569       * @return array[]
 570       */
 571      protected function get_playbacks() {
 572          if ($playbacks = $this->metadata_get('playbacks')) {
 573              return array_map(function (array $playback): array {
 574                  $clone = array_merge([], $playback);
 575                  $clone['url'] = new moodle_url('/mod/bigbluebuttonbn/bbb_view.php', [
 576                      'action' => 'play',
 577                      'bn' => $this->raw_get('bigbluebuttonbnid'),
 578                      'rid' => $this->get('id'),
 579                      'rtype' => $clone['type'],
 580                  ]);
 581  
 582                  return $clone;
 583              }, $playbacks);
 584          }
 585  
 586          return [];
 587      }
 588  
 589      /**
 590       * Get the playback URL for the specified type.
 591       *
 592       * @param string $type
 593       * @return null|string
 594       */
 595      public function get_remote_playback_url(string $type): ?string {
 596          $this->refresh_metadata_if_required();
 597  
 598          $playbacks = $this->metadata_get('playbacks');
 599          foreach ($playbacks as $playback) {
 600              if ($playback['type'] == $type) {
 601                  return $playback['url'];
 602              }
 603          }
 604  
 605          return null;
 606      }
 607  
 608      /**
 609       * Is protected. Return null if protected is not implemented.
 610       *
 611       * @return bool|null
 612       */
 613      protected function get_protected() {
 614          $protectedtext = $this->metadata_get('protected');
 615          return is_null($protectedtext) ? null : $protectedtext === "true";
 616      }
 617  
 618      /**
 619       * Start time
 620       *
 621       * @return mixed|null
 622       */
 623      protected function get_starttime() {
 624          return $this->metadata_get('starttime');
 625      }
 626  
 627      /**
 628       * Start time
 629       *
 630       * @return mixed|null
 631       */
 632      protected function get_endtime() {
 633          return $this->metadata_get('endtime');
 634      }
 635  
 636      /**
 637       * Is published
 638       *
 639       * @return bool
 640       */
 641      protected function get_published() {
 642          $publishedtext = $this->metadata_get('published');
 643          return $publishedtext === "true";
 644      }
 645  
 646      /**
 647       * Set locally stored metadata from this instance
 648       *
 649       * @param string $fieldname
 650       * @param mixed $value
 651       */
 652      protected function metadata_set($fieldname, $value) {
 653          // Can we can change the metadata on the imported record ?
 654          if ($this->get('imported')) {
 655              return;
 656          }
 657  
 658          $this->metadatachanged = true;
 659  
 660          $metadata = $this->fetch_metadata();
 661          $possiblesourcename = $this->get_possible_meta_name_for_source($fieldname, $metadata);
 662          $metadata[$possiblesourcename] = $value;
 663  
 664          $this->metadata = $metadata;
 665      }
 666  
 667      /**
 668       * Get information stored in the recording metadata such as description, name and other info
 669       *
 670       * @param string $fieldname
 671       * @return mixed|null
 672       */
 673      protected function metadata_get($fieldname) {
 674          $metadata = $this->fetch_metadata();
 675  
 676          $possiblesourcename = $this->get_possible_meta_name_for_source($fieldname, $metadata);
 677          return $metadata[$possiblesourcename] ?? null;
 678      }
 679  
 680      /**
 681       * @var string Default sort for recordings when fetching from the database.
 682       */
 683      const DEFAULT_RECORDING_SORT = 'timecreated ASC';
 684  
 685      /**
 686       * Fetch all records which match the specified parameters, including all metadata that relates to them.
 687       *
 688       * @param array $selects
 689       * @param array $params
 690       * @return recording[]
 691       */
 692      protected static function fetch_records(array $selects, array $params): array {
 693          global $DB, $CFG;
 694  
 695          $withindays = time() - (self::RECORDING_TIME_LIMIT_DAYS * DAYSECS);
 696          // Sort for recordings when fetching from the database.
 697          $recordingsort = $CFG->bigbluebuttonbn_recordings_asc_sort ? 'timecreated ASC' : 'timecreated DESC';
 698  
 699          // Fetch the local data. Arbitrary sort by id, so we get the same result on different db engines.
 700          $recordings = $DB->get_records_select(
 701              static::TABLE,
 702              implode(" AND ", $selects),
 703              $params,
 704              $recordingsort
 705          );
 706  
 707          // Grab the recording IDs.
 708          $recordingids = array_values(array_map(function ($recording) {
 709              return $recording->recordingid;
 710          }, $recordings));
 711  
 712          // Fetch all metadata for these recordings.
 713          $metadatas = recording_proxy::fetch_recordings($recordingids);
 714  
 715          // Return the instances.
 716          return array_filter(array_map(function ($recording) use ($metadatas, $withindays) {
 717              // Filter out if no metadata was fetched.
 718              if (!array_key_exists($recording->recordingid, $metadatas)) {
 719                  // Mark it as dismissed if it is older than 30 days.
 720                  if ($withindays > $recording->timecreated) {
 721                      $recording = new self(0, $recording, null);
 722                      $recording->set_status(self::RECORDING_STATUS_DISMISSED);
 723                  }
 724                  return false;
 725              }
 726              $metadata = $metadatas[$recording->recordingid];
 727              // Filter out and mark it as deleted if it was deleted in BBB.
 728              if ($metadata['state'] == 'deleted') {
 729                  $recording = new self(0, $recording, null);
 730                  $recording->set_status(self::RECORDING_STATUS_DELETED);
 731                  return false;
 732              }
 733              // Include it otherwise.
 734              return new self(0, $recording, $metadata);
 735          }, $recordings));
 736      }
 737  
 738      /**
 739       * Fetch metadata
 740       *
 741       * If metadata has changed locally or if it an imported recording, nothing will be done.
 742       *
 743       * @param bool $force
 744       * @return array
 745       */
 746      protected function fetch_metadata(bool $force = false): ?array {
 747          if ($this->metadata !== null && !$force) {
 748              // Metadata is already up-to-date.
 749              return $this->metadata;
 750          }
 751  
 752          if ($this->get('imported')) {
 753              $this->metadata = json_decode($this->get('importeddata'), true);
 754          } else {
 755              $this->metadata = recording_proxy::fetch_recording($this->get('recordingid'));
 756          }
 757  
 758          return $this->metadata;
 759      }
 760  
 761      /**
 762       * Refresh metadata if required.
 763       *
 764       * If this is a protected recording which whose data was not fetched in the current request, then the metadata will
 765       * be purged and refetched. This ensures that the url is safe for use with a protected recording.
 766       */
 767      protected function refresh_metadata_if_required() {
 768          recording_proxy::purge_protected_recording($this->get('recordingid'));
 769          $this->fetch_metadata(true);
 770      }
 771  
 772      /**
 773       * Synchronise pending recordings from the server.
 774       *
 775       * This function should be called by the check_pending_recordings scheduled task.
 776       *
 777       * @param bool $dismissedonly fetch dismissed recording only
 778       */
 779      public static function sync_pending_recordings_from_server(bool $dismissedonly = false): void {
 780          global $DB;
 781          $params = [
 782              'withindays' => time() - (self::RECORDING_TIME_LIMIT_DAYS * DAYSECS),
 783          ];
 784          // Fetch the local data.
 785          if ($dismissedonly) {
 786              mtrace("=> Looking for any recording that has been 'dismissed' in the past " . self::RECORDING_TIME_LIMIT_DAYS
 787                  . " days.");
 788              $select = 'status = :status_dismissed AND timecreated > :withindays';
 789              $params['status_dismissed'] = self::RECORDING_STATUS_DISMISSED;
 790          } else {
 791              mtrace("=> Looking for any recording awaiting processing from the past " . self::RECORDING_TIME_LIMIT_DAYS . " days.");
 792              $select = '(status = :status_awaiting AND timecreated > :withindays) OR status = :status_reset';
 793              $params['status_reset'] = self::RECORDING_STATUS_RESET;
 794              $params['status_awaiting'] = self::RECORDING_STATUS_AWAITING;
 795          }
 796  
 797          $recordings = $DB->get_records_select(static::TABLE, $select, $params, self::DEFAULT_RECORDING_SORT);
 798          // Sort by DEFAULT_RECORDING_SORT we get the same result on different db engines.
 799  
 800          $recordingcount = count($recordings);
 801          mtrace("=> Found {$recordingcount} recordings to query");
 802  
 803          // Grab the recording IDs.
 804          $recordingids = array_map(function($recording) {
 805              return $recording->recordingid;
 806          }, $recordings);
 807  
 808          // Fetch all metadata for these recordings.
 809          mtrace("=> Fetching recording metadata from server");
 810          $metadatas = recording_proxy::fetch_recordings($recordingids);
 811  
 812          $foundcount = 0;
 813          foreach ($metadatas as $recordingid => $metadata) {
 814              mtrace("==> Found metadata for {$recordingid}.");
 815              $id = array_search($recordingid, $recordingids);
 816              if (!$id) {
 817                  // Recording was not found, skip.
 818                  mtrace("===> Skip as fetched recording was not found.");
 819                  continue;
 820              }
 821              // Recording was found, update status.
 822              mtrace("===> Update local cache as fetched recording was found.");
 823              $recording = new self(0, $recordings[$id], $metadata);
 824              $recording->set_status(self::RECORDING_STATUS_PROCESSED);
 825              $foundcount++;
 826  
 827              if (array_key_exists('breakouts', $metadata)) {
 828                  // Iterate breakout recordings (if any) and update status.
 829                  foreach ($metadata['breakouts'] as $breakoutrecordingid => $breakoutmetadata) {
 830                      $breakoutrecording = self::get_record(['recordingid' => $breakoutrecordingid]);
 831                      if (!$breakoutrecording) {
 832                          $breakoutrecording = new recording(0, (object) [
 833                              'courseid' => $recording->get('courseid'),
 834                              'bigbluebuttonbnid' => $recording->get('bigbluebuttonbnid'),
 835                              'groupid' => $recording->get('groupid'),
 836                              'recordingid' => $breakoutrecordingid
 837                          ], $breakoutmetadata);
 838                          $breakoutrecording->create();
 839                      }
 840                      $breakoutrecording->set_status(self::RECORDING_STATUS_PROCESSED);
 841                      $foundcount++;
 842                  }
 843              }
 844          }
 845  
 846          mtrace("=> Finished processing recordings. Updated status for {$foundcount} / {$recordingcount} recordings.");
 847      }
 848  }