be252e49e2
Migrate legacy ajax/JS to external services and AMD, enable TLS verification, scope admin CSS with RTLCSS-safe rules, and fix README/lang/boilerplate gaps. Co-authored-by: Cursor <cursoragent@cursor.com>
482 lines
22 KiB
PHP
482 lines
22 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/>.
|
|
|
|
/**
|
|
* MeetLet activity module form.
|
|
*
|
|
* @package mod_meetletacbfree
|
|
* @copyright 2026 MeetLet <info@meetlet.net>
|
|
* @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<int,string> $descriptions
|
|
* @param array<int,string>|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;
|
|
}
|
|
}
|