<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_bigbluebuttonbn\local\proxy;
use cache;
use cache_helper;
use SimpleXMLElement;
/**
* The recording proxy.
*
* This class acts as a proxy between Moodle and the BigBlueButton API server,
* and deals with all requests relating to recordings.
*
* @package mod_bigbluebuttonbn
* @copyright 2021 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Laurent David (laurent [at] call-learning [dt] fr)
* @author Jesus Federico (jesus [at] blindsidenetworks [dt] com)
*/
class recording_proxy extends proxy_base {
/**
* Invalidate the MUC cache for the specified recording.
*
* @param string $recordid
*/
protected static function invalidate_cache_for_recording(string $recordid): void {
cache_helper::invalidate_by_event('mod_bigbluebuttonbn/recordingchanged', [$recordid]);
}
/**
* Perform deleteRecordings on BBB.
*
* @param string $recordid a recording id
* @return bool
*/
< public static function delete_recording(string $recordid): bool {
> public static function delete_recording(string $recordid, ?int $instanceid = null): bool {
$result = self::fetch_endpoint_xml('deleteRecordings', ['recordID' => $recordid]);
if (!$result || $result->returncode != 'SUCCESS') {
return false;
}
return true;
}
/**
* Perform publishRecordings on BBB.
*
* @param string $recordid
* @param string $publish
* @return bool
*/
public static function publish_recording(string $recordid, string $publish = 'true'): bool {
$result = self::fetch_endpoint_xml('publishRecordings', [
'recordID' => $recordid,
'publish' => $publish,
]);
self::invalidate_cache_for_recording($recordid);
if (!$result || $result->returncode != 'SUCCESS') {
return false;
}
return true;
}
/**
* Perform publishRecordings on BBB.
*
* @param string $recordid
* @param string $protected
* @return bool
*/
public static function protect_recording(string $recordid, string $protected = 'true'): bool {
global $CFG;
// Ignore action if recording_protect_editable is set to false.
if (empty($CFG->bigbluebuttonbn_recording_protect_editable)) {
return false;
}
$result = self::fetch_endpoint_xml('updateRecordings', [
'recordID' => $recordid,
'protect' => $protected,
]);
self::invalidate_cache_for_recording($recordid);
if (!$result || $result->returncode != 'SUCCESS') {
return false;
}
return true;
}
/**
* Perform updateRecordings on BBB.
*
* @param string $recordid a single record identifier
* @param array $params ['key'=>param_key, 'value']
*/
public static function update_recording(string $recordid, array $params): bool {
$result = self::fetch_endpoint_xml('updateRecordings', array_merge([
'recordID' => $recordid
], $params));
self::invalidate_cache_for_recording($recordid);
return $result ? $result->returncode == 'SUCCESS' : false;
}
/**
* Helper function to fetch a single recording from a BigBlueButton server.
*
* @param string $recordingid
* @return null|array
*/
public static function fetch_recording(string $recordingid): ?array {
$data = self::fetch_recordings([$recordingid]);
if (array_key_exists($recordingid, $data)) {
return $data[$recordingid];
}
return null;
}
/**
* Check whether the current recording is a protected recording and purge the cache if necessary.
*
* @param string $recordingid
*/
public static function purge_protected_recording(string $recordingid): void {
$cache = cache::make('mod_bigbluebuttonbn', 'recordings');
$recording = $cache->get($recordingid);
if (empty($recording)) {
// This value was not cached to begin with.
return;
}
$currentfetchcache = cache::make('mod_bigbluebuttonbn', 'currentfetch');
if ($currentfetchcache->has($recordingid)) {
// This item was fetched in the current request.
return;
}
if (array_key_exists('protected', $recording) && $recording['protected'] === 'true') {
// This item is protected. Purge it from the cache.
$cache->delete($recordingid);
return;
}
}
/**
* Helper function to fetch recordings from a BigBlueButton server.
*
* We use a cache to store recording indexed by keyids/recordingID.
* @param array $keyids list of recordingids
* @return array (associative) with recordings indexed by recordID, each recording is a non sequential array
* and sorted by {@see recording_proxy::sort_recordings}
*/
public static function fetch_recordings(array $keyids = []): array {
$recordings = [];
// If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''.
if (empty($keyids)) {
return $recordings;
}
$cache = cache::make('mod_bigbluebuttonbn', 'recordings');
$currentfetchcache = cache::make('mod_bigbluebuttonbn', 'currentfetch');
$recordings = array_filter($cache->get_many($keyids));
$missingkeys = array_diff(array_values($keyids), array_keys($recordings));
$recordings += self::do_fetch_recordings($missingkeys);
$cache->set_many($recordings);
$currentfetchcache->set_many(array_flip(array_keys($recordings)));
return $recordings;
}
/**
* Helper function to fetch recordings from a BigBlueButton server.
*
* @param array $keyids list of meetingids
* @return array (associative) with recordings indexed by recordID, each recording is a non sequential array
* and sorted by {@see recording_proxy::sort_recordings}
*/
public static function fetch_recording_by_meeting_id(array $keyids = []): array {
$recordings = [];
// If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''.
if (empty($keyids)) {
return $recordings;
}
$recordings = self::do_fetch_recordings($keyids, 'meetingID');
return $recordings;
}
/**
* Helper function to fetch recordings from a BigBlueButton server.
*
* @param array $keyids list of meetingids or recordingids
* @param string $key the param name used for the BBB request (<recordID>|meetingID)
* @return array (associative) with recordings indexed by recordID, each recording is a non sequential array.
* and sorted {@see recording_proxy::sort_recordings}
*/
private static function do_fetch_recordings(array $keyids = [], string $key = 'recordID'): array {
$recordings = [];
$pagesize = 25;
while ($ids = array_splice($keyids, 0, $pagesize)) {
$fetchrecordings = self::fetch_recordings_page($ids, $key);
$recordings += $fetchrecordings;
}
// Sort recordings.
return self::sort_recordings($recordings);
}
/**
* Helper function to fetch a page of recordings from the remote server.
*
* @param array $ids
* @param string $key
* @return array
*/
private static function fetch_recordings_page(array $ids, $key = 'recordID'): array {
// The getRecordings call is executed using a method GET (supported by all versions of BBB).
$xml = self::fetch_endpoint_xml('getRecordings', [$key => implode(',', $ids), 'state' => 'any']);
if (!$xml) {
return [];
}
if ($xml->returncode != 'SUCCESS') {
return [];
}
if (!isset($xml->recordings)) {
return [];
}
$recordings = [];
// If there were recordings already created.
foreach ($xml->recordings->recording as $recordingxml) {
$recording = self::parse_recording($recordingxml);
$recordings[$recording['recordID']] = $recording;
// Check if there are any child.
if (isset($recordingxml->breakoutRooms->breakoutRoom)) {
$breakoutrooms = [];
foreach ($recordingxml->breakoutRooms->breakoutRoom as $breakoutroom) {
$breakoutrooms[] = trim((string) $breakoutroom);
}
if ($breakoutrooms) {
$xml = self::fetch_endpoint_xml('getRecordings', ['recordID' => implode(',', $breakoutrooms)]);
if ($xml && $xml->returncode == 'SUCCESS' && isset($xml->recordings)) {
// If there were already created meetings.
foreach ($xml->recordings->recording as $subrecordingxml) {
$recording = self::parse_recording($subrecordingxml);
$recordings[$recording['recordID']] = $recording;
}
}
}
}
}
return $recordings;
}
/**
* Helper function to sort an array of recordings. It compares the startTime in two recording objects.
*
* @param array $recordings
* @return array
*/
public static function sort_recordings(array $recordings): array {
global $CFG;
uasort($recordings, function($a, $b) {
if ($a['startTime'] < $b['startTime']) {
return -1;
}
if ($a['startTime'] == $b['startTime']) {
return 0;
}
return 1;
});
return $recordings;
}
/**
* Helper function to parse an xml recording object and produce an array in the format used by the plugin.
*
* @param SimpleXMLElement $recording
*
* @return array
*/
public static function parse_recording(SimpleXMLElement $recording): array {
// Add formats.
$playbackarray = [];
foreach ($recording->playback->format as $format) {
$playbackarray[(string) $format->type] = [
'type' => (string) $format->type,
'url' => trim((string) $format->url), 'length' => (string) $format->length
];
// Add preview per format when existing.
if ($format->preview) {
$playbackarray[(string) $format->type]['preview'] =
self::parse_preview_images($format->preview);
}
}
// Add the metadata to the recordings array.
$metadataarray =
self::parse_recording_meta(get_object_vars($recording->metadata));
$recordingarray = [
'recordID' => (string) $recording->recordID,
'meetingID' => (string) $recording->meetingID,
'meetingName' => (string) $recording->name,
'published' => (string) $recording->published,
'state' => (string) $recording->state,
'startTime' => (string) $recording->startTime,
'endTime' => (string) $recording->endTime,
'playbacks' => $playbackarray
];
if (isset($recording->protected)) {
$recordingarray['protected'] = (string) $recording->protected;
}
return $recordingarray + $metadataarray;
}
/**
* Helper function to convert an xml recording metadata object to an array in the format used by the plugin.
*
* @param array $metadata
*
* @return array
*/
public static function parse_recording_meta(array $metadata): array {
$metadataarray = [];
foreach ($metadata as $key => $value) {
if (is_object($value)) {
$value = '';
}
$metadataarray['meta_' . $key] = $value;
}
return $metadataarray;
}
/**
* Helper function to convert an xml recording preview images to an array in the format used by the plugin.
*
* @param SimpleXMLElement $preview
*
* @return array
*/
public static function parse_preview_images(SimpleXMLElement $preview): array {
$imagesarray = [];
foreach ($preview->images->image as $image) {
$imagearray = ['url' => trim((string) $image)];
foreach ($image->attributes() as $attkey => $attvalue) {
$imagearray[$attkey] = (string) $attvalue;
}
array_push($imagesarray, $imagearray);
}
return $imagesarray;
}
}