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,135 @@
<?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\bigbluebutton\recordings;
use mod_bigbluebuttonbn\instance;
use mod_bigbluebuttonbn\recording;
use mod_bigbluebuttonbn\local\config;
/**
* Collection of helper methods for handling recordings actions in Moodle.
*
* Utility class for meeting actions
*
* @package mod_bigbluebuttonbn
* @copyright 2021 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class recording_action {
/**
* Import recording
*
* @param recording $recording
* @param instance $targetinstance
*/
public static function import(recording $recording, instance $targetinstance): void {
$recording->create_imported_recording($targetinstance);
}
/**
* Helper for performing delete on recordings.
*
* @param recording $recording
*/
public static function delete(recording $recording): void {
// As the recordingid was not identified as imported recording link, execute delete on a real recording.
// Step 1, delete imported links associated to the recording.
$recordingstodelete = recording::get_records(['recordingid' => $recording->get('recordingid'),
'imported' => true]);
foreach ($recordingstodelete as $rec) {
$rec->delete();
}
$recording->delete();
}
/**
* Helper for performing edit on recordings.
*
* @param recording $recording
*/
public static function edit(recording $recording): void {
$recording->update();
}
/**
* Helper for performing unprotect on recordings.
*
* @param recording $recording
*/
public static function unprotect(recording $recording): void {
if (!(boolean) config::get('recording_protect_editable')) {
// Recording protect action through UI is disabled, there is no need to do anything else.
throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'unprotect');
}
if ($recording->get('imported')) {
// Imported recordings can not be unprotected. There is no need to do anything else.
throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'unprotect');
}
$recording->set('protected', false);
$recording->update();
}
/**
* Helper for performing protect on recordings.
*
* @param recording $recording
*/
public static function protect(recording $recording): void {
if (!(boolean) config::get('recording_protect_editable')) {
// Recording protect action through UI is disabled, there is no need to do anything else.
throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'protect');
}
if ($recording->get('imported')) {
// Imported recordings can not be unprotected. There is no need to do anything else.
throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'protect');
}
$recording->set('protected', true);
$recording->update();
}
/**
* Helper for performing unpublish on recordings.
*
* @param recording $recording
*/
public static function unpublish(recording $recording): void {
if ($recording->get('imported')) {
/* Since the recording link is the one fetched from the BBB server, imported recordings can not be
* unpublished. There is no need to do anything else.
*/
throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'unpublish');
}
$recording->set('published', false);
$recording->update();
}
/**
* Helper for performing publish on recordings.
*
* @param recording $recording
*/
public static function publish(recording $recording): void {
if ($recording->get('imported')) {
/* Since the recording link is the one fetched from the BBB server, imported recordings can not be
* unpublished. There is no need to do anything else.
*/
throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'publish');
}
$recording->set('published', true);
$recording->update();
}
}
@@ -0,0 +1,319 @@
<?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\bigbluebutton\recordings;
use mod_bigbluebuttonbn\instance;
use mod_bigbluebuttonbn\local\config;
use mod_bigbluebuttonbn\local\helpers\roles;
use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy;
use mod_bigbluebuttonbn\output\recording_description_editable;
use mod_bigbluebuttonbn\output\recording_name_editable;
use mod_bigbluebuttonbn\output\recording_row_actionbar;
use mod_bigbluebuttonbn\output\recording_row_playback;
use mod_bigbluebuttonbn\output\recording_row_preview;
use mod_bigbluebuttonbn\recording;
use stdClass;
/**
* The recordings_data.
*
* @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.david [at] call-learning [dt] fr)
* @author Jesus Federico (jesus [at] blindsidenetworks [dt] com)
*/
class recording_data {
/**
* Get the full recording table
*
* @param array $recordings
* @param array $tools
* @param instance|null $instance
* @param int $courseid
* @return array
*/
public static function get_recording_table(array $recordings, array $tools, instance $instance = null,
int $courseid = 0): array {
$typeprofiles = bigbluebutton_proxy::get_instance_type_profiles();
$typeprofile = empty($instance) ? $typeprofiles[0] : $typeprofiles[$instance->get_type()];
$lang = get_string('locale', 'core_langconfig');
$locale = substr($lang, 0, strpos($lang, '.'));
$tabledata = [
'activity' => empty($instance) ? '' : bigbluebutton_proxy::view_get_activity_status($instance),
'ping_interval' => (int) config::get('waitformoderator_ping_interval') * 1000,
'locale' => substr($locale, 0, strpos($locale, '_')),
'profile_features' => $typeprofile['features'],
'columns' => [],
'data' => '',
];
$hascapabilityincourse = empty($instance) && roles::has_capability_in_course($courseid,
'mod/bigbluebuttonbn:managerecordings');
$data = [];
// Build table content.
foreach ($recordings as $recording) {
$rowtools = $tools;
// Protected recordings may be enabled or disabled from UI through configuration.
if (!(boolean) config::get('recording_protect_editable')) {
$rowtools = array_diff($rowtools, ['protect', 'unprotect']);
}
// Protected recordings is not a standard feature, remove actions when protected flag is not present.
if (in_array('protect', $rowtools) && $recording->get('protected') === null) {
$rowtools = array_diff($rowtools, ['protect', 'unprotect']);
}
$rowdata = self::row($instance, $recording, $rowtools);
if (!empty($rowdata)) {
$data[] = $rowdata;
}
}
$columns = [
[
'key' => 'playback',
'label' => get_string('view_recording_playback', 'bigbluebuttonbn'),
'width' => '125px',
'type' => 'html',
'allowHTML' => true,
],
[
'key' => 'recording',
'label' => get_string('view_recording_name', 'bigbluebuttonbn'),
'width' => '125px',
'type' => 'html',
'allowHTML' => true,
],
[
'key' => 'description',
'label' => get_string('view_recording_description', 'bigbluebuttonbn'),
'sortable' => true,
'width' => '250px',
'type' => 'html',
'allowHTML' => true,
],
];
// Initialize table headers.
$ispreviewenabled = !empty($instance) && self::preview_enabled($instance);
$ispreviewenabled = $ispreviewenabled || $hascapabilityincourse;
if ($ispreviewenabled) {
$columns[] = [
'key' => 'preview',
'label' => get_string('view_recording_preview', 'bigbluebuttonbn'),
'width' => '250px',
'type' => 'html',
'allowHTML' => true,
];
}
$columns[] = [
'key' => 'date',
'label' => get_string('view_recording_date', 'bigbluebuttonbn'),
'sortable' => true,
'width' => '225px',
'type' => 'html',
'formatter' => 'customDate',
];
$columns[] = [
'key' => 'duration',
'label' => get_string('view_recording_duration', 'bigbluebuttonbn'),
'width' => '50px',
'allowHTML' => false,
'sortable' => true,
];
// Either instance is empty and we must show the toolbar (with restricted content) or we check
// specific rights related to the instance.
$canmanagerecordings = !empty($instance) && $instance->can_manage_recordings();
$canmanagerecordings = $canmanagerecordings || $hascapabilityincourse;
if ($canmanagerecordings) {
$columns[] = [
'key' => 'actionbar',
'label' => get_string('view_recording_actionbar', 'bigbluebuttonbn'),
'width' => '120px',
'type' => 'html',
'allowHTML' => true,
];
}
$tabledata['columns'] = $columns;
$tabledata['data'] = json_encode($data);
return $tabledata;
}
/**
* Helper function builds a row for the data used by the recording table.
*
* TODO: replace this with templates whenever possible so we just
* return the data via the API.
*
* @param instance|null $instance $instance
* @param recording $rec a recording row
* @param array|null $tools
* @param int|null $courseid
* @return stdClass|null
*/
public static function row(?instance $instance, recording $rec, ?array $tools = null, ?int $courseid = 0): ?stdClass {
global $PAGE;
$hascapabilityincourse = empty($instance) && roles::has_capability_in_course($courseid,
'mod/bigbluebuttonbn:managerecordings');
$renderer = $PAGE->get_renderer('mod_bigbluebuttonbn');
foreach ($tools as $key => $tool) {
if ((!empty($instance) && !$instance->can_perform_on_recordings($tool))
|| (empty($instance) && !$hascapabilityincourse)) {
unset($tools[$key]);
}
}
if (!self::include_recording_table_row($instance, $rec)) {
return null;
}
$rowdata = new stdClass();
// Set recording_playback.
$recordingplayback = new recording_row_playback($rec, $instance);
$rowdata->playback = $renderer->render($recordingplayback);
if (empty($instance)) {
// Set activity name.
$rowdata->recording = $rec->get('name');
// Set activity description.
$rowdata->description = $rec->get('description');
} else {
// Set activity name.
$recordingname = new recording_name_editable($rec, $instance);
$rowdata->recording = $renderer->render_inplace_editable($recordingname);
// Set activity description.
$recordingdescription = new recording_description_editable($rec, $instance);
$rowdata->description = $renderer->render_inplace_editable($recordingdescription);
}
if ((!empty($instance) && self::preview_enabled($instance)) || $hascapabilityincourse) {
// Set recording_preview.
$rowdata->preview = '';
if ($rec->get('playbacks')) {
$rowpreview = new recording_row_preview($rec);
$rowdata->preview = $renderer->render($rowpreview);
}
}
// Set date.
$starttime = $rec->get('starttime');
$rowdata->date = !is_null($starttime) ? floatval($starttime) : 0;
// Set duration.
$rowdata->duration = self::row_duration($rec);
// Set actionbar, if user is allowed to manage recordings.
if ((!empty($instance) && $instance->can_manage_recordings()) || $hascapabilityincourse) {
$actionbar = new recording_row_actionbar($rec, $tools);
$rowdata->actionbar = $renderer->render($actionbar);
}
return $rowdata;
}
/**
* Helper function evaluates if recording preview should be included.
*
* @param instance $instance
* @return bool
*/
public static function preview_enabled(instance $instance): bool {
return $instance->get_instance_var('recordings_preview') == '1';
}
/**
* Helper function converts recording duration used in row for the data used by the recording table.
*
* @param recording $recording
* @return int
*/
protected static function row_duration(recording $recording): int {
$playbacks = $recording->get('playbacks');
if (empty($playbacks)) {
return 0;
}
foreach ($playbacks as $playback) {
// Ignore restricted playbacks.
if (array_key_exists('restricted', $playback) && strtolower($playback['restricted']) == 'true') {
continue;
}
// Take the length form the fist playback with an actual value.
if (!empty($playback['length'])) {
return intval($playback['length']);
}
}
return 0;
}
/**
* Helper function to handle yet unknown recording types
*
* @param string $playbacktype : for now presentation, video, statistics, capture, notes, podcast
* @return string the matching language string or a capitalised version of the provided string
*/
public static function type_text(string $playbacktype): string {
// Check first if string exists, and if it does not, just default to the capitalised version of the string.
$text = ucwords($playbacktype);
$typestringid = 'view_recording_format_' . $playbacktype;
if (get_string_manager()->string_exists($typestringid, 'bigbluebuttonbn')) {
$text = get_string($typestringid, 'bigbluebuttonbn');
}
return $text;
}
/**
* Helper function evaluates if recording row should be included in the table.
*
* @param instance|null $instance
* @param recording $rec a bigbluebuttonbn_recordings row
* @return bool
*/
protected static function include_recording_table_row(?instance $instance, recording $rec): bool {
if (empty($instance)) {
return roles::has_capability_in_course($rec->get('courseid'), 'mod/bigbluebuttonbn:managerecordings');
}
// Exclude unpublished recordings, only if user has no rights to manage them.
if (!$rec->get('published') && !$instance->can_manage_recordings()) {
return false;
}
// Imported recordings are always shown as long as they are published.
if ($rec->get('imported')) {
return true;
}
// When show imported recordings only is enabled, exclude all other recordings.
if ($instance->get_recordings_imported() && !$rec->get('imported')) {
return false;
}
// Administrators and moderators are always allowed.
if ($instance->is_admin() || $instance->is_moderator()) {
return true;
}
// When groups are enabled, exclude those to which the user doesn't have access to.
if ($instance->uses_groups() && !$instance->can_manage_recordings()) {
if (groups_get_activity_groupmode($instance->get_cm()) == VISIBLEGROUPS) {
// In case we are in visible group mode, we show all recordings.
return true;
}
// Else we check if the Recording group is the same as the instance. Instance group
// being the group chosen for this instance.
return intval($rec->get('groupid')) === $instance->get_group_id();
}
return true;
}
}
@@ -0,0 +1,283 @@
<?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;
use mod_bigbluebuttonbn\instance;
use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy;
use mod_bigbluebuttonbn\recording;
/**
* Handles the global configuration based on config.php.
*
* @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 config {
/** @var string Default bigbluebutton server url */
public const DEFAULT_SERVER_URL = 'https://test-moodle.blindsidenetworks.com/bigbluebutton/';
/** @var string Default bigbluebutton server shared secret */
public const DEFAULT_SHARED_SECRET = '0b21fcaf34673a8c3ec8ed877d76ae34';
/** @var string the default bigbluebutton checksum algorithm */
public const DEFAULT_CHECKSUM_ALGORITHM = 'SHA256';
/** @var array list of supported bigbluebutton checksum algorithm */
const CHECKSUM_ALGORITHMS = [
self::DEFAULT_CHECKSUM_ALGORITHM,
'SHA1',
'SHA512'
];
/**
* Returns moodle version.
*
* @return string
*/
protected static function get_moodle_version_major(): string {
global $CFG;
$versionarray = explode('.', $CFG->version);
return $versionarray[0];
}
/**
* Returns configuration default values.
*
* @return array
*/
protected static function defaultvalues(): array {
return [
'server_url' => '',
'shared_secret' => '',
'voicebridge_editable' => false,
'importrecordings_enabled' => false,
'importrecordings_from_deleted_enabled' => false,
'waitformoderator_default' => false,
'waitformoderator_editable' => true,
'waitformoderator_ping_interval' => '10',
'waitformoderator_cache_ttl' => '60',
'userlimit_default' => '0',
'userlimit_editable' => false,
'preuploadpresentation_editable' => false,
'recordingready_enabled' => false,
'recordingstatus_enabled' => false,
'meetingevents_enabled' => false,
'participant_moderator_default' => '0',
'profile_picture_enabled' => false,
'scheduled_pre_opening' => '10',
'recordings_enabled' => true,
'recordings_deleted_default' => false,
'recordings_deleted_editable' => false,
'recordings_imported_default' => false,
'recordings_imported_editable' => false,
'recordings_preview_default' => true,
'recordings_preview_editable' => false,
'recording_default' => true,
'recording_editable' => true,
'recording_refresh_period' => recording::RECORDING_REFRESH_DEFAULT_PERIOD,
'recording_all_from_start_default' => false,
'recording_all_from_start_editable' => false,
'recording_hide_button_default' => false,
'recording_hide_button_editable' => false,
'recording_protect_editable' => true,
'general_warning_message' => '',
'general_warning_roles' => 'editingteacher,teacher',
'general_warning_box_type' => 'info',
'general_warning_button_text' => '',
'general_warning_button_href' => '',
'general_warning_button_class' => '',
'muteonstart_default' => false,
'muteonstart_editable' => false,
'disablecam_default' => false,
'disablecam_editable' => true,
'disablemic_default' => false,
'disablemic_editable' => true,
'disableprivatechat_default' => false,
'disableprivatechat_editable' => true,
'disablepublicchat_default' => false,
'disablepublicchat_editable' => true,
'disablenote_default' => false,
'disablenote_editable' => true,
'hideuserlist_default' => false,
'hideuserlist_editable' => true,
'welcome_default' => '',
'welcome_editable' => true,
'default_dpa_accepted' => false,
'poll_interval' => bigbluebutton_proxy::DEFAULT_POLL_INTERVAL,
'checksum_algorithm' => self::DEFAULT_CHECKSUM_ALGORITHM,
];
}
/**
* Returns default value for an specific setting.
*
* @param string $setting
* @return string|null
*/
public static function defaultvalue(string $setting): ?string {
$defaultvalues = self::defaultvalues();
if (!array_key_exists($setting, $defaultvalues)) {
return null;
}
return $defaultvalues[$setting];
}
/**
* Returns value for an specific setting.
*
* @param string $setting
* @return string
*/
public static function get(string $setting): string {
global $CFG;
if (isset($CFG->bigbluebuttonbn[$setting])) {
return (string) $CFG->bigbluebuttonbn[$setting];
}
if (isset($CFG->{'bigbluebuttonbn_' . $setting})) {
return (string) $CFG->{'bigbluebuttonbn_' . $setting};
}
return (string) self::defaultvalue($setting);
}
/**
* Validates if recording settings are enabled.
*
* @return bool
*/
public static function recordings_enabled(): bool {
return (boolean) self::get('recordings_enabled');
}
/**
* Validates if imported recording settings are enabled.
*
* @return bool
*/
public static function importrecordings_enabled(): bool {
return (boolean) self::get('importrecordings_enabled');
}
/**
* Check if bbb server credentials are invalid.
*
* @return bool
*/
public static function server_credentials_invalid(): bool {
// Test server credentials across all versions of the plugin are flagged.
$parsedurl = parse_url(self::get('server_url'));
$defaultserverurl = parse_url(self::DEFAULT_SERVER_URL);
if (!isset($parsedurl['host'])) {
return false;
}
if (strpos($parsedurl['host'], $defaultserverurl['host']) === 0) {
return true;
}
if (strpos($parsedurl['host'], 'test-install.blindsidenetworks.com') === 0) {
return true;
}
return false;
}
/**
* Wraps current settings in an array.
*
* @return array
*/
public static function get_options(): array {
return [
'version_major' => self::get_moodle_version_major(),
'voicebridge_editable' => self::get('voicebridge_editable'),
'importrecordings_enabled' => self::get('importrecordings_enabled'),
'importrecordings_from_deleted_enabled' => self::get('importrecordings_from_deleted_enabled'),
'waitformoderator_default' => self::get('waitformoderator_default'),
'waitformoderator_editable' => self::get('waitformoderator_editable'),
'userlimit_default' => self::get('userlimit_default'),
'userlimit_editable' => self::get('userlimit_editable'),
'preuploadpresentation_editable' => self::get('preuploadpresentation_editable'),
'recordings_enabled' => self::get('recordings_enabled'),
'meetingevents_enabled' => self::get('meetingevents_enabled'),
'recordings_deleted_default' => self::get('recordings_deleted_default'),
'recordings_deleted_editable' => self::get('recordings_deleted_editable'),
'recordings_imported_default' => self::get('recordings_imported_default'),
'recordings_imported_editable' => self::get('recordings_imported_editable'),
'recordings_preview_default' => self::get('recordings_preview_default'),
'recordings_preview_editable' => self::get('recordings_preview_editable'),
'recording_default' => self::get('recording_default'),
'recording_editable' => self::get('recording_editable'),
'recording_refresh_period' => self::get('recording_refresh_period'),
'recording_all_from_start_default' => self::get('recording_all_from_start_default'),
'recording_all_from_start_editable' => self::get('recording_all_from_start_editable'),
'recording_hide_button_default' => self::get('recording_hide_button_default'),
'recording_hide_button_editable' => self::get('recording_hide_button_editable'),
'recording_protect_editable' => self::get('recording_protect_editable'),
'general_warning_message' => self::get('general_warning_message'),
'general_warning_box_type' => self::get('general_warning_box_type'),
'general_warning_button_text' => self::get('general_warning_button_text'),
'general_warning_button_href' => self::get('general_warning_button_href'),
'general_warning_button_class' => self::get('general_warning_button_class'),
'muteonstart_editable' => self::get('muteonstart_editable'),
'muteonstart_default' => self::get('muteonstart_default'),
'disablecam_editable' => self::get('disablecam_editable'),
'disablecam_default' => self::get('disablecam_default'),
'disablemic_editable' => self::get('disablemic_editable'),
'disablemic_default' => self::get('disablemic_default'),
'disableprivatechat_editable' => self::get('disableprivatechat_editable'),
'disableprivatechat_default' => self::get('disableprivatechat_default'),
'disablepublicchat_editable' => self::get('disablepublicchat_editable'),
'disablepublicchat_default' => self::get('disablepublicchat_default'),
'disablenote_editable' => self::get('disablenote_editable'),
'disablenote_default' => self::get('disablenote_default'),
'hideuserlist_editable' => self::get('hideuserlist_editable'),
'hideuserlist_default' => self::get('hideuserlist_default'),
'welcome_default' => self::get('welcome_default'),
'welcome_editable' => self::get('welcome_editable'),
'poll_interval' => self::get('poll_interval'),
'guestaccess_enabled' => self::get('guestaccess_enabled'),
];
}
/**
* Helper function returns an array with enabled features for an specific profile type.
*
* @param array $typeprofiles
* @param string|null $type
*
* @return array
*/
public static function get_enabled_features(array $typeprofiles, ?string $type = null): array {
$enabledfeatures = [];
$features = $typeprofiles[instance::TYPE_ALL]['features'];
if (!is_null($type) && key_exists($type, $typeprofiles)) {
$features = $typeprofiles[$type]['features'];
}
$enabledfeatures['showroom'] = (in_array('all', $features) || in_array('showroom', $features));
// Evaluates if recordings are enabled for the Moodle site.
$enabledfeatures['showrecordings'] = false;
if (self::recordings_enabled()) {
$enabledfeatures['showrecordings'] = (in_array('all', $features) || in_array('showrecordings', $features));
}
$enabledfeatures['importrecordings'] = false;
if (self::importrecordings_enabled()) {
$enabledfeatures['importrecordings'] = (in_array('all', $features) || in_array('importrecordings', $features));
}
return $enabledfeatures;
}
}
@@ -0,0 +1,40 @@
<?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\exceptions;
use mod_bigbluebuttonbn\plugin;
/**
* Class bigbluebutton_exception generic exception. This is supposed to be recoverable.
*
* @package mod_bigbluebuttonbn
* @copyright 2010 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)
*/
class bigbluebutton_exception extends \moodle_exception {
/**
* Constructor
*
* @param string $errorcode The name of the string from error.php to print
* @param mixed $additionalinfo Extra words and phrases that might be required in the error string
*/
public function __construct($errorcode, $additionalinfo = null) {
parent::__construct($errorcode, plugin::COMPONENT, '', $additionalinfo);
}
}
@@ -0,0 +1,38 @@
<?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\exceptions;
use mod_bigbluebuttonbn\plugin;
/**
* The mod_bigbluebuttonbn cannot join meeting exception.
*
* @package mod_bigbluebuttonbn
* @copyright 2010 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)
*/
class meeting_join_exception extends \moodle_exception {
/**
* Constructor
*
* @param string $errorcode The name of the string from error.php to print
*/
public function __construct($errorcode) {
parent::__construct($errorcode, plugin::COMPONENT);
}
}
@@ -0,0 +1,55 @@
<?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\exceptions;
/**
* Class server_not_available_exception
*
* This kind of error cannot be recovered and should be displayed to the user
* signaling that there is an error in the configuration.
*
* @package mod_bigbluebuttonbn
* @copyright 2010 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)
*/
class server_not_available_exception extends \moodle_exception {
/**
* Constructor
*
* @param string $errorcode The name of the string from error.php to print
* @param string $module name of module
* @param string $link The url where the user will be prompted to continue. If no url is provided the user will be directed to
* the site index page.
* @param mixed $a Extra words and phrases that might be required in the error string
* @param string $debuginfo optional debugging information
*/
public function __construct($errorcode, $module = '', $link = '', $a = null, $debuginfo = null) {
global $CFG;
$hasdebugdeveloper = (
isset($CFG->debugdisplay) &&
isset($CFG->debug) &&
$CFG->debugdisplay &&
$CFG->debug === DEBUG_DEVELOPER
);
if ($hasdebugdeveloper && is_null($debuginfo)) {
$debuginfo = $this->getTraceAsString();
}
parent::__construct($errorcode, $module, $link, $a, $debuginfo);
}
}
@@ -0,0 +1,43 @@
<?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\extension;
/**
* A single action class to mutate the action URL.
*
* @package mod_bigbluebuttonbn
* @copyright 2023 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Laurent David (laurent@call-learning.fr)
*/
class action_url_addons {
/**
* Mutate the action URL.
*
* By design:
* 1. we should only add parameters
* 2. we cannot count on the order the subplugins are called
*
* @param string $action
* @param array $data
* @param array $metadata
* @return array associative array with the additional data and metadata (indexed by 'data' and
* 'metadata' keys).
*/
public function execute(string $action = '', array $data = [], array $metadata = []): array {
return ['data' => [], 'metadata' => []];
}
}
@@ -0,0 +1,79 @@
<?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\extension;
use cm_info;
/**
* A class to deal with completion rules addons in a subplugin
*
* @package mod_bigbluebuttonbn
* @copyright 2023 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Laurent David (laurent@call-learning.fr)
*/
abstract class custom_completion_addons {
/** @var cm_info The course module information object. */
protected $cm;
/** @var int The user's ID. */
protected $userid;
/** @var array The current state of core completion */
protected $completionstate;
/**
* activity_custom_completion constructor.
*
* @param cm_info $cm
* @param int $userid
* @param array|null $completionstate The current state of the core completion criteria
*/
public function __construct(cm_info $cm, int $userid, ?array $completionstate = null) {
$this->cm = $cm;
$this->userid = $userid;
$this->completionstate = $completionstate;
}
/**
* Fetches the completion state for a given completion rule.
*
* @param string $rule The completion rule.
* @return int The completion state.
*/
abstract public function get_state(string $rule): int;
/**
* Fetch the list of custom completion rules that this module defines.
*
* @return array
*/
abstract public static function get_defined_custom_rules(): array;
/**
* Returns an associative array of the descriptions of custom completion rules.
*
* @return array
*/
abstract public function get_custom_rule_descriptions(): array;
/**
* Returns an array of all completion rules, in the order they should be displayed to users.
*
* @return array
*/
abstract public function get_sort_order(): array;
}
@@ -0,0 +1,113 @@
<?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\extension;
use stdClass;
/**
* A class for the main mod form extension
*
* @package mod_bigbluebuttonbn
* @copyright 2023 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Laurent David (laurent@call-learning.fr)
*/
abstract class mod_form_addons {
/**
* @var \MoodleQuickForm|null moodle form
*/
protected $mform = null;
/**
* @var stdClass|null $bigbluebuttonbndata BigBlueButton data if any
*/
protected $bigbluebuttonbndata = null;
/**
* @var string|null $suffix suffix for form elements
*/
protected $suffix = null;
/**
* Constructor
*
* @param \MoodleQuickForm $mform
* @param stdClass|null $bigbluebuttonbndata
* @param string|null $suffix
*/
public function __construct(\MoodleQuickForm &$mform, ?stdClass $bigbluebuttonbndata = null, string $suffix = null) {
$this->mform = $mform;
$this->bigbluebuttonbndata = $bigbluebuttonbndata;
$this->suffix = $suffix;
}
/**
* Add new form field definition
*/
abstract public function add_fields(): void;
/**
* Validate form and returns an array of errors indexed by field name
*
* @param array $data
* @param array $files
* @return array
*/
abstract public function validation(array $data, array $files): array;
/**
* Allows modules to modify the data returned by form get_data().
* This method is also called in the bulk activity completion form.
*
* Only available on moodleform_mod.
*
* @param stdClass $data passed by reference
*/
abstract public function data_postprocessing(\stdClass &$data): void;
/**
* Can be overridden to add custom completion rules if the module wishes
* them. If overriding this, you should also override completion_rule_enabled.
* <p>
* Just add elements to the form as needed and return the list of IDs. The
* system will call disabledIf and handle other behaviour for each returned
* ID.
*
* @return string[] Array of string IDs of added items, empty array if none
*/
abstract public function add_completion_rules(): array;
/**
* Called during validation. Override to indicate, based on the data, whether
* a custom completion rule is enabled (selected).
*
* @param array $data Input data (not yet validated)
* @return bool True if one or more rules is enabled, false if none are;
* default returns false
*/
public function completion_rule_enabled(array $data): bool {
return false;
}
/**
* Form adjustments after setting data
*
* @return void
*/
public function definition_after_data() {
// Nothing for now.
}
}
@@ -0,0 +1,60 @@
<?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\extension;
use stdClass;
/**
* Class defining a way to deal with instance save/update/delete in extension
*
* @package mod_bigbluebuttonbn
* @copyright 2023 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Laurent David (laurent@call-learning.fr)
*/
class mod_instance_helper {
/**
* Runs any processes that must run before a bigbluebuttonbn insert/update.
*
* @param stdClass $bigbluebuttonbn BigBlueButtonBN form data
*/
public function add_instance(stdClass $bigbluebuttonbn) {
// Nothing for now.
}
/**
* Runs any processes that must be run after a bigbluebuttonbn insert/update.
*
* @param stdClass $bigbluebuttonbn BigBlueButtonBN form data
*/
public function update_instance(stdClass $bigbluebuttonbn): void {
// Nothing for now.
}
/**
* Runs any processes that must be run after a bigbluebuttonbn delete.
*
* @param int $cmid
*/
public function delete_instance(int $cmid): void {
}
/**
* Get any join table name that is used to store additional data for the instance.
* @return string[]
*/
public function get_join_tables(): array {
return [];
}
}
@@ -0,0 +1,324 @@
<?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/>.
/**
* The mod_bigbluebuttonbn files helper
*
* @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)
*/
namespace mod_bigbluebuttonbn\local\helpers;
use cache;
use cache_store;
use context;
use context_module;
use context_system;
use mod_bigbluebuttonbn\instance;
use moodle_url;
use stdClass;
/**
* Utility class for all files routines helper
*
* @package mod_bigbluebuttonbn
* @copyright 2021 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class files {
/**
* Helper for validating pluginfile.
*
* @param stdClass $context context object
* @param string $filearea file area
*
* @return bool|null false if file not valid
*/
public static function pluginfile_valid(stdClass $context, string $filearea): ?bool {
// Can be in context module or in context_system (if is the presentation by default).
if (!in_array($context->contextlevel, [CONTEXT_MODULE, CONTEXT_SYSTEM])) {
return false;
}
if (!array_key_exists($filearea, self::get_file_areas())) {
return false;
}
return true;
}
/**
* Helper for getting pluginfile.
*
* @param stdClass|null $course course object
* @param stdClass|null $cm course module object
* @param context $context context object
* @param string $filearea file area
* @param array $args extra arguments
*
* @return \stored_file|bool
*/
public static function pluginfile_file(?stdClass $course, ?stdClass $cm, context $context, string $filearea, array $args) {
$filename = self::get_plugin_filename($course, $cm, $context, $args);
if (!$filename) {
return false;
}
$fullpath = "/$context->id/mod_bigbluebuttonbn/$filearea/0/" . $filename;
$fs = get_file_storage();
$file = $fs->get_file_by_hash(sha1($fullpath));
if (!$file || $file->is_directory()) {
return false;
}
return $file;
}
/**
* Get a full path to the file attached as a preuploaded presentation
* or if there is none, set the presentation field will be set to blank.
*
* @param stdClass $bigbluebuttonformdata BigBlueButtonBN form data
* Note that $bigbluebuttonformdata->presentation is the id of the filearea whereas the bbb instance table
* stores the file name/path
* @return string
*/
public static function save_media_file(stdClass &$bigbluebuttonformdata): string {
if (!isset($bigbluebuttonformdata->presentation) || $bigbluebuttonformdata->presentation == '') {
return '';
}
$context = context_module::instance($bigbluebuttonformdata->coursemodule);
// Set the filestorage object.
$fs = get_file_storage();
// Save the file if it exists that is currently in the draft area.
file_save_draft_area_files($bigbluebuttonformdata->presentation, $context->id, 'mod_bigbluebuttonbn', 'presentation', 0);
// Get the file if it exists.
$files = $fs->get_area_files(
$context->id,
'mod_bigbluebuttonbn',
'presentation',
0,
'itemid, filepath, filename',
false
);
// Check that there is a file to process.
$filesrc = '';
if (count($files) == 1) {
// Get the first (and only) file.
$file = reset($files);
$filesrc = '/' . $file->get_filename();
}
return $filesrc;
}
/**
* Helper return array containing the file descriptor for a preuploaded presentation.
*
* @param context $context
* @param string $presentation matching presentation file name
* @param int $id bigbluebutton instance id
* @param bool $withnonce add nonce to the url
* @return array|null the representation of the presentation as an associative array
*/
public static function get_presentation(context $context, string $presentation, $id = null, $withnonce = false): ?array {
global $CFG;
$fs = get_file_storage();
$files = [];
$defaultpresentation = $fs->get_area_files(
context_system::instance()->id,
'mod_bigbluebuttonbn',
'presentationdefault',
0,
"filename",
false
);
$activitypresentation = $files = $fs->get_area_files(
$context->id,
'mod_bigbluebuttonbn',
'presentation',
false,
'itemid, filepath, filename',
false
);
// Presentation upload logic based on config settings.
if (empty($defaultpresentation)) {
if (empty($activitypresentation) || !\mod_bigbluebuttonbn\local\config::get('preuploadpresentation_editable')) {
return null;
}
$files = $activitypresentation;
} else {
if (empty($activitypresentation) || !\mod_bigbluebuttonbn\local\config::get('preuploadpresentation_editable')) {
$files = $defaultpresentation;
$id = null;
} else {
$files = $activitypresentation;
}
}
$pnoncevalue = 0;
if ($withnonce) {
$nonceid = 0;
if (!is_null($id)) {
$instance = instance::get_from_instanceid($id);
$nonceid = $instance->get_instance_id();
}
$pnoncevalue = self::generate_nonce($nonceid);
}
$file = null;
foreach ($files as $f) {
if (basename($f->get_filename()) == basename($presentation)) {
$file = $f;
}
}
if (!$file && !empty($files)) {
$file = reset($files);
}
if (empty($file)) {
return null; // File was not found.
}
// Note: $pnoncevalue is an int.
$url = moodle_url::make_pluginfile_url(
$file->get_contextid(),
$file->get_component(),
$file->get_filearea(),
$withnonce ? $pnoncevalue : null, // Hack: item id as a nonce.
$file->get_filepath(),
$file->get_filename()
);
return [
'icondesc' => get_mimetype_description($file),
'iconname' => file_file_icon($file),
'name' => $file->get_filename(),
'url' => $url->out(false),
];
}
/**
* Helper for getting pluginfile name.
*
* @param stdClass|null $course course object
* @param stdClass|null $cm course module object
* @param context $context context object
* @param array $args extra arguments
*
* @return string|null
*/
public static function get_plugin_filename(?stdClass $course, ?stdClass $cm, context $context, array $args): ?string {
global $DB;
if ($context->contextlevel != CONTEXT_SYSTEM) {
// Plugin has a file to use as default in general setting.
// The difference with the standard bigbluebuttonbn_pluginfile_filename() are.
// - Context is system, so we don't need to check the cmid in this case.
// - The area is "presentationdefault_cache".
if (!$DB->get_record('bigbluebuttonbn', ['id' => $cm->instance])) {
return null;
}
}
// Plugin has a file to use as default in general setting.
// The difference with the standard bigbluebuttonbn_pluginfile_filename() are.
// - Context is system, so we don't need to check the cmid in this case.
// - The area is "presentationdefault_cache".
if (count($args) > 1) {
$id = 0;
if ($cm) {
$instance = instance::get_from_cmid($cm->id);
$id = $instance->get_instance_id();
}
$actualnonce = self::get_nonce($id);
return ($args['0'] == $actualnonce) ? $args['1'] : null;
}
if (!empty($course)) {
require_course_login($course, true, $cm, true, true);
} else {
require_login(null, true, $cm, true, true);
}
if (!has_capability('mod/bigbluebuttonbn:join', $context)) {
return null;
}
return implode('/', $args);
}
/**
* Helper generates a salt used for the preuploaded presentation callback url.
*
* @param int $id
* @return int
*/
protected static function get_nonce(int $id): int {
$cache = static::get_nonce_cache();
$pnoncekey = sha1($id);
$existingnoncedata = $cache->get($pnoncekey);
if ($existingnoncedata) {
if ($existingnoncedata->counter > 0) {
$existingnoncedata->counter--;
$cache->set($pnoncekey, $existingnoncedata);
return $existingnoncedata->nonce;
}
}
// The item id was adapted for granting public access to the presentation once in order to allow BigBlueButton to gather
// the file once.
return static::generate_nonce($id);
}
/**
* Generate a nonce and store it in the cache
*
* @param int $id
* @return int
*/
protected static function generate_nonce($id): int {
$cache = static::get_nonce_cache();
$pnoncekey = sha1($id);
// The item id was adapted for granting public access to the presentation once in order to allow BigBlueButton to gather
// the file once.
$pnoncevalue = ((int) microtime()) + mt_rand();
$cache->set($pnoncekey, (object) ['nonce' => $pnoncevalue, 'counter' => 2]);
return $pnoncevalue;
}
/**
* Get cache for nonce
*
* @return \cache_application|\cache_session|cache_store
*/
private static function get_nonce_cache() {
return cache::make_from_params(
cache_store::MODE_APPLICATION,
'mod_bigbluebuttonbn',
'presentation_cache'
);
}
/**
* Returns an array of file areas.
*
* @return array a list of available file areas
*
*/
protected static function get_file_areas(): array {
$areas = [];
$areas['presentation'] = get_string('mod_form_block_presentation', 'bigbluebuttonbn');
$areas['presentationdefault'] = get_string('mod_form_block_presentation_default', 'bigbluebuttonbn');
return $areas;
}
}
@@ -0,0 +1,209 @@
<?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\helpers;
use calendar_event;
use mod_bigbluebuttonbn\instance;
use mod_bigbluebuttonbn\logger;
use mod_bigbluebuttonbn\plugin;
use stdClass;
/**
* Utility class for all instance (module) routines helper.
*
* @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)
*/
class mod_helper {
/**
* Runs any processes that must run before a bigbluebuttonbn insert/update.
*
* @param stdClass $bigbluebuttonbn BigBlueButtonBN form data
**/
public static function process_pre_save(stdClass $bigbluebuttonbn) {
self::process_pre_save_instance($bigbluebuttonbn);
self::process_pre_save_checkboxes($bigbluebuttonbn);
self::process_pre_save_common($bigbluebuttonbn);
$bigbluebuttonbn->participants = htmlspecialchars_decode($bigbluebuttonbn->participants, ENT_COMPAT);
}
/**
* Runs process for defining the instance (insert/update).
*
* @param stdClass $bigbluebuttonbn BigBlueButtonBN form data
**/
protected static function process_pre_save_instance(stdClass $bigbluebuttonbn): void {
$bigbluebuttonbn->timemodified = time();
if ((integer) $bigbluebuttonbn->instance == 0) {
$bigbluebuttonbn->meetingid = 0;
$bigbluebuttonbn->timecreated = time();
$bigbluebuttonbn->timemodified = 0;
// As it is a new activity, assign passwords.
$bigbluebuttonbn->moderatorpass = plugin::random_password(12);
$bigbluebuttonbn->viewerpass = plugin::random_password(12, $bigbluebuttonbn->moderatorpass);
}
}
/**
* Runs process for assigning default value to checkboxes.
*
* @param stdClass $bigbluebuttonbn BigBlueButtonBN form data
**/
protected static function process_pre_save_checkboxes($bigbluebuttonbn) {
if (!isset($bigbluebuttonbn->wait)) {
$bigbluebuttonbn->wait = 0;
}
if (!isset($bigbluebuttonbn->record)) {
$bigbluebuttonbn->record = 0;
}
if (!isset($bigbluebuttonbn->recordallfromstart)) {
$bigbluebuttonbn->recordallfromstart = 0;
}
if (!isset($bigbluebuttonbn->recordhidebutton)) {
$bigbluebuttonbn->recordhidebutton = 0;
}
if (!isset($bigbluebuttonbn->recordings_html)) {
$bigbluebuttonbn->recordings_html = 0;
}
if (!isset($bigbluebuttonbn->recordings_deleted)) {
$bigbluebuttonbn->recordings_deleted = 0;
}
if (!isset($bigbluebuttonbn->recordings_imported)) {
$bigbluebuttonbn->recordings_imported = 0;
}
if (!isset($bigbluebuttonbn->recordings_preview)) {
$bigbluebuttonbn->recordings_preview = 0;
}
if (!isset($bigbluebuttonbn->muteonstart)) {
$bigbluebuttonbn->muteonstart = 0;
}
if (!isset($bigbluebuttonbn->disablecam)) {
$bigbluebuttonbn->disablecam = 0;
}
if (!isset($bigbluebuttonbn->disablemic)) {
$bigbluebuttonbn->disablemic = 0;
}
if (!isset($bigbluebuttonbn->disableprivatechat)) {
$bigbluebuttonbn->disableprivatechat = 0;
}
if (!isset($bigbluebuttonbn->disablepublicchat)) {
$bigbluebuttonbn->disablepublicchat = 0;
}
if (!isset($bigbluebuttonbn->disablenote)) {
$bigbluebuttonbn->disablenote = 0;
}
if (!isset($bigbluebuttonbn->hideuserlist)) {
$bigbluebuttonbn->hideuserlist = 0;
}
}
/**
* Runs process for wipping common settings when 'recordings only'.
*
* @param stdClass $bigbluebuttonbn BigBlueButtonBN form data
**/
protected static function process_pre_save_common(stdClass $bigbluebuttonbn): void {
// Make sure common settings are removed when 'recordings only'.
if ($bigbluebuttonbn->type == instance::TYPE_RECORDING_ONLY) {
$bigbluebuttonbn->groupmode = 0;
$bigbluebuttonbn->groupingid = 0;
}
}
/**
* Runs any processes that must be run after a bigbluebuttonbn insert/update.
*
* @param stdClass $bigbluebuttonbn BigBlueButtonBN form data
**/
public static function process_post_save(stdClass $bigbluebuttonbn): void {
self::process_post_save_event($bigbluebuttonbn);
self::process_post_save_completion($bigbluebuttonbn);
}
/**
* Generates an event after a bigbluebuttonbn insert/update.
*
* @param stdClass $bigbluebuttonbn BigBlueButtonBN form data
**/
protected static function process_post_save_event(stdClass $bigbluebuttonbn): void {
global $CFG, $DB;
require_once($CFG->dirroot . '/calendar/lib.php');
$eventid = $DB->get_field('event', 'id', [
'modulename' => 'bigbluebuttonbn',
'instance' => $bigbluebuttonbn->id,
'eventtype' => logger::EVENT_MEETING_START
]);
// Delete the event from calendar when/if openingtime is NOT set.
if (!isset($bigbluebuttonbn->openingtime) || !$bigbluebuttonbn->openingtime) {
if ($eventid) {
$calendarevent = calendar_event::load($eventid);
$calendarevent->delete();
}
return;
}
// Add event to the calendar as openingtime is set.
$event = (object) [
'eventtype' => logger::EVENT_MEETING_START,
'type' => CALENDAR_EVENT_TYPE_ACTION,
'name' => get_string('calendarstarts', 'bigbluebuttonbn', $bigbluebuttonbn->name),
'description' => format_module_intro('bigbluebuttonbn', $bigbluebuttonbn, $bigbluebuttonbn->coursemodule, false),
'format' => FORMAT_HTML,
'courseid' => $bigbluebuttonbn->course,
'groupid' => 0,
'userid' => 0,
'modulename' => 'bigbluebuttonbn',
'instance' => $bigbluebuttonbn->id,
'timestart' => $bigbluebuttonbn->openingtime,
'timeduration' => 0,
'timesort' => $bigbluebuttonbn->openingtime,
'visible' => instance_is_visible('bigbluebuttonbn', $bigbluebuttonbn),
'priority' => null,
];
// Update the event in calendar when/if eventid was found.
if ($eventid) {
$event->id = $eventid;
$calendarevent = calendar_event::load($eventid);
$calendarevent->update($event);
return;
}
calendar_event::create($event);
}
/**
* Generates an event after a bigbluebuttonbn activity is completed.
*
* @param stdClass $bigbluebuttonbn BigBlueButtonBN form data
**/
protected static function process_post_save_completion(stdClass $bigbluebuttonbn): void {
if (empty($bigbluebuttonbn->completionexpected)) {
return;
}
\core_completion\api::update_completion_date_event(
$bigbluebuttonbn->coursemodule,
'bigbluebuttonbn',
$bigbluebuttonbn->id,
$bigbluebuttonbn->completionexpected
);
}
}
@@ -0,0 +1,131 @@
<?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/>.
/**
* The mod_bigbluebuttonbn resetting instance helper
*
* @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)
*/
namespace mod_bigbluebuttonbn\local\helpers;
use context_module;
use core_tag_tag;
use mod_bigbluebuttonbn\local\config;
use mod_bigbluebuttonbn\recording;
/**
* Utility class for resetting instance routines helper
*
* @package mod_bigbluebuttonbn
* @copyright 2021 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class reset {
/**
* Used by the reset_course_userdata for deleting recordings
*
* This will delete recordings in the database and not in the remote BBB server.
*
* @param int $courseid
*/
public static function reset_recordings(int $courseid): void {
// Criteria for search : courseid or bigbluebuttonbn=null or subset=false or includedeleted=true.
$recordings = recording::get_recordings_for_course(
$courseid,
[], // Exclude itself.
false,
true
);
if ($recordings) {
// Remove all the recordings.
foreach ($recordings as $recording) {
$recording->delete();
}
}
}
/**
* Used by the reset_course_userdata for deleting tags linked to bigbluebuttonbn instances in the course.
*
* @param int $courseid
*/
public static function reset_tags(int $courseid): void {
global $DB;
// Remove all the tags linked to the room/activities in this course.
if ($bigbluebuttonbns = $DB->get_records('bigbluebuttonbn', ['course' => $courseid])) {
foreach ($bigbluebuttonbns as $bigbluebuttonbn) {
if (!$cm = get_coursemodule_from_instance('bigbluebuttonbn', $bigbluebuttonbn->id, $courseid)) {
continue;
}
$context = context_module::instance($cm->id);
core_tag_tag::delete_instances('mod_bigbluebuttonbn', null, $context->id);
}
}
}
/**
* Used by the reset_course_userdata for deleting events linked to bigbluebuttonbn instances in the course.
*
* @param string $courseid
* @return bool status
*/
public static function reset_events($courseid) {
global $DB;
// Remove all the events.
return $DB->delete_records('event', ['modulename' => 'bigbluebuttonbn', 'courseid' => $courseid]);
}
/**
* Returns status used on every defined reset action.
*
* @param string $item
* @return array status array
*/
public static function reset_getstatus(string $item): array {
return ['component' => get_string('modulenameplural', 'bigbluebuttonbn'),
'item' => get_string("removed{$item}", 'bigbluebuttonbn'),
'error' => false];
}
/**
* Define items to be reset by course/reset.php
*
* @return array
*/
public static function reset_course_items(): array {
$items = ["events" => 0, "tags" => 0, "logs" => 0];
// Include recordings only if enabled.
if ((boolean) config::recordings_enabled()) {
$items["recordings"] = 0;
}
return $items;
}
/**
* Reset logs for each BBB instance of this course
*
* @param int $courseid
* @return bool status
*/
public static function reset_logs(int $courseid) {
global $DB;
return $DB->delete_records('bigbluebuttonbn_logs', ['courseid' => $courseid]);
}
}
@@ -0,0 +1,452 @@
<?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/>.
/**
* The mod_bigbluebuttonbn roles helper
*
* @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)
*/
namespace mod_bigbluebuttonbn\local\helpers;
use cache;
use cache_store;
use context;
use context_course;
use mod_bigbluebuttonbn\instance;
use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy;
use stdClass;
/**
* Utility class for all roles routines helper
*
* @package mod_bigbluebuttonbn
* @copyright 2021 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class roles {
/** @var int The bigbluebutton viewer role */
public const ROLE_VIEWER = 'viewer';
/** @var string The bigbluebutton moderator role */
public const ROLE_MODERATOR = 'moderator';
/**
* Returns user roles in a context.
*
* @param context $context
* @param int $userid
*
* @return array $userroles
*/
public static function get_user_roles(context $context, int $userid) {
global $DB;
$userroles = get_user_roles($context, $userid);
if ($userroles) {
$where = '';
foreach ($userroles as $userrole) {
$where .= (empty($where) ? ' WHERE' : ' OR') . ' id=' . $userrole->roleid;
}
$userroles = $DB->get_records_sql('SELECT * FROM {role}' . $where);
}
return $userroles;
}
/**
* Returns guest role wrapped in an array.
*
* @return array
*/
protected static function get_guest_role() {
$guestrole = get_guest_role();
return [$guestrole->id => $guestrole];
}
/**
* Returns an array containing all the users in a context wrapped for html select element.
*
* @param context_course $context
* @param null $bbactivity
* @return array $users
*/
public static function get_users_array(context_course $context, $bbactivity = null) {
// CONTRIB-7972, check the group of current user and course group mode.
$groups = null;
$users = (array) get_enrolled_users($context, '', 0, 'u.*', null, 0, 0, true);
$course = get_course($context->instanceid);
$groupmode = groups_get_course_groupmode($course);
if ($bbactivity) {
list($bbcourse, $cm) = get_course_and_cm_from_instance($bbactivity->id, 'bigbluebuttonbn');
$groupmode = groups_get_activity_groupmode($cm);
}
if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) {
global $USER;
$groups = groups_get_all_groups($course->id, $USER->id);
$users = [];
foreach ($groups as $g) {
$users += (array) get_enrolled_users($context, '', $g->id, 'u.*', null, 0, 0, true);
}
}
return array_map(
function($u) {
return ['id' => $u->id, 'name' => fullname($u)];
},
$users);
}
/**
* Can do some administration in this course, likely manage recordings
*
* @param int $courseid
* @param string $capability
*/
public static function has_capability_in_course(int $courseid, string $capability) {
global $DB;
if (empty($courseid) || !$DB->record_exists('course', ['id' => $courseid])) {
return has_capability('moodle/site:config', \context_system::instance());
}
$coursecontext = context_course::instance($courseid);
return has_capability($capability, $coursecontext);
}
/**
* Returns an array containing all the roles in a context.
*
* @param context|null $context $context
* @param bool|null $onlyviewableroles
*
* @return array $roles
*/
public static function get_roles(?context $context = null, ?bool $onlyviewableroles = true) {
global $CFG;
if ($onlyviewableroles == true && $CFG->branch >= 35) {
$roles = (array) get_viewable_roles($context);
foreach ($roles as $key => $value) {
$roles[$key] = $value;
}
} else {
$roles = (array) role_get_names($context);
foreach ($roles as $key => $value) {
$roles[$key] = $value->localname;
}
}
return $roles;
}
/**
* Returns an array containing all the roles in a context wrapped for html select element.
*
* @param context|null $context $context
* @param bool $onlyviewableroles
*
* @return array $users
*/
protected static function get_roles_select(context $context = null, bool $onlyviewableroles = true) {
global $CFG;
if ($onlyviewableroles == true && $CFG->branch >= 35) {
$roles = (array) get_viewable_roles($context);
foreach ($roles as $key => $value) {
$roles[$key] = ['id' => $key, 'name' => $value];
}
} else {
$roles = (array) role_get_names($context);
foreach ($roles as $key => $value) {
$roles[$key] = ['id' => $value->id, 'name' => $value->localname];
}
}
return $roles;
}
/**
* Returns role that corresponds to an id.
*
* @param string|integer $id
*
* @return stdClass|null $role
*/
protected static function get_role($id): ?stdClass {
$roles = (array) role_get_names();
if (is_numeric($id) && isset($roles[$id])) {
return (object) $roles[$id];
}
foreach ($roles as $role) {
if ($role->shortname == $id) {
return $role;
}
}
return null;
}
/**
* Returns an array to populate a list of participants used in mod_form.js.
*
* @param context $context
* @param null|stdClass $bbactivity
* @return array $data
*/
public static function get_participant_data(context $context, ?stdClass $bbactivity = null) {
$data = [
'all' => [
'name' => get_string('mod_form_field_participant_list_type_all', 'bigbluebuttonbn'),
'children' => []
],
];
$data['role'] = [
'name' => get_string('mod_form_field_participant_list_type_role', 'bigbluebuttonbn'),
'children' => self::get_roles_select($context, true)
];
$data['user'] = [
'name' => get_string('mod_form_field_participant_list_type_user', 'bigbluebuttonbn'),
'children' => self::get_users_array($context, $bbactivity),
];
return $data;
}
/**
* Returns an array to populate a list of participants used in mod_form.php.
*
* @param stdClass|null $bigbluebuttonbn
* @param context $context
*
* @return array
*/
public static function get_participant_list(?stdClass $bigbluebuttonbn, context $context): array {
global $USER;
if ($bigbluebuttonbn == null) {
return self::get_participant_rules_encoded(
self::get_participant_list_default($context, $USER->id)
);
}
if (empty($bigbluebuttonbn->participants)) {
$bigbluebuttonbn->participants = "[]";
}
$rules = json_decode($bigbluebuttonbn->participants, true);
if (empty($rules)) {
$rules = self::get_participant_list_default($context,
bigbluebutton_proxy::get_instance_ownerid($bigbluebuttonbn));
}
return self::get_participant_rules_encoded($rules);
}
/**
* Returns an array to populate a list of participants used in mod_form.php with default values.
*
* @param context $context
* @param int|null $ownerid
*
* @return array
*/
protected static function get_participant_list_default(context $context, ?int $ownerid = null) {
$participantlist = [];
$participantlist[] = [
'selectiontype' => 'all',
'selectionid' => 'all',
'role' => self::ROLE_VIEWER,
];
$defaultrules = explode(',', \mod_bigbluebuttonbn\local\config::get('participant_moderator_default'));
foreach ($defaultrules as $defaultrule) {
if ($defaultrule == '0') {
if (!empty($ownerid) && is_enrolled($context, $ownerid)) {
$participantlist[] = [
'selectiontype' => 'user',
'selectionid' => (string) $ownerid,
'role' => self::ROLE_MODERATOR];
}
continue;
}
$participantlist[] = [
'selectiontype' => 'role',
'selectionid' => $defaultrule,
'role' => self::ROLE_MODERATOR];
}
return $participantlist;
}
/**
* Returns an array to populate a list of participants used in mod_form.php with bigbluebuttonbn values.
*
* @param array $rules
*
* @return array
*/
protected static function get_participant_rules_encoded(array $rules): array {
foreach ($rules as $key => $rule) {
if ($rule['selectiontype'] !== 'role' || is_numeric($rule['selectionid'])) {
continue;
}
$role = self::get_role($rule['selectionid']);
if ($role == null) {
unset($rules[$key]);
continue;
}
$rule['selectionid'] = $role->id;
$rules[$key] = $rule;
}
return $rules;
}
/**
* Returns an array to populate a list of participant_selection used in mod_form.php.
*
* @return array
*/
public static function get_participant_selection_data(): array {
return [
'type_options' => [
'all' => get_string('mod_form_field_participant_list_type_all', 'bigbluebuttonbn'),
'role' => get_string('mod_form_field_participant_list_type_role', 'bigbluebuttonbn'),
'user' => get_string('mod_form_field_participant_list_type_user', 'bigbluebuttonbn'),
],
'type_selected' => 'all',
'options' => ['all' => '---------------'],
'selected' => 'all',
];
}
/**
* Evaluate if a user in a context is moderator based on roles and participation rules.
*
* @param context $context
* @param array $participantlist
* @param int $userid
*
* @return bool
*/
public static function is_moderator(context $context, array $participantlist, ?int $userid = null): bool {
global $USER;
// If an admin, then also a moderator.
if (has_capability('moodle/site:config', $context)) {
return true;
}
if (!is_array($participantlist)) {
return false;
}
if (empty($userid)) {
$userid = $USER->id;
}
$userroles = self::get_guest_role();
if (!isguestuser()) {
$userroles = self::get_user_roles($context, $userid);
}
return self::is_moderator_validator($participantlist, $userid, $userroles);
}
/**
* Iterates participant list rules to evaluate if a user is moderator.
*
* @param array $participantlist
* @param int $userid
* @param array $userroles
*
* @return bool
*/
protected static function is_moderator_validator(array $participantlist, int $userid, array $userroles): bool {
// Iterate participant rules.
foreach ($participantlist as $participant) {
if (self::is_moderator_validate_rule($participant, $userid, $userroles)) {
return true;
}
}
return false;
}
/**
* Evaluate if a user is moderator based on roles and a particular participation rule.
*
* @param array $participant
* @param int $userid
* @param array $userroles
*
* @return bool
*/
protected static function is_moderator_validate_rule(array $participant, int $userid, array $userroles): bool {
if ($participant['role'] == self::ROLE_VIEWER) {
return false;
}
// Validation for the 'all' rule.
if ($participant['selectiontype'] == 'all') {
return true;
}
// Validation for a 'user' rule.
if ($participant['selectiontype'] == 'user') {
if ($participant['selectionid'] == $userid) {
return true;
}
return false;
}
// Validation for a 'role' rule.
$role = self::get_role($participant['selectionid']);
if ($role != null && array_key_exists($role->id, $userroles)) {
return true;
}
return false;
}
/**
* Updates the meeting info cached object when a participant has joined.
*
* @param string $meetingid
* @param bool $ismoderator
*
* @return void
*/
public static function participant_joined(string $meetingid, bool $ismoderator): void {
$cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', 'meetings_cache');
$result = $cache->get($meetingid);
$meetinginfo = json_decode($result['meeting_info']);
$meetinginfo->participantCount += 1;
if ($ismoderator) {
$meetinginfo->moderatorCount += 1;
}
$cache->set($meetingid, ['creation_time' => $result['creation_time'],
'meeting_info' => json_encode($meetinginfo)]);
}
/**
* Helper function returns a list of courses a user has access to, wrapped in an array that can be used
* by a html select.
*
* @param instance $instance
* @return array
*/
public static function import_get_courses_for_select(instance $instance): array {
if ($instance->is_admin()) {
$courses = get_courses('all', 'c.fullname ASC');
// It includes the name of the site as a course (category 0), so remove the first one.
unset($courses['1']);
} else {
$courses = enrol_get_users_courses($instance->get_user_id(), false, 'id,shortname,fullname');
}
$courses = array_filter($courses, function($course) {
$modules = get_fast_modinfo($course->id);
return !empty($modules->instances['bigbluebuttonbn']);
});
$coursesforselect = [];
foreach ($courses as $course) {
$coursesforselect[$course->id] = $course->fullname . " (" . $course->shortname . ")";
}
return $coursesforselect;
}
}
@@ -0,0 +1,73 @@
<?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\helpers;
use cm_info;
use mod_bigbluebuttonbn\instance;
use mod_bigbluebuttonbn\logger;
use stdClass;
/**
* Utility class for all user information
*
* Used mainly in user_outline and user_complete
*
* @package mod_bigbluebuttonbn
* @copyright 2022 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)
*/
class user_info {
/**
* Event to watch for.
*/
const EVENT_TO_WATCH = [
'join' => logger::EVENT_JOIN,
'play_recording' => logger::EVENT_PLAYED
];
/**
* Get user outline and complete info
*
* @param stdClass $course
* @param stdClass $user
* @param cm_info $mod
* @return array[] an array of infos and timestamps (latest timestamp)
*/
public static function get_user_info_outline(stdClass $course, stdClass $user, cm_info $mod): array {
$completion = new \completion_info($course);
$cdata = $completion->get_data($mod, false, $user->id);
$logtimestamps = [];
$infos = [];
if (!empty($cdata->viewed) && $cdata->viewed) {
$infos[] = get_string('report_room_view', 'mod_bigbluebuttonbn');
$logtimestamps[] = $cdata->timemodified;
}
$instance = instance::get_from_cmid($mod->id);
foreach (self::EVENT_TO_WATCH as $eventtype => $logtype) {
$logs = logger::get_user_completion_logs($instance, $user->id, [$logtype]);
if ($logs) {
$infos[] = get_string("report_{$eventtype}_info", 'mod_bigbluebuttonbn', count($logs));
$latesttime = array_reduce($logs,
function($acc, $log) {
return ($acc > $log->timecreated) ? $acc : $log->timecreated;
}, 0);
$logtimestamps[] = $latesttime;
}
}
return [$infos, $logtimestamps];
}
}
@@ -0,0 +1,81 @@
<?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\plugins;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/adminlib.php');
use admin_externalpage;
use core_component;
use core_text;
use mod_bigbluebuttonbn\extension;
use moodle_url;
/**
* Admin external page that displays a list of the installed extension plugins.
*
* @package mod_bigbluebuttonbn
* @copyright 2023 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Laurent David (laurent@call-learning.fr)
*/
class admin_page_manage_extensions extends admin_externalpage {
/**
* Global URL for page.
*/
const ADMIN_PAGE_URL = '/mod/bigbluebuttonbn/adminmanageplugins.php';
/**
* The constructor - calls parent constructor
*
*/
public function __construct() {
$url = new moodle_url(self::ADMIN_PAGE_URL);
$managepagename = 'manage' . extension::BBB_EXTENSION_PLUGIN_NAME . 'plugins';
parent::__construct(
$managepagename,
get_string($managepagename, 'mod_bigbluebuttonbn'),
$url
);
}
/**
* Search plugins for the specified string
*
* @param string $query The string to search for
* @return array
*/
public function search($query): array {
if ($result = parent::search($query)) {
return $result;
}
foreach (core_component::get_plugin_list(extension::BBB_EXTENSION_PLUGIN_NAME ) as $name => $notused) {
$pluginname = core_text::strtolower(
get_string('pluginname', extension::BBB_EXTENSION_PLUGIN_NAME . '_' . $name)
);
if (str_contains($pluginname, $query) !== false) {
$result = (object)[
'page' => $this,
'settings' => [],
];
return [$this->name => $result];
}
}
return [];
}
}
@@ -0,0 +1,329 @@
<?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\plugins;
use cache_helper;
use context_system;
use core_component;
use core_plugin_manager;
use flexible_table;
use html_writer;
use mod_bigbluebuttonbn\extension;
use moodle_url;
use pix_icon;
/**
* Class that handles the display and configuration of the list of extension plugins.
*
* This is directly taken from the mod_assign code. We might need to have a global API there for this.
*
* @package mod_bigbluebuttonbn
* @copyright 2023 onwards, Blindside Networks Inc
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Laurent David (laurent@call-learning.fr)
*/
class admin_plugin_manager {
/** @var object the url of the manage submission plugin page */
private $pageurl;
/**
* Constructor for this assignment plugin manager
*
*/
public function __construct() {
$this->pageurl = new moodle_url(admin_page_manage_extensions::ADMIN_PAGE_URL);
}
/**
* This is the entry point for this controller class.
*
* @param string|null $action - The action to perform
* @param string|null $plugin - Optional name of a plugin type to perform the action on
* @return void
*/
public function execute(?string $action = null, ?string $plugin = null): void {
if (empty($action) || empty($plugin)) {
$action = 'view';
}
$this->check_permissions();
$actionname = "plugins_$action";
if (method_exists($this, $actionname)) {
$nextaction = $this->$actionname($plugin);
if ($nextaction) {
$this->execute($nextaction, $plugin);
}
}
}
/**
* Check this user has permission to edit the list of installed plugins
*
* @return void
*/
private function check_permissions(): void {
require_login();
$systemcontext = context_system::instance();
require_capability('moodle/site:config', $systemcontext);
}
/**
* Write the HTML for the submission plugins table.
*
* @return void
*/
private function plugins_view(): void {
global $OUTPUT, $CFG;
require_once($CFG->libdir . '/tablelib.php');
$this->print_header();
$table = new flexible_table(extension::BBB_EXTENSION_PLUGIN_NAME . 'pluginsadminttable');
$table->define_baseurl($this->pageurl);
$table->define_columns([
'pluginname',
'version',
'hideshow',
'order',
'settings',
'uninstall'
]);
$table->define_headers([
get_string('subplugintype_bbbext', 'mod_bigbluebuttonbn'),
get_string('version'), get_string('hide') . '/' . get_string('show'),
get_string('order'),
get_string('settings'),
get_string('uninstallplugin', 'core_admin')
]);
$table->set_attribute('id', extension::BBB_EXTENSION_PLUGIN_NAME . 'plugins');
$table->set_attribute('class', 'admintable generaltable');
$table->setup();
$plugins = $this->get_sorted_plugins_list();
$instances = core_plugin_manager::instance()->get_plugins_of_type(extension::BBB_EXTENSION_PLUGIN_NAME);
foreach ($plugins as $idx => $plugin) {
$componentname = extension::BBB_EXTENSION_PLUGIN_NAME . '_' . $plugin;
$typebasedir = "";
if (in_array($plugin, array_keys($instances))) {
$typebasedir = ($instances[$plugin])->typerootdir;
}
$row = [];
$class = '';
$pluginversion = get_config($componentname, 'version');
$row[] = get_string('pluginname', $componentname);
$row[] = $pluginversion;
$visible = !get_config($componentname, 'disabled');
if ($visible) {
$row[] = $this->format_icon_link('hide', $plugin, 't/hide', get_string('disable'));
} else {
$row[] = $this->format_icon_link('show', $plugin, 't/show', get_string('enable'));
$class = 'dimmed_text';
}
$movelinks = '';
if (!$idx == 0) {
$movelinks .= $this->format_icon_link('moveup', $plugin, 't/up', get_string('up')) . ' ';
} else {
$movelinks .= $OUTPUT->spacer(['width' => 16]);
}
if ($idx != count($plugins) - 1) {
$movelinks .= $this->format_icon_link('movedown', $plugin, 't/down', get_string('down')) . ' ';
}
$row[] = $movelinks;
$exists = file_exists($typebasedir . '/' . $plugin . '/settings.php');
// We do not display settings for plugin who have not yet been installed (so have no version yet).
if (!empty($pluginversion) && $exists) {
$row[] = html_writer::link(
new moodle_url('/admin/settings.php', ['section' => $componentname]),
get_string('settings')
);
} else {
$row[] = '&nbsp;';
}
$url = core_plugin_manager::instance()->get_uninstall_url(
$componentname,
'manage'
);
if ($url) {
$row[] = html_writer::link($url, get_string('uninstallplugin', 'core_admin'));
} else {
$row[] = '&nbsp;';
}
$table->add_data($row, $class);
}
$table->finish_output();
$this->print_footer();
}
/**
* Write the page header
*
* @return void
*/
private function print_header(): void {
global $OUTPUT;
$pageidentifier = 'manage' . extension::BBB_EXTENSION_PLUGIN_NAME . 'plugins';
admin_externalpage_setup($pageidentifier);
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string($pageidentifier, 'mod_bigbluebuttonbn'));
}
/**
* Return a list of plugins sorted by the order defined in the admin interface
*
* @return array The list of plugins
*/
public function get_sorted_plugins_list(): array {
$names = core_component::get_plugin_list(extension::BBB_EXTENSION_PLUGIN_NAME);
$result = [];
foreach ($names as $name => $path) {
$idx = get_config(extension::BBB_EXTENSION_PLUGIN_NAME . '_' . $name, 'sortorder');
if (!$idx) {
$idx = 0;
}
while (array_key_exists($idx, $result)) {
$idx += 1;
}
$result[$idx] = $name;
}
ksort($result);
return $result;
}
/**
* Util function for writing an action icon link
*
* @param string $action URL parameter to include in the link
* @param string $plugin URL parameter to include in the link
* @param string $icon The key to the icon to use (e.g. 't/up')
* @param string $alt The string description of the link used as the title and alt text
* @return string The icon/link
*/
private function format_icon_link(string $action, string $plugin, string $icon, string $alt): string {
global $OUTPUT;
return $OUTPUT->action_icon(
new moodle_url(
$this->pageurl,
['action' => $action, 'plugin' => $plugin, 'sesskey' => sesskey()]
),
new pix_icon($icon, $alt, 'moodle', ['title' => $alt]),
null,
['title' => $alt]
);
}
/**
* Write the page footer
*
* @return void
*/
private function print_footer(): void {
global $OUTPUT;
echo $OUTPUT->footer();
}
/**
* Hide this plugin.
*
* @param string $plugin - The plugin to hide
* @return string The next page to display
*/
private function plugins_hide(string $plugin): string {
$class = \core_plugin_manager::resolve_plugininfo_class(extension::BBB_EXTENSION_PLUGIN_NAME);
$class::enable_plugin($plugin, false);
cache_helper::purge_by_event('mod_bigbluebuttonbn/pluginenabledisabled');
// Also clear the cache for all BigBlueButtonModules.
rebuild_course_cache(0, true);
return 'view';
}
/**
* Show this plugin.
*
* @param string $plugin - The plugin to show
* @return string The next page to display
*/
private function plugins_show(string $plugin): string {
$class = \core_plugin_manager::resolve_plugininfo_class(extension::BBB_EXTENSION_PLUGIN_NAME);
$class::enable_plugin($plugin, true);
cache_helper::purge_by_event('mod_bigbluebuttonbn/pluginenabledisabled');
return 'view';
}
/**
* Move this plugin up
*
* We need this function so we can call directly (without the dir parameter)
* @param string $plugintomove - The plugin to move
* @return string The next page to display
*/
private function plugins_moveup(string $plugintomove): string {
return $this->move_plugin($plugintomove, 'up');
}
/**
* Move this plugin down
*
* We need this function so we can call directly (without the dir parameter)
* @param string $plugintomove - The plugin to move
* @return string The next page to display
*/
private function plugins_movedown(string $plugintomove): string {
return $this->move_plugin($plugintomove, 'down');
}
/**
* Change the order of this plugin.
*
* @param string $plugintomove - The plugin to move
* @param string $dir - up or down
* @return string The next page to display
*/
private function move_plugin(string $plugintomove, string $dir): string {
$plugins = $this->get_sorted_plugins_list();
$plugins = array_values($plugins);
$currentindex = array_search($plugintomove, $plugins);
if ($currentindex === false) {
return 'view';
}
// Make the switch.
if ($dir === 'up') {
if ($currentindex > 0) {
$tempplugin = $plugins[$currentindex - 1];
$plugins[$currentindex - 1] = $plugins[$currentindex];
$plugins[$currentindex] = $tempplugin;
}
} else if ($dir === 'down') {
if ($currentindex < (count($plugins) - 1)) {
$tempplugin = $plugins[$currentindex + 1];
$plugins[$currentindex + 1] = $plugins[$currentindex];
$plugins[$currentindex] = $tempplugin;
}
}
// Save the new normal order.
foreach ($plugins as $key => $plugin) {
set_config('sortorder', $key, extension::BBB_EXTENSION_PLUGIN_NAME . '_' . $plugin);
}
return 'view';
}
}
@@ -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;
}
}