. /** * 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 */ 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}); } }