. /** * MeetLet activity module form. * * @package mod_meetletacbfree * @copyright 2026 MeetLet * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/course/moodleform_mod.php'); use mod_meetletacbfree\api\service; use mod_meetletacbfree\local\config; use mod_meetletacbfree\local\featureaccess; use mod_meetletacbfree\local\roles; use mod_meetletacbfree\registration\manager as registration_manager; /** * MeetLet mod form. */ class mod_meetletacbfree_mod_form extends moodleform_mod { public function definition() { global $OUTPUT, $PAGE, $USER; $mform = $this->_form; $context = context_course::instance($this->_course->id); $current = empty($this->current->instance) ? null : $this->current; $isnew = empty($this->current->instance); if (!registration_manager::is_registered()) { $mform->addElement('static', 'registrationlock', '', registration_manager::admin_gate_notice_html()); $this->standard_hidden_coursemodule_elements(); $this->add_action_buttons(false); return; } $PAGE->requires->js_call_amd('mod_meetletacbfree/modform', 'init'); $PAGE->requires->js_call_amd('mod_meetletacbfree/featureaccess_matrix', 'init'); $PAGE->requires->js_call_amd('mod_meetletacbfree/settings_toggles', 'init'); $mform->addElement('header', 'general', get_string('general', 'form')); $mform->addElement('text', 'name', get_string('meetingname', 'meetletacbfree'), ['size' => '64']); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', null, 'required', null, 'client'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $this->standard_intro_elements(get_string('meetingintro', 'meetletacbfree')); $mform->addElement('header', 'meetletroom', get_string('roomsettings', 'meetletacbfree')); // Create-new is default; existing meetings are loaded lazily via Ajax. $linkmodes = [ 0 => get_string('linkmode_create', 'meetletacbfree'), 1 => get_string('linkmode_existing', 'meetletacbfree'), ]; $mform->addElement('select', 'linkmode', get_string('linkmode', 'meetletacbfree'), $linkmodes); $mform->setDefault('linkmode', 0); $mform->addHelpButton('linkmode', 'linkmode', 'meetletacbfree'); if (!$isnew) { $mform->hardFreeze('linkmode'); } $meetingsmeta = []; $selectedsco = (int)($current->existingscoid ?? 0); $selectedlabel = get_string('chooseservermeeting', 'meetletacbfree'); $readonly = !$isnew; // On edit of a linked activity, keep the selected meeting visible without listing all. if (!$isnew && !empty($current->linkmode) && $selectedsco && config::is_configured()) { try { $service = new service(); $service->login_admin(); $info = $service->meeting_info($selectedsco); $service->logout(); if ($info) { $selectedlabel = (string)$info->name; $urlpath = trim((string)($info->url ?? ''), '/'); if ($urlpath !== '') { $selectedlabel .= ' (/' . $urlpath . '/)'; } $meetingsmeta[$selectedsco] = [ 'name' => (string)$info->name, 'urlpath' => $urlpath, 'starttime' => !empty($info->starttime) ? (int)strtotime((string)$info->starttime) : 0, 'endtime' => !empty($info->endtime) ? (int)strtotime((string)$info->endtime) : 0, ]; } } catch (Throwable $e) { $selectedlabel = 'SCO ' . $selectedsco; } } $mform->addElement('hidden', 'existingscoid', $selectedsco); $mform->setType('existingscoid', PARAM_INT); $listhtml = ''; if ($selectedsco) { $listhtml = html_writer::tag('li', html_writer::tag('button', s($selectedlabel), [ 'type' => 'button', 'class' => 'meetlet-combobox__option is-selected', 'data-scoid' => $selectedsco, 'data-name' => $meetingsmeta[$selectedsco]['name'] ?? $selectedlabel, 'data-urlpath' => $meetingsmeta[$selectedsco]['urlpath'] ?? '', 'data-starttime' => (string)($meetingsmeta[$selectedsco]['starttime'] ?? 0), 'data-endtime' => (string)($meetingsmeta[$selectedsco]['endtime'] ?? 0), ]) ); } $toggleattrs = [ 'type' => 'button', 'class' => 'meetlet-combobox__toggle', 'data-meeting-toggle' => '1', 'aria-expanded' => 'false', ]; if ($readonly) { $toggleattrs['disabled'] = 'disabled'; } $picker = html_writer::div( html_writer::tag('button', html_writer::span(s($selectedlabel), 'meetlet-combobox__value', ['data-meeting-label' => '1']) . html_writer::span('▾', 'meetlet-combobox__caret', ['aria-hidden' => 'true']), $toggleattrs ) . html_writer::div( html_writer::empty_tag('input', [ 'type' => 'search', 'class' => 'meetlet-combobox__search', 'data-meeting-search' => '1', 'placeholder' => get_string('searchservermeetings', 'meetletacbfree'), 'autocomplete' => 'off', ]) . html_writer::tag('ul', $listhtml, [ 'class' => 'meetlet-combobox__list', 'data-meeting-list' => '1', ]) . html_writer::div('', 'meetlet-combobox__hint', ['data-meeting-hint' => '1']), 'meetlet-combobox__panel', ['data-meeting-panel' => '1', 'hidden' => 'hidden'] ), 'meetlet-combobox' . ($readonly ? ' is-readonly' : ''), [ 'data-meeting-picker' => '1', 'data-courseid' => (int)$this->_course->id, 'data-readonly' => $readonly ? '1' : '0', 'data-str-loading' => get_string('loadingmeetings', 'meetletacbfree'), 'data-str-empty' => get_string('nomeetingsfound', 'meetletacbfree'), 'data-str-choose' => get_string('chooseservermeeting', 'meetletacbfree'), ] ); $mform->addElement('static', 'existingscoid_ui', get_string('existingscoid', 'meetletacbfree'), $picker); $mform->hideIf('existingscoid_ui', 'linkmode', 'neq', 1); $mform->addElement('html', html_writer::div('', 'd-none', [ 'data-meetings-meta' => json_encode($meetingsmeta, JSON_UNESCAPED_UNICODE), ])); $mform->addElement('text', 'meeturl', get_string('meeturl', 'meetletacbfree'), ['size' => 40]); $mform->setType('meeturl', PARAM_PATH); $mform->addHelpButton('meeturl', 'meeturl', 'meetletacbfree'); $mform->hideIf('meeturl', 'linkmode', 'eq', 1); $mform->addElement('select', 'meetingpublic', get_string('meetingtype', 'meetletacbfree'), [ 0 => get_string('private', 'meetletacbfree'), 1 => get_string('public', 'meetletacbfree'), ]); $mform->setDefault('meetingpublic', 0); $langs = config::meeting_lang_options(); $mform->addElement('select', 'meetinglang', get_string('meetinglang', 'meetletacbfree'), $langs); $mform->setType('meetinglang', PARAM_ALPHANUMEXT); $mform->addHelpButton('meetinglang', 'meetinglang', 'meetletacbfree'); $mform->setDefault('meetinglang', config::default_meeting_lang()); $waitstate = $this->resolve_toggle_state($current, 'waitforhost', $isnew); $mform->addElement('static', 'waitforhost_state_ui', get_string('waitforhost', 'meetletacbfree'), $this->render_override_chip_group( 'waitforhost_state', $waitstate, get_string('waitforhost_help', 'meetletacbfree'), [ config::OVERRIDE_INHERIT => get_string( config::default_wait_for_host() ? 'settingstate_inherit_on' : 'settingstate_inherit_off', 'meetletacbfree' ), ] ) ); $mform->addElement('hidden', 'waitforhost', $waitstate === config::OVERRIDE_ON ? 1 : 0); $mform->setType('waitforhost', PARAM_INT); $templates = [0 => get_string('defaulttemplate', 'meetletacbfree')]; if (config::is_configured()) { try { $service = new service(); $service->login_admin(); $templates = $templates + $service->get_templates(); $service->logout(); } catch (Throwable $e) { // Ignore. } } $mform->addElement('select', 'templatescoid', get_string('meettemplates', 'meetletacbfree'), $templates); $mform->setType('templatescoid', PARAM_INT); $mform->hideIf('templatescoid', 'linkmode', 'eq', 1); $mform->addElement('date_time_selector', 'starttime', get_string('starttime', 'meetletacbfree')); $mform->addRule('starttime', null, 'required', null, 'client'); $mform->addElement('advcheckbox', 'endtimeenable', get_string('endtimeenable', 'meetletacbfree')); $mform->addElement('date_time_selector', 'endtime', get_string('endtime', 'meetletacbfree')); $mform->disabledIf('endtime', 'endtimeenable'); $mform->setDefault('endtime', time() + 2 * HOURSECS); // BBB-style participant mapping. $participantlist = roles::get_participant_list($current, $context); $participantselection = roles::get_participant_selection_data(); $mform->addElement('header', 'permissions', get_string('rolemapping', 'meetletacbfree')); $mform->setExpanded('permissions'); $mform->addElement('static', 'rolemappingdesc', '', get_string('rolemapping_desc', 'meetletacbfree')); $participantsmode = $this->resolve_participants_mode($current, $isnew); $mform->addElement('static', 'participantsmode_ui', get_string('rolemapping_mode', 'meetletacbfree'), $this->render_override_chip_group( 'participantsmode_state', $participantsmode, get_string('rolemapping_mode_help', 'meetletacbfree'), [], [ roles::MODE_INHERIT => get_string('settingstate_inherit', 'meetletacbfree'), roles::MODE_OVERRIDE => get_string('rolemapping_mode_override_short', 'meetletacbfree'), ] ) ); $mform->addElement('hidden', 'participantsmode', $participantsmode === roles::MODE_OVERRIDE ? 1 : 0); $mform->setType('participantsmode', PARAM_INT); $mform->addElement('hidden', 'participants', json_encode($participantlist)); $mform->setType('participants', PARAM_RAW); $selectiontype = new single_select(new moodle_url(qualified_me()), 'meetlet_participant_selection_type', $participantselection['type_options'], $participantselection['type_selected']); $selectionparticipants = new single_select(new moodle_url(qualified_me()), 'meetlet_participant_selection', $participantselection['options'], $participantselection['selected']); $action = new single_button(new moodle_url(qualified_me()), get_string('participant_add', 'meetletacbfree'), 'post', single_button::BUTTON_SECONDARY, ['name' => 'meetlet_participant_selection_add'] ); $html = $OUTPUT->render_from_template('mod_meetletacbfree/participant_form', [ 'selectionType' => $selectiontype->export_for_template($OUTPUT), 'selectionParticipant' => $selectionparticipants->export_for_template($OUTPUT), 'action' => $action->export_for_template($OUTPUT), ]); $mform->addElement('static', 'static_participant_list', get_string('participant_list', 'meetletacbfree'), $html); $mform->addElement('html', html_writer::div('', 'd-none', [ 'data-participant-data' => json_encode(roles::get_participant_data($context, $current ?: (object)['id' => 0])), ])); if (featureaccess::activity_override_enabled()) { $mform->addElement('header', 'meetletaccess', get_string('featureaccess_activity', 'meetletacbfree')); $mform->addElement('advcheckbox', 'featureaccess_override', get_string('featureaccess_override', 'meetletacbfree'), get_string('featureaccess_override_help', 'meetletacbfree')); $hascustom = $current && !empty($current->featureaccess); $mform->setDefault('featureaccess_override', $hascustom ? 1 : 0); $matrix = featureaccess::matrix_for_activity($current); $mform->addElement('html', featureaccess::render_matrix_html($matrix, 'featureaccess_matrix')); $mform->addElement('hidden', 'featureaccess', ''); $mform->setType('featureaccess', PARAM_RAW); } if (config::offline_render_active()) { $mform->addElement('header', 'meetletoffline', get_string('offlinesettings', 'meetletacbfree')); $autostate = $this->resolve_toggle_state($current, 'autooffline', $isnew); $mform->addElement('static', 'autooffline_state_ui', get_string('autooffline', 'meetletacbfree'), $this->render_override_chip_group( 'autooffline_state', $autostate, get_string('autooffline_help', 'meetletacbfree'), [ config::OVERRIDE_INHERIT => get_string( config::auto_offline_default() ? 'settingstate_inherit_on' : 'settingstate_inherit_off', 'meetletacbfree' ), ] ) ); $mform->addElement('hidden', 'autooffline', $autostate === config::OVERRIDE_ON ? 1 : 0); $mform->setType('autooffline', PARAM_INT); } $mform->addElement('header', 'meetletfeatures', get_string('activityfeatures', 'meetletacbfree')); $featurefields = [ 'zip' => 'showzip', 'recordingstab' => 'showrecordingstab', 'contenttab' => 'showcontenttab', 'mp4' => 'showmp4', 'makeoffline' => 'showmakeoffline', ]; foreach ($featurefields as $feature => $field) { if (!config::feature_enabled($feature)) { continue; } if (!config::feature_allow_override($feature)) { continue; } $state = $this->resolve_toggle_state($current, $field, $isnew); $inheritkey = config::feature_enabled($feature) ? 'settingstate_inherit_on' : 'settingstate_inherit_off'; $help = ($field === 'showzip') ? get_string('showzip_help', 'meetletacbfree') : ''; $mform->addElement('static', $field . '_state_ui', get_string($field, 'meetletacbfree'), $this->render_override_chip_group( $field . '_state', $state, $help, [config::OVERRIDE_INHERIT => get_string($inheritkey, 'meetletacbfree')] ) ); $mform->addElement('hidden', $field, $state === config::OVERRIDE_ON ? 1 : 0); $mform->setType($field, PARAM_INT); if ($field === 'showzip') { $mform->removeElement($field . '_state_ui'); $mform->addElement('static', $field . '_state_ui', get_string($field, 'meetletacbfree'), $this->render_override_chip_group( $field . '_state', $state, get_string('showzip_help', 'meetletacbfree'), [config::OVERRIDE_INHERIT => get_string($inheritkey, 'meetletacbfree')] ) ); } } $this->standard_grading_coursemodule_elements(); $this->standard_coursemodule_elements(); $this->add_action_buttons(); } public function definition_after_data() { if (!registration_manager::is_registered()) { return; } parent::definition_after_data(); } public function data_preprocessing(&$defaultvalues) { parent::data_preprocessing($defaultvalues); $defaultvalues['endtimeenable'] = !empty($defaultvalues['endtime']) ? 1 : 0; foreach (\meetletacbfree_activity_override_fields() as $field) { $defaultvalues[$field . '_state'] = $this->resolve_toggle_state((object)$defaultvalues, $field, false); } $defaultvalues['participantsmode'] = $this->resolve_participants_mode((object)$defaultvalues, false); if (!empty($defaultvalues['participants'])) { $rules = json_decode($defaultvalues['participants'], true); if (is_array($rules)) { $defaultvalues['participants'] = json_encode(roles::get_participant_list( (object)$defaultvalues, context_course::instance($this->_course->id) )); } } } public function validation($data, $files) { $errors = parent::validation($data, $files); if (!registration_manager::is_registered()) { $errors['registrationlock'] = get_string('registration_required', 'meetletacbfree'); return $errors; } if (!empty($data['endtimeenable']) && (int)$data['endtime'] <= (int)$data['starttime']) { $errors['endtime'] = get_string('greaterstarttime', 'meetletacbfree'); } if (!empty($data['meeturl']) && empty($data['linkmode']) && !preg_match('/^[a-zA-Z][a-zA-Z0-9\-]*$/', $data['meeturl'])) { $errors['meeturl'] = get_string('invalidurl', 'meetletacbfree'); } if (!empty($data['linkmode']) && empty($data['existingscoid'])) { $errors['existingscoid_ui'] = get_string('errorexistingsco', 'meetletacbfree'); } return $errors; } /** * Render a compact chip-style radio group inside the activity form. * * @param array $descriptions * @param array|null $labels */ protected function render_override_chip_group( string $name, int $selected, string $help = '', array $descriptions = [], ?array $labels = null ): string { $labels = $labels ?? config::override_state_options(); $chips = ''; foreach ($labels as $value => $label) { $id = 'id_' . preg_replace('/[^a-zA-Z0-9_-]/', '_', $name . '_' . $value); $description = $descriptions[$value] ?? ''; $attrs = [ 'type' => 'radio', 'name' => $name, 'id' => $id, 'value' => (int)$value, 'class' => 'meetlet-rolechip__input meetlet-settingchip__input', 'data-meetlet-setting-radio' => '1', ]; if ((int)$selected === (int)$value) { $attrs['checked'] = 'checked'; } if ($description !== '') { $attrs['aria-label'] = $label . ' - ' . $description; } $body = \html_writer::empty_tag('input', $attrs) . \html_writer::span( \html_writer::span(s($label), 'meetlet-rolechip__text') . ($description !== '' ? \html_writer::span(s($description), 'meetlet-settingchip__meta', ['aria-hidden' => 'true']) . \html_writer::span(s($description), 'meetlet-sr-only') : ''), 'meetlet-settingchip__content' ); $chips .= \html_writer::tag('label', $body, [ 'class' => 'meetlet-rolechip meetlet-settingchip', 'for' => $id, ]); } $groupclasses = 'meetlet-rolechip-group meetlet-settingchip-group'; if (count($labels) === 2) { $groupclasses .= ' meetlet-settingchip-group--binary'; } $html = \html_writer::div($chips, $groupclasses, [ 'role' => 'radiogroup', ]); if ($help !== '') { $html .= \html_writer::div(s($help), 'meetlet-settinghint'); } return $html; } protected function resolve_toggle_state(?object $current, string $field, bool $isnew): int { if ($isnew || $current === null) { return config::OVERRIDE_INHERIT; } if (property_exists($current, 'settingoverrides') || property_exists($current, 'participantsmode')) { return config::activity_override_state($current, $field); } return !empty($current->{$field}) ? config::OVERRIDE_ON : config::OVERRIDE_OFF; } protected function resolve_participants_mode(?object $current, bool $isnew): int { if ($isnew || $current === null) { return roles::MODE_INHERIT; } if (property_exists($current, 'participantsmode')) { return roles::activity_inherits_defaults($current) ? roles::MODE_INHERIT : roles::MODE_OVERRIDE; } return roles::MODE_OVERRIDE; } }