Files

585 lines
20 KiB
PHP

<?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/>.
/**
* Library functions for MeetLet Adobe Connect Bridge.
*
* @package mod_meetletacbfree
* @copyright 2026 MeetLet
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use mod_meetletacbfree\api\service;
use mod_meetletacbfree\local\config;
use mod_meetletacbfree\local\roles;
/**
* Feature support.
*
* @param string $feature
* @return mixed
*/
function meetletacbfree_supports($feature) {
switch ($feature) {
case FEATURE_GROUPS:
return true;
case FEATURE_GROUPINGS:
return true;
case FEATURE_MOD_INTRO:
return true;
case FEATURE_COMPLETION_TRACKS_VIEWS:
return true;
case FEATURE_GRADE_HAS_GRADE:
return true;
case FEATURE_GRADE_OUTCOMES:
return false;
case FEATURE_BACKUP_MOODLE2:
return true;
case FEATURE_SHOW_DESCRIPTION:
return true;
case FEATURE_MOD_PURPOSE:
return MOD_PURPOSE_COMMUNICATION;
default:
return null;
}
}
/**
* Declare branded monologo so Moodle does not recolor it (orange communication filter).
*
* @return bool
*/
function meetletacbfree_is_branded(): bool {
return true;
}
/**
* Control activity chooser visibility until online registration is complete.
*
* @param \core_course\local\entity\content_item $contentitem
* @param \stdClass $user
* @param \stdClass $course
* @return array
*/
function mod_meetletacbfree_get_course_content_items(
\core_course\local\entity\content_item $contentitem,
\stdClass $user,
\stdClass $course
): array {
if (!\mod_meetletacbfree\registration\manager::is_registered()) {
return [];
}
return [$contentitem];
}
/**
* Add instance.
*
* @param stdClass $data
* @param mod_meetletacbfree_mod_form|null $mform
* @return int
*/
function meetletacbfree_add_instance($data, $mform = null) {
global $DB, $USER;
\mod_meetletacbfree\registration\manager::require_registered();
$data->timecreated = time();
$data->timemodified = $data->timecreated;
$data->userid = $USER->id;
if (empty($data->endtimeenable)) {
$data->endtime = 0;
}
$coursecontext = context_course::instance($data->course);
$data->participantsmode = empty($data->participantsmode) ? 0 : 1;
if (empty($data->participantsmode)) {
$data->participants = '';
} else if (empty($data->participants)) {
$data->participants = json_encode(roles::get_participant_list_default($coursecontext, $USER->id));
}
$togglefields = meetletacbfree_activity_override_fields();
$data->settingoverrides = config::setting_overrides_json(meetletacbfree_collect_setting_override_states($data, $togglefields));
$data->autooffline = meetletacbfree_legacy_setting_value($data, 'autooffline');
$data->meetingpublic = empty($data->meetingpublic) ? 0 : 1;
$data->showzip = meetletacbfree_legacy_setting_value($data, 'showzip');
$data->showofflineplayer = empty($data->showofflineplayer) ? 0 : 1;
$data->showrecordingstab = meetletacbfree_legacy_setting_value($data, 'showrecordingstab');
$data->showcontenttab = meetletacbfree_legacy_setting_value($data, 'showcontenttab');
$data->showmp4 = meetletacbfree_legacy_setting_value($data, 'showmp4');
$data->showmakeoffline = meetletacbfree_legacy_setting_value($data, 'showmakeoffline');
$data->linkmode = empty($data->linkmode) ? 0 : 1;
$data->existingscoid = (int)($data->existingscoid ?? 0);
$data->waitforhost = meetletacbfree_legacy_setting_value($data, 'waitforhost');
$data->meetinglang = clean_param($data->meetinglang ?? '', PARAM_ALPHANUMEXT);
if ($data->linkmode) {
$data->templatescoid = 0;
$data->meeturl = '';
}
$data->meeturl = meetletacbfree_clean_meet_url($data->meeturl ?? '');
if (empty($data->featureaccess_override)) {
$data->featureaccess = '';
} else {
$matrix = optional_param_array('featureaccess_matrix', [], PARAM_RAW);
$data->featureaccess = \mod_meetletacbfree\local\featureaccess::matrix_from_post($matrix);
}
unset($data->featureaccess_override, $data->featureaccess_matrix);
meetletacbfree_clear_override_form_fields($data, $togglefields);
$id = $DB->insert_record('meetletacbfree', $data);
$data->id = $id;
meetletacbfree_create_remote_meetings($data);
meetletacbfree_grade_item_update($data);
meetletacbfree_update_calendar($data);
return $id;
}
/**
* Update instance.
*
* @param stdClass $data
* @param mod_meetletacbfree_mod_form|null $mform
* @return bool
*/
function meetletacbfree_update_instance($data, $mform = null) {
global $DB, $USER;
\mod_meetletacbfree\registration\manager::require_registered();
$data->timemodified = time();
$data->id = $data->instance;
if (empty($data->endtimeenable)) {
$data->endtime = 0;
}
$coursecontext = context_course::instance($data->course);
$data->participantsmode = empty($data->participantsmode) ? 0 : 1;
if (empty($data->participantsmode)) {
$data->participants = '';
} else if (empty($data->participants)) {
$data->participants = json_encode(roles::get_participant_list_default($coursecontext, $USER->id ?? 0));
}
$togglefields = meetletacbfree_activity_override_fields();
$data->settingoverrides = config::setting_overrides_json(meetletacbfree_collect_setting_override_states($data, $togglefields));
$data->autooffline = meetletacbfree_legacy_setting_value($data, 'autooffline');
$data->meetingpublic = empty($data->meetingpublic) ? 0 : 1;
$data->showzip = meetletacbfree_legacy_setting_value($data, 'showzip');
$data->showofflineplayer = empty($data->showofflineplayer) ? 0 : 1;
$data->showrecordingstab = meetletacbfree_legacy_setting_value($data, 'showrecordingstab');
$data->showcontenttab = meetletacbfree_legacy_setting_value($data, 'showcontenttab');
$data->showmp4 = meetletacbfree_legacy_setting_value($data, 'showmp4');
$data->showmakeoffline = meetletacbfree_legacy_setting_value($data, 'showmakeoffline');
$data->waitforhost = meetletacbfree_legacy_setting_value($data, 'waitforhost');
$data->meetinglang = clean_param($data->meetinglang ?? '', PARAM_ALPHANUMEXT);
// linkmode / existingscoid are immutable after create.
unset($data->linkmode, $data->existingscoid);
if (empty($data->featureaccess_override)) {
$data->featureaccess = '';
} else {
$matrix = optional_param_array('featureaccess_matrix', [], PARAM_RAW);
$data->featureaccess = \mod_meetletacbfree\local\featureaccess::matrix_from_post($matrix);
}
unset($data->featureaccess_override, $data->featureaccess_matrix);
meetletacbfree_clear_override_form_fields($data, $togglefields);
$DB->update_record('meetletacbfree', $data);
if (config::is_configured()) {
$service = new service();
$service->login_admin();
$maps = $DB->get_records('meetletacbfree_meetings', ['instanceid' => $data->id]);
foreach ($maps as $map) {
$payload = clone $data;
if (!empty($map->groupid)) {
$group = groups_get_group($map->groupid);
if ($group) {
$payload->name = $data->name . '_' . $group->name;
}
}
// Create or linked: push name/times/options to Adobe.
$service->update_meeting((int)$map->meetingscoid, $payload);
$service->set_meeting_public((int)$map->meetingscoid, !empty($data->meetingpublic));
}
$service->logout();
}
meetletacbfree_grade_item_update($data);
meetletacbfree_update_calendar($data);
return true;
}
/**
* Delete instance.
*
* @param int $id
* @return bool
*/
function meetletacbfree_delete_instance($id) {
global $DB;
\mod_meetletacbfree\registration\manager::require_registered();
if (!$record = $DB->get_record('meetletacbfree', ['id' => $id])) {
return false;
}
if (config::is_configured()) {
try {
$service = new service();
$service->login_admin();
$maps = $DB->get_records('meetletacbfree_meetings', ['instanceid' => $id]);
$deleteremote = empty($record->linkmode);
foreach ($maps as $map) {
if ($deleteremote) {
$service->delete_sco((int)$map->meetingscoid);
}
}
$service->logout();
} catch (Throwable $e) {
debugging('MeetLet delete remote meeting failed: ' . $e->getMessage(), DEBUG_DEVELOPER);
}
}
$DB->delete_records('meetletacbfree_recordings', ['instanceid' => $id]);
$DB->delete_records('meetletacbfree_meetings', ['instanceid' => $id]);
$DB->delete_records('meetletacbfree', ['id' => $id]);
meetletacbfree_grade_item_delete($record);
return true;
}
/**
* Create Adobe meetings for activity (and groups).
*
* @param stdClass $data
*/
function meetletacbfree_create_remote_meetings(stdClass $data): void {
global $DB, $COURSE, $USER;
if (!config::is_configured()) {
return;
}
$service = new service();
$service->login_admin();
$coursecontext = context_course::instance((int)$data->course);
$creatorrole = roles::resolve_adobe_role($data, $coursecontext, (int)($USER->id ?? 0));
if ($creatorrole === null) {
$creatorrole = service::ROLE_HOST;
}
$provisionscoids = [];
// Link an existing Adobe meeting instead of creating one.
if (!empty($data->linkmode) && !empty($data->existingscoid)) {
$scoid = (int)$data->existingscoid;
$info = $service->meeting_info($scoid);
if (!$info) {
$service->logout();
throw new moodle_exception('errorexistingsco', 'meetletacbfree');
}
$service->set_meeting_public($scoid, !empty($data->meetingpublic));
// Push Moodle form values (name/times/options) onto the existing Adobe meeting.
$service->update_meeting($scoid, $data);
$map = (object)[
'instanceid' => $data->id,
'meetingscoid' => $scoid,
'groupid' => 0,
'urlpath' => $info->url ?? '',
];
$DB->insert_record('meetletacbfree_meetings', $map);
$provisionscoids[] = $scoid;
if (!empty($info->url)) {
$DB->set_field('meetletacbfree', 'meeturl', trim($info->url, '/'), ['id' => $data->id]);
}
meetletacbfree_provision_creator_on_meetings($service, $data, $provisionscoids, $creatorrole);
$service->logout();
return;
}
$folder = $service->get_folder_scoid('meetings');
if (!$folder) {
$service->logout();
throw new moodle_exception('errornofolder', 'meetletacbfree');
}
// During add_instance, course_modules.instance is not linked yet — use form data.
$groupmode = (int)($data->groupmode ?? 0);
if ($groupmode) {
$groups = groups_get_all_groups($data->course);
foreach ($groups as $group) {
$payload = clone $data;
$payload->name = $data->name . '_' . $group->name;
if (!empty($data->meeturl)) {
$payload->meeturl = meetletacbfree_clean_meet_url($data->meeturl . '_' . $group->name);
}
$scoid = $service->create_meeting($payload, $folder);
$service->set_meeting_public($scoid, !empty($data->meetingpublic));
$info = $service->meeting_info($scoid);
$map = (object)[
'instanceid' => $data->id,
'meetingscoid' => $scoid,
'groupid' => $group->id,
'urlpath' => $info->url ?? '',
];
$DB->insert_record('meetletacbfree_meetings', $map);
$provisionscoids[] = $scoid;
}
} else {
$scoid = $service->create_meeting($data, $folder);
$service->set_meeting_public($scoid, !empty($data->meetingpublic));
$info = $service->meeting_info($scoid);
$map = (object)[
'instanceid' => $data->id,
'meetingscoid' => $scoid,
'groupid' => 0,
'urlpath' => $info->url ?? '',
];
$DB->insert_record('meetletacbfree_meetings', $map);
$provisionscoids[] = $scoid;
if (empty($data->meeturl) && !empty($info->url)) {
$DB->set_field('meetletacbfree', 'meeturl', trim($info->url, '/'), ['id' => $data->id]);
}
}
meetletacbfree_provision_creator_on_meetings($service, $data, $provisionscoids, $creatorrole);
$service->logout();
}
/**
* Provision the activity creator on newly created Adobe meetings (admin API).
*
* @param int[] $scoids
*/
function meetletacbfree_provision_creator_on_meetings(service $service, stdClass $data, array $scoids, string $permission): void {
global $USER;
if (empty($scoids) || empty($USER->id)) {
return;
}
try {
foreach (array_unique(array_map('intval', $scoids)) as $scoid) {
if ($scoid > 0) {
$service->provision_user_on_meeting($USER, $scoid, $permission);
}
}
} catch (Throwable $e) {
debugging('MeetLet provision creator on meeting failed: ' . $e->getMessage(), DEBUG_DEVELOPER);
}
}
/**
* Clean custom meeting URL path.
*/
function meetletacbfree_clean_meet_url(string $url): string {
$url = trim($url);
$url = trim($url, '/');
$url = preg_replace('/[^a-zA-Z0-9\-]/', '', $url);
return substr((string)$url, 0, 60);
}
/**
* Grade item update.
*/
function meetletacbfree_grade_item_update($meetlet, $grades = null) {
global $CFG;
require_once($CFG->libdir . '/gradelib.php');
$params = [
'itemname' => $meetlet->name,
'idnumber' => $meetlet->cmidnumber ?? '',
];
if (isset($meetlet->grade) && $meetlet->grade > 0) {
$params['gradetype'] = GRADE_TYPE_VALUE;
$params['grademax'] = $meetlet->grade;
$params['grademin'] = 0;
} else if (isset($meetlet->grade) && $meetlet->grade < 0) {
$params['gradetype'] = GRADE_TYPE_SCALE;
$params['scaleid'] = -$meetlet->grade;
} else {
$params['gradetype'] = GRADE_TYPE_NONE;
}
if ($grades === 'reset') {
$params['reset'] = true;
$grades = null;
}
return grade_update('mod/meetletacbfree', $meetlet->course, 'mod', 'meetletacbfree', $meetlet->id, 0, $grades, $params);
}
/**
* Delete grade item.
*/
function meetletacbfree_grade_item_delete($meetlet) {
global $CFG;
require_once($CFG->libdir . '/gradelib.php');
return grade_update('mod/meetletacbfree', $meetlet->course, 'mod', 'meetletacbfree', $meetlet->id, 0, null, ['deleted' => 1]);
}
/**
* Update calendar event.
*/
function meetletacbfree_update_calendar($meetlet): void {
global $DB, $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$event = $DB->get_record('event', [
'modulename' => 'meetletacbfree',
'instance' => $meetlet->id,
]);
if (!$event) {
// Pre-rename calendar rows.
$event = $DB->get_record('event', [
'modulename' => 'meetlet',
'instance' => $meetlet->id,
]);
}
if (empty($meetlet->starttime)) {
if ($event) {
$calendarevent = calendar_event::load($event->id);
$calendarevent->delete();
}
return;
}
$eventdata = new stdClass();
$eventdata->name = $meetlet->name;
$eventdata->description = format_module_intro('meetletacbfree', $meetlet, $meetlet->coursemodule ?? 0, false);
$eventdata->format = FORMAT_HTML;
$eventdata->courseid = $meetlet->course;
$eventdata->groupid = 0;
$eventdata->userid = 0;
$eventdata->modulename = 'meetletacbfree';
$eventdata->instance = $meetlet->id;
$eventdata->eventtype = 'course';
$eventdata->timestart = $meetlet->starttime;
$eventdata->timeduration = !empty($meetlet->endtime) ? max(0, $meetlet->endtime - $meetlet->starttime) : 0;
$eventdata->visible = 1;
if ($event) {
$calendarevent = calendar_event::load($event->id);
$calendarevent->update($eventdata);
} else {
calendar_event::create($eventdata);
}
}
/**
* Extend settings navigation (optional).
*/
function meetletacbfree_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $meetletnode) {
// Intentionally minimal ? roles are configured in activity settings.
}
/**
* Course module info for course page.
*/
function meetletacbfree_get_coursemodule_info($coursemodule) {
global $DB;
if (!$record = $DB->get_record('meetletacbfree', ['id' => $coursemodule->instance], 'id, name, intro, introformat, starttime, endtime')) {
return false;
}
$info = new cached_cm_info();
$info->name = $record->name;
if ($coursemodule->showdescription) {
$info->content = format_module_intro('meetletacbfree', $record, $coursemodule->id, false);
}
// Branded monologo: skip Moodle purpose colour filter (orange on course page).
$info->customdata['filtericon'] = false;
return $info;
}
/**
* Apply plugin cron interval to the Moodle scheduled task row.
*/
function meetletacbfree_update_cron_interval() {
\mod_meetletacbfree\local\config::apply_cron_interval();
}
/**
* Activity boolean settings managed by the inherit/override model.
*
* @return string[]
*/
function meetletacbfree_activity_override_fields(): array {
return ['waitforhost', 'autooffline', 'showzip', 'showrecordingstab', 'showcontenttab', 'showmp4', 'showmakeoffline'];
}
/**
* Collect posted three-state values for activity settings.
*
* @param stdClass $data
* @param string[] $fields
* @return array<string,int>
*/
function meetletacbfree_collect_setting_override_states(stdClass $data, array $fields): array {
$states = [];
foreach ($fields as $field) {
$statefield = $field . '_state';
$states[$field] = $data->{$statefield} ?? \mod_meetletacbfree\local\config::OVERRIDE_INHERIT;
}
return $states;
}
/**
* Keep legacy columns in sync for older code, reports and backups.
*/
function meetletacbfree_legacy_setting_value(stdClass $data, string $field): int {
$statefield = $field . '_state';
$state = \mod_meetletacbfree\local\config::normalize_override_state($data->{$statefield} ?? null);
if ($state === \mod_meetletacbfree\local\config::OVERRIDE_ON) {
return 1;
}
if ($state === \mod_meetletacbfree\local\config::OVERRIDE_OFF) {
return 0;
}
switch ($field) {
case 'waitforhost':
return \mod_meetletacbfree\local\config::default_wait_for_host() ? 1 : 0;
case 'autooffline':
return \mod_meetletacbfree\local\config::auto_offline_default() ? 1 : 0;
case 'showzip':
return \mod_meetletacbfree\local\config::feature_enabled('zip') ? 1 : 0;
case 'showrecordingstab':
return \mod_meetletacbfree\local\config::feature_enabled('recordingstab') ? 1 : 0;
case 'showcontenttab':
return \mod_meetletacbfree\local\config::feature_enabled('contenttab') ? 1 : 0;
case 'showmp4':
return \mod_meetletacbfree\local\config::feature_enabled('mp4') ? 1 : 0;
case 'showmakeoffline':
return \mod_meetletacbfree\local\config::feature_enabled('makeoffline') ? 1 : 0;
default:
return empty($data->{$field}) ? 0 : 1;
}
}
/**
* Remove transient form-only fields before DB write.
*
* @param stdClass $data
* @param string[] $fields
*/
function meetletacbfree_clear_override_form_fields(stdClass $data, array $fields): void {
foreach ($fields as $field) {
$statefield = $field . '_state';
unset($data->{$statefield});
}
}