first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,567 @@
<?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 completion_info;
use Exception;
use mod_bigbluebuttonbn\completion\custom_completion;
use mod_bigbluebuttonbn\instance;
use mod_bigbluebuttonbn\local\config;
use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception;
use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception;
use moodle_url;
use stdClass;
use user_picture;
/**
* The bigbluebutton proxy class.
*
* This class acts as a proxy between Moodle and the BigBlueButton API server,
* and handles all requests relating to the server and meetings.
*
* @package mod_bigbluebuttonbn
* @copyright 2010 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Jesus Federico (jesus [at] blindsidenetworks [dt] com)
*/
class bigbluebutton_proxy extends proxy_base {
/**
* Minimum poll interval for remote bigbluebutton server in seconds.
*/
const MIN_POLL_INTERVAL = 2;
/**
* Default poll interval for remote bigbluebutton server in seconds.
*/
const DEFAULT_POLL_INTERVAL = 5;
/**
* Builds and returns a url for joining a BigBlueButton meeting.
*
* @param instance $instance
* @param string|null $createtime
*
* @return string
*/
public static function get_join_url(
instance $instance,
?string $createtime
): string {
return self::internal_get_join_url($instance, $createtime);
}
/**
* Builds and returns a url for joining a BigBlueButton meeting.
*
* @param instance $instance
* @param string|null $createtime
* @param string $username
* @return string
*/
public static function get_guest_join_url(
instance $instance,
?string $createtime,
string $username
): string {
return self::internal_get_join_url($instance, $createtime, $username, true);
}
/**
* Internal helper method to builds and returns a url for joining a BigBlueButton meeting.
*
* @param instance $instance
* @param string|null $jointime = null
* @param string|null $userfullname
* @param bool $isguestjoin
* @return string
*/
private static function internal_get_join_url(
instance $instance,
?string $jointime,
string $userfullname = null,
bool $isguestjoin = false
): string {
$data = [
'meetingID' => $instance->get_meeting_id(),
'fullName' => $userfullname ?? $instance->get_user_fullname(),
'password' => $instance->get_current_user_password(),
'logoutURL' => $isguestjoin ? $instance->get_guest_access_url()->out(false) : $instance->get_logout_url()->out(false),
'role' => $instance->get_current_user_role()
];
if (!$isguestjoin) {
$data['userID'] = $instance->get_user_id();
$data['guest'] = "false";
} else {
$data['guest'] = "true";
}
if (!is_null($jointime)) {
$data['createTime'] = $jointime;
}
$currentlang = current_language();
if (!empty(trim($currentlang))) {
$data['userdata-bbb_override_default_locale'] = $currentlang;
}
if ($instance->is_profile_picture_enabled()) {
$user = $instance->get_user();
if (!empty($user->picture)) {
$data['avatarURL'] = self::get_avatar_url($user)->out(false);
}
}
return self::action_url('join', $data, [], $instance->get_instance_id());
}
/**
* Get user avatar URL
*
* @param stdClass $user
* @return moodle_url
*/
private static function get_avatar_url(stdClass $user): moodle_url {
global $PAGE;
$userpicture = new user_picture($user);
$userpicture->includetoken = true;
$userpicture->size = 3; // Size f3.
return $userpicture->get_url($PAGE);
}
/**
* Perform api request on BBB.
*
* @return null|string
*/
public static function get_server_version(): ?string {
$cache = cache::make('mod_bigbluebuttonbn', 'serverinfo');
$serverversion = $cache->get('serverversion');
if (!$serverversion) {
$xml = self::fetch_endpoint_xml('');
if (!$xml || $xml->returncode != 'SUCCESS') {
return null;
}
if (!isset($xml->version)) {
return null;
}
$serverversion = (string) $xml->version;
$cache->set('serverversion', $serverversion);
}
return (double) $serverversion;
}
/**
* Helper for getting the owner userid of a bigbluebuttonbn instance.
*
* @param stdClass $bigbluebuttonbn BigBlueButtonBN instance
* @return int ownerid (a valid user id or null if not registered/found)
*/
public static function get_instance_ownerid(stdClass $bigbluebuttonbn): int {
global $DB;
$filters = [
'bigbluebuttonbnid' => $bigbluebuttonbn->id,
'log' => 'Add',
];
return (int) $DB->get_field('bigbluebuttonbn_logs', 'userid', $filters);
}
/**
* Helper evaluates if a voicebridge number is unique.
*
* @param int $instance
* @param int $voicebridge
* @return bool
*/
public static function is_voicebridge_number_unique(int $instance, int $voicebridge): bool {
global $DB;
if ($voicebridge == 0) {
return true;
}
$select = 'voicebridge = ' . $voicebridge;
if ($instance != 0) {
$select .= ' AND id <>' . $instance;
}
if (!$DB->get_records_select('bigbluebuttonbn', $select)) {
return true;
}
return false;
}
/**
* Helper function validates a remote resource.
*
* @param string $url
* @return bool
*/
public static function is_remote_resource_valid(string $url): bool {
$urlhost = parse_url($url, PHP_URL_HOST);
$serverurlhost = parse_url(\mod_bigbluebuttonbn\local\config::get('server_url'), PHP_URL_HOST);
if ($urlhost == $serverurlhost) {
// Skip validation when the recording URL host is the same as the configured BBB server.
return true;
}
$cache = cache::make('mod_bigbluebuttonbn', 'validatedurls');
if ($cachevalue = $cache->get($urlhost)) {
// Skip validation when the recording URL was already validated.
return $cachevalue == 1;
}
$curl = new curl();
$curl->head($url);
$isvalid = false;
if ($info = $curl->get_info()) {
if ($info['http_code'] == 200) {
$isvalid = true;
} else {
debugging(
"Resources hosted by {$urlhost} are unreachable. Server responded with {$info['http_code']}",
DEBUG_DEVELOPER
);
$isvalid = false;
}
// Note: When a cache key is not found, it returns false.
// We need to distinguish between a result not found, and an invalid result.
$cache->set($urlhost, $isvalid ? 1 : 0);
}
return $isvalid;
}
/**
* Helper function enqueues one user for being validated as for completion.
*
* @param stdClass $bigbluebuttonbn
* @param int $userid
* @return void
*/
public static function enqueue_completion_event(stdClass $bigbluebuttonbn, int $userid): void {
try {
// Create the instance of completion_update_state task.
$task = new \mod_bigbluebuttonbn\task\completion_update_state();
// Add custom data.
$data = [
'bigbluebuttonbn' => $bigbluebuttonbn,
'userid' => $userid,
];
$task->set_custom_data($data);
// CONTRIB-7457: Task should be executed by a user, maybe Teacher as Student won't have rights for overriding.
// $ task -> set_userid ( $ user -> id );.
// Enqueue it.
\core\task\manager::queue_adhoc_task($task);
} catch (Exception $e) {
mtrace("Error while enqueuing completion_update_state task. " . (string) $e);
}
}
/**
* Helper function enqueues completion trigger.
*
* @param stdClass $bigbluebuttonbn
* @param int $userid
* @return void
*/
public static function update_completion_state(stdClass $bigbluebuttonbn, int $userid) {
global $CFG;
require_once($CFG->libdir . '/completionlib.php');
list($course, $cm) = get_course_and_cm_from_instance($bigbluebuttonbn, 'bigbluebuttonbn');
$completion = new completion_info($course);
if (!$completion->is_enabled($cm)) {
mtrace("Completion not enabled");
return;
}
$bbbcompletion = new custom_completion($cm, $userid);
if ($bbbcompletion->get_overall_completion_state()) {
mtrace("Completion for userid $userid and bigbluebuttonid {$bigbluebuttonbn->id} updated.");
$completion->update_state($cm, COMPLETION_COMPLETE, $userid, true);
} else {
// Still update state to current value (prevent unwanted caching).
$completion->update_state($cm, COMPLETION_UNKNOWN, $userid);
mtrace("Activity not completed for userid $userid and bigbluebuttonid {$bigbluebuttonbn->id}.");
}
}
/**
* Helper function returns an array with the profiles (with features per profile) for the different types
* of bigbluebuttonbn instances.
*
* @return array
*/
public static function get_instance_type_profiles(): array {
$instanceprofiles = [
instance::TYPE_ALL => [
'id' => instance::TYPE_ALL,
'name' => get_string('instance_type_default', 'bigbluebuttonbn'),
'features' => ['all']
],
instance::TYPE_ROOM_ONLY => [
'id' => instance::TYPE_ROOM_ONLY,
'name' => get_string('instance_type_room_only', 'bigbluebuttonbn'),
'features' => ['showroom', 'welcomemessage', 'voicebridge', 'waitformoderator', 'userlimit',
'recording', 'sendnotifications', 'lock', 'preuploadpresentation', 'permissions', 'schedule', 'groups',
'modstandardelshdr', 'availabilityconditionsheader', 'tagshdr', 'competenciessection',
'completionattendance', 'completionengagement', 'availabilityconditionsheader']
],
instance::TYPE_RECORDING_ONLY => [
'id' => instance::TYPE_RECORDING_ONLY,
'name' => get_string('instance_type_recording_only', 'bigbluebuttonbn'),
'features' => ['showrecordings', 'importrecordings', 'availabilityconditionsheader']
],
];
return $instanceprofiles;
}
/**
* Helper function returns an array with the profiles (with features per profile) for the different types
* of bigbluebuttonbn instances that the user is allowed to create.
*
* @param bool $room
* @param bool $recording
*
* @return array
*/
public static function get_instance_type_profiles_create_allowed(bool $room, bool $recording): array {
$profiles = self::get_instance_type_profiles();
if (!$room) {
unset($profiles[instance::TYPE_ROOM_ONLY]);
unset($profiles[instance::TYPE_ALL]);
}
if (!$recording) {
unset($profiles[instance::TYPE_RECORDING_ONLY]);
unset($profiles[instance::TYPE_ALL]);
}
return $profiles;
}
/**
* Helper function returns an array with the profiles (with features per profile) for the different types
* of bigbluebuttonbn instances.
*
* @param array $profiles
*
* @return array
*/
public static function get_instance_profiles_array(array $profiles = []): array {
$profilesarray = [];
foreach ($profiles as $key => $profile) {
$profilesarray[$profile['id']] = $profile['name'];
}
return $profilesarray;
}
/**
* Return the status of an activity [open|not_started|ended].
*
* @param instance $instance
* @return string
*/
public static function view_get_activity_status(instance $instance): string {
$now = time();
if (!empty($instance->get_instance_var('openingtime')) && $now < $instance->get_instance_var('openingtime')) {
// The activity has not been opened.
return 'not_started';
}
if (!empty($instance->get_instance_var('closingtime')) && $now > $instance->get_instance_var('closingtime')) {
// The activity has been closed.
return 'ended';
}
// The activity is open.
return 'open';
}
/**
* Ensure that the remote server was contactable.
*
* @param instance $instance
*/
public static function require_working_server(instance $instance): void {
$version = null;
try {
$version = self::get_server_version();
} catch (server_not_available_exception $e) {
self::handle_server_not_available($instance);
}
if (empty($version)) {
self::handle_server_not_available($instance);
}
}
/**
* Handle the server not being available.
*
* @param instance $instance
*/
public static function handle_server_not_available(instance $instance): void {
\core\notification::add(
self::get_server_not_available_message($instance),
\core\notification::ERROR
);
redirect(self::get_server_not_available_url($instance));
}
/**
* Get message when server not available
*
* @param instance $instance
* @return string
*/
public static function get_server_not_available_message(instance $instance): string {
if ($instance->is_admin()) {
return get_string('view_error_unable_join', 'mod_bigbluebuttonbn');
} else if ($instance->is_moderator()) {
return get_string('view_error_unable_join_teacher', 'mod_bigbluebuttonbn');
} else {
return get_string('view_error_unable_join_student', 'mod_bigbluebuttonbn');
}
}
/**
* Get URL to the page displaying that the server is not available
*
* @param instance $instance
* @return string
*/
public static function get_server_not_available_url(instance $instance): string {
if ($instance->is_admin()) {
return new moodle_url('/admin/settings.php', ['section' => 'modsettingbigbluebuttonbn']);
} else if ($instance->is_moderator()) {
return new moodle_url('/course/view.php', ['id' => $instance->get_course_id()]);
} else {
return new moodle_url('/course/view.php', ['id' => $instance->get_course_id()]);
}
}
/**
* Create a Meeting
*
* @param array $data
* @param array $metadata
* @param string|null $presentationname
* @param string|null $presentationurl
* @param int|null $instanceid
* @return array
* @throws bigbluebutton_exception
*/
public static function create_meeting(
array $data,
array $metadata,
?string $presentationname = null,
?string $presentationurl = null,
?int $instanceid = null
): array {
$createmeetingurl = self::action_url('create', $data, $metadata, $instanceid);
$curl = new curl();
if (!is_null($presentationname) && !is_null($presentationurl)) {
$payload = "<?xml version='1.0' encoding='UTF-8'?><modules><module name='presentation'><document url='" .
$presentationurl . "' /></module></modules>";
$xml = $curl->post($createmeetingurl, $payload);
} else {
$xml = $curl->get($createmeetingurl);
}
self::assert_returned_xml($xml);
if (empty($xml->meetingID)) {
throw new bigbluebutton_exception('general_error_cannot_create_meeting');
}
if ($xml->hasBeenForciblyEnded === 'true') {
throw new bigbluebutton_exception('index_error_forciblyended');
}
return [
'meetingID' => (string) $xml->meetingID,
'internalMeetingID' => (string) $xml->internalMeetingID,
'attendeePW' => (string) $xml->attendeePW,
'moderatorPW' => (string) $xml->moderatorPW
];
}
/**
* Get meeting info for a given meeting id
*
* @param string $meetingid
* @param int|null $instanceid
* @return array
*/
public static function get_meeting_info(string $meetingid, ?int $instanceid = null): array {
$xmlinfo = self::fetch_endpoint_xml('getMeetingInfo', ['meetingID' => $meetingid], [], $instanceid);
self::assert_returned_xml($xmlinfo, ['meetingid' => $meetingid]);
return (array) $xmlinfo;
}
/**
* Perform end meeting on BBB.
*
* @param string $meetingid
* @param string $modpw
* @param int|null $instanceid
*/
public static function end_meeting(string $meetingid, string $modpw, ?int $instanceid = null): void {
$xml = self::fetch_endpoint_xml('end', ['meetingID' => $meetingid, 'password' => $modpw], [], $instanceid);
self::assert_returned_xml($xml, ['meetingid' => $meetingid]);
}
/**
* Helper evaluates if the bigbluebutton server used belongs to blindsidenetworks domain.
*
* @return bool
*/
public static function is_bn_server() {
if (config::get('bn_server')) {
return true;
}
$parsedurl = parse_url(config::get('server_url'));
if (!isset($parsedurl['host'])) {
return false;
}
$h = $parsedurl['host'];
$hends = explode('.', $h);
$hendslength = count($hends);
return ($hends[$hendslength - 1] == 'com' && $hends[$hendslength - 2] == 'blindsidenetworks');
}
/**
* Get the poll interval as it is set in the configuration
*
* If configuration value is under the threshold of {@see self::MIN_POLL_INTERVAL},
* then return the {@see self::MIN_POLL_INTERVAL} value.
*
* @return int the poll interval in seconds
*/
public static function get_poll_interval(): int {
$pollinterval = intval(config::get('poll_interval'));
if ($pollinterval < self::MIN_POLL_INTERVAL) {
$pollinterval = self::MIN_POLL_INTERVAL;
}
return $pollinterval;
}
}
@@ -0,0 +1,157 @@
<?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/>.
/**
* A curl wrapper for bbb.
*
* @package mod_bigbluebuttonbn
* @copyright 2021 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_bigbluebuttonbn\local\proxy;
use SimpleXMLElement;
defined('MOODLE_INTERNAL') || die;
global $CFG;
require_once("{$CFG->libdir}/filelib.php");
/**
* A curl wrapper for bbb.
*
* @package mod_bigbluebuttonbn
* @copyright 2021 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class curl extends \curl {
/** @var string */
protected $contenttype;
/**
* Constructor for the class.
*/
public function __construct() {
$settings = [];
if (debugging()) {
$settings = ['ignoresecurity' => true];
}
parent::__construct($settings);
$this->setopt(['SSL_VERIFYPEER' => true]);
$this->set_content_type('application/xml');
}
/**
* Fetch the content type.
*/
public function get_content_type(): string {
return $this->contenttype;
}
/**
* Set the desired current content type.
*
* @param string $type
* @return self
*/
public function set_content_type(string $type): self {
$this->contenttype = $type;
return $this;
}
/**
* HTTP POST method
*
* @param string $url
* @param array|string $params
* @param array $options
* @return null|SimpleXMLElement Null on error
*/
public function post($url, $params = '', $options = []) {
if (!is_string($params)) {
debugging('Only string parameters are supported', DEBUG_DEVELOPER);
$params = '';
}
$options = array_merge($options, [
'CURLOPT_HTTPHEADER' => [
'Content-Type: ' . $this->get_content_type(),
'Content-Length: ' . strlen($params),
'Content-Language: en-US',
]
]);
return $this->handle_response(parent::post($url, $params, $options));
}
/**
* Fetch the specified URL via a HEAD request.
*
* @param string $url
* @param array $options
*/
public function head($url, $options = []) {
$options['followlocation'] = true;
$options['timeout'] = 1;
parent::head($url, $options);
return $this->get_info();
}
/**
* Fetch the specified URL via a GET request.
*
* @param string $url
* @param string $params
* @param array $options
*/
public function get($url, $params = [], $options = []) {
return $this->handle_response(parent::get($url, $params, $options));
}
/**
* Handle the response.
*
* @param mixed $response
* @return null|SimpleXMLElement Null on error
*/
protected function handle_response($response): ?SimpleXMLElement {
if (!$response) {
debugging('No response returned for call', DEBUG_DEVELOPER);
return null;
}
$previous = libxml_use_internal_errors(true);
try {
$xml = simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NOBLANKS);
} catch (Exception $e) {
libxml_use_internal_errors($previous);
debugging('Caught exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
return null;
}
if ($xml instanceof SimpleXMLElement) {
return $xml;
}
$debugabstract = html_to_text($response);
$debugabstract = substr($debugabstract, 0, 1024); // Limit to small amount of info so we do not overload logs.
debugging('Issue retrieving information from the server: ' . $debugabstract, DEBUG_DEVELOPER);
return null;
}
}
@@ -0,0 +1,206 @@
<?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 mod_bigbluebuttonbn\extension;
use mod_bigbluebuttonbn\local\config;
use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception;
use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception;
use mod_bigbluebuttonbn\plugin;
use moodle_url;
/**
* The abstract proxy base class.
*
* This class provides common and shared functions used when interacting with
* the BigBlueButton API server.
*
* @package mod_bigbluebuttonbn
* @copyright 2010 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Jesus Federico (jesus [at] blindsidenetworks [dt] com)
*/
abstract class proxy_base {
/**
* Sometimes the server sends back some error and errorKeys that
* can be converted to Moodle error messages
*/
const BBB_TO_MOODLE_ERROR_CODE = [
'checksumError' => 'index_error_checksum',
'notFound' => 'general_error_not_found',
'maxConcurrent' => 'view_error_max_concurrent',
];
/**
* Returns the right URL for the action specified.
*
* @param string $action
* @param array $data
* @param array $metadata
* @param int|null $instanceid
* @return string
*/
protected static function action_url(
string $action = '',
array $data = [],
array $metadata = [],
?int $instanceid = null
): string {
$baseurl = self::sanitized_url() . $action . '?';
['data' => $additionaldata, 'metadata' => $additionalmetadata] =
extension::action_url_addons($action, $data, $metadata, $instanceid);
$data = array_merge($data, $additionaldata ?? []);
$metadata = array_merge($metadata, $additionalmetadata ?? []);
$metadata = array_combine(array_map(function($k) {
return 'meta_' . $k;
}, array_keys($metadata)), $metadata);
$params = http_build_query($data + $metadata, '', '&');
$checksum = self::get_checksum($action, $params);
return $baseurl . $params . '&checksum=' . $checksum;
}
/**
* Makes sure the url used doesn't is in the format required.
*
* @return string
*/
protected static function sanitized_url(): string {
$serverurl = trim(config::get('server_url'));
if (PHPUNIT_TEST) {
$serverurl = (new moodle_url(TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER))->out(false);
}
if (substr($serverurl, -1) == '/') {
$serverurl = rtrim($serverurl, '/');
}
if (substr($serverurl, -4) == '/api') {
$serverurl = rtrim($serverurl, '/api');
}
return $serverurl . '/api/';
}
/**
* Makes sure the shared_secret used doesn't have trailing white characters.
*
* @return string
*/
protected static function sanitized_secret(): string {
return trim(config::get('shared_secret'));
}
/**
* Throw an exception if there is a problem in the returned XML value
*
* @param \SimpleXMLElement|bool $xml
* @param array|null $additionaldetails
* @throws bigbluebutton_exception
* @throws server_not_available_exception
*/
protected static function assert_returned_xml($xml, ?array $additionaldetails = null): void {
$messagekey = '';
if (!empty($xml)) {
$messagekey = (string) ($xml->messageKey ?? '');
}
if (empty($xml) || static::is_known_server_unavailable_errorcode($messagekey)) {
$errorcode = self::get_errorcode_from_xml_messagekey($messagekey);
throw new server_not_available_exception(
$errorcode,
plugin::COMPONENT,
(new moodle_url('/admin/settings.php?section=modsettingbigbluebuttonbn'))->out(),
);
}
// If it is a checksum error, this is equivalent to the server not being available.
// So we treat it the same way as if there is not answer.
if (is_bool($xml) && $xml) {
// Nothing to do here, this might be a post returning that everything went well.
return;
}
if ((string) $xml->returncode == 'FAILED') {
$errorcode = self::get_errorcode_from_xml_messagekey($messagekey);
if (!$additionaldetails) {
$additionaldetails = [];
}
$additionaldetails['xmlmessage'] = (string) $xml->message ?? '';
throw new bigbluebutton_exception($errorcode, json_encode($additionaldetails));
}
}
/**
* Get Moodle error code from returned Message Key
*
* @param string $messagekey
* @return string
*/
private static function get_errorcode_from_xml_messagekey(string $messagekey): string {
$errorcode = 'general_error_no_answer';
if ($messagekey) {
$errorcode = self::BBB_TO_MOODLE_ERROR_CODE[$messagekey] ?? $errorcode;
}
return $errorcode;
}
/**
* Get Moodle error code from returned Message Key
*
* @param string $messagekey
* @return string
*/
private static function is_known_server_unavailable_errorcode(string $messagekey): string {
// For now, only checksumError is supposed to mean that the server is unavailable.
// Other errors are recoverable.
return in_array($messagekey, ['checksumError']);
}
/**
* Fetch the XML from an endpoint and test for success.
*
* If the result could not be loaded, or the returncode was not 'SUCCESS', a null value is returned.
*
* @param string $action
* @param array $data
* @param array $metadata
* @param int|null $instanceid
* @return null|bool|\SimpleXMLElement
*/
protected static function fetch_endpoint_xml(
string $action,
array $data = [],
array $metadata = [],
?int $instanceid = null
) {
if (PHPUNIT_TEST && !defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) {
return true; // In case we still use fetch and mock server is not defined, this prevents
// an error. This can happen if a function from lib.php is called in test from other modules
// for example.
}
$curl = new curl();
return $curl->get(self::action_url($action, $data, $metadata, $instanceid));
}
/**
* Get checksum
*
* @param string $action
* @param string $params
* @return string
*/
public static function get_checksum(string $action, string $params): string {
return hash(config::get('checksum_algorithm'), $action . $params . self::sanitized_secret());
}
}
@@ -0,0 +1,405 @@
<?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 {
$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 retrieve recordings that failed to be fetched from a BigBlueButton server.
*
* @param array $keyids list of recordingids
* @return array array of recording recordingids not fetched from server
* and sorted by {@see recording_proxy::sort_recordings}
*/
public static function fetch_missing_recordings(array $keyids = []): array {
$unfetchedids = [];
$pagesize = 25;
// If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''.
if (empty($keyids)) {
return $unfetchedids;
}
while ($ids = array_splice($keyids, 0, $pagesize)) {
// We make getRecordings API call to check recordings are successfully retrieved.
$xml = self::fetch_endpoint_xml('getRecordings', ['recordID' => implode(',', $ids), 'state' => 'any']);
if (!$xml || $xml->returncode != 'SUCCESS' || !isset($xml->recordings)) {
$unfetchedids = array_merge($unfetchedids, $ids);
continue; // We will keep record of all unfetched ids.
}
}
return $unfetchedids;
}
/**
* 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;
}
}