From be252e49e2324dc8e48921d1590f99e2519f2098 Mon Sep 17 00:00:00 2001 From: Mohammad Reza Zamani Date: Sun, 20 Sep 2026 12:41:47 +0330 Subject: [PATCH] Address Moodle Marketplace review for mod_meetletacbfree 1.1.3. 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 --- README.md | 4 +- ajax/conntest.php | 41 -- ajax/registration.php | 83 --- ajax/settings_reset.php | 69 -- .../src}/featureaccess_matrix.js | 31 +- .../src}/meetlet_modal_scroll.js | 66 +- {javascript => amd/src}/modform.js | 28 +- .../src}/settings_access_reset.js | 72 +- {javascript => amd/src}/settings_conntest.js | 49 +- .../src}/settings_registration.js | 106 +-- {javascript => amd/src}/settings_toggles.js | 32 +- {javascript => amd/src}/view.js | 29 +- classes/admin/access_reset_ui.php | 5 +- classes/admin/setting_registration.php | 13 +- classes/admin/settingspage_tabs.php | 18 +- classes/api/client.php | 4 +- classes/api/service.php | 3 +- classes/external/registration_action.php | 124 ++++ classes/external/reset_access_settings.php | 89 +++ classes/external/run_conntest_step.php | 97 +++ classes/local/content.php | 3 +- .../registration/registration_exception.php | 15 +- conntest.php | 22 +- db/services.php | 24 + lang/en/meetletacbfree.php | 9 + lang/fa/meetletacbfree.php | 9 + mod_form.php | 33 +- settings.php | 620 +----------------- styles.css | 559 +++++++++++++++- templates/participant_form_add.mustache | 20 + templates/participant_single_select.mustache | 20 + version.php | 2 +- view.php | 5 +- 33 files changed, 1281 insertions(+), 1023 deletions(-) delete mode 100644 ajax/conntest.php delete mode 100644 ajax/registration.php delete mode 100644 ajax/settings_reset.php rename {javascript => amd/src}/featureaccess_matrix.js (64%) rename {javascript => amd/src}/meetlet_modal_scroll.js (51%) rename {javascript => amd/src}/modform.js (94%) rename {javascript => amd/src}/settings_access_reset.js (90%) rename {javascript => amd/src}/settings_conntest.js (90%) rename {javascript => amd/src}/settings_registration.js (81%) rename {javascript => amd/src}/settings_toggles.js (85%) rename {javascript => amd/src}/view.js (97%) create mode 100644 classes/external/registration_action.php create mode 100644 classes/external/reset_access_settings.php create mode 100644 classes/external/run_conntest_step.php diff --git a/README.md b/README.md index 80f221f..310ad11 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # MeetLet Adobe Connect Bridge — Marketplace Edition **Component:** `mod_meetletacbfree` -**Release:** 1.0.9-marketplace +**Release:** 1.1.3 **License:** [GNU GPL v3 or later](COPYING.txt) Connect Moodle to Adobe Connect for live classes, single sign-on (SSO), recordings, and shared meeting content — without a separate Windows agent or commercial license. @@ -34,7 +34,7 @@ This edition is free to install and use. It includes the core classroom workflow | **Activity defaults** | Site-wide meeting language, wait-for-host, and tab visibility with per-activity overrides | | **Backup & restore** | Standard Moodle 2 backup API support for course copies and restores | | **Privacy API** | GDPR-ready privacy provider for exported and deleted user data | -| **Optional site registration** | Transparent opt-in registration with MeetLet for update notices and support statistics (plugin works fully without it) | +| **Plugin activation** | Voluntary administrator action with MeetLet; required to unlock meeting features (SSO join, recordings, content, and related tools) | --- diff --git a/ajax/conntest.php b/ajax/conntest.php deleted file mode 100644 index 9279d5a..0000000 --- a/ajax/conntest.php +++ /dev/null @@ -1,41 +0,0 @@ -. - -/** - * Ajax endpoint: run one Adobe Connect connection-test step. - * - * @package mod_meetletacbfree - * @copyright 2026 MeetLet - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -define('AJAX_SCRIPT', true); - -require_once('../../../config.php'); - -use mod_meetletacbfree\local\conntest_runner; - -require_login(); -require_capability('moodle/site:config', context_system::instance()); -require_sesskey(); - -\mod_meetletacbfree\registration\manager::require_registered(); - -$step = required_param('step', PARAM_ALPHA); -$result = conntest_runner::run_step($step); - -header('Content-Type: application/json; charset=utf-8'); -echo json_encode($result); diff --git a/ajax/registration.php b/ajax/registration.php deleted file mode 100644 index 93a76b0..0000000 --- a/ajax/registration.php +++ /dev/null @@ -1,83 +0,0 @@ - true, - 'message' => get_string('registration_activated', 'meetletacbfree'), - 'summary' => manager::status_summary(), - ]); - exit; - } - - if ($action === 'recheck' || $action === 'refresh' || $action === 'validate') { - manager::recheck(true); - echo json_encode([ - 'success' => true, - 'message' => get_string('registration_rechecked', 'meetletacbfree'), - 'summary' => manager::status_summary(), - ]); - exit; - } - - if ($action === 'deactivate') { - manager::deactivate(); - echo json_encode([ - 'success' => true, - 'message' => get_string('registration_deactivated', 'meetletacbfree'), - 'summary' => manager::status_summary(), - ]); - exit; - } - - if ($action === 'status') { - echo json_encode([ - 'success' => true, - 'summary' => manager::status_summary(), - ]); - exit; - } - - throw new moodle_exception('invalidparameter'); -} catch (Throwable $e) { - $ex = registration_exception::from_throwable($e); - $mapped = error_mapper::from_exception($ex); - echo json_encode([ - 'success' => false, - 'error' => $mapped['message'] ?? $ex->get_api_message(), - 'error_code' => $ex->get_error_code(), - 'request_id' => $ex->get_request_id(), - 'summary' => manager::status_summary(), - ]); -} diff --git a/ajax/settings_reset.php b/ajax/settings_reset.php deleted file mode 100644 index dd53118..0000000 --- a/ajax/settings_reset.php +++ /dev/null @@ -1,69 +0,0 @@ - true, - 'section' => 'rolemapping', - 'matrix' => roles::matrix_from_rules($rules), - 'message' => get_string('reset_success_rolemapping', 'meetletacbfree'), - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - exit; - } - - if ($section === 'featureaccess') { - $matrix = featureaccess::builtin_defaults(); - $json = json_encode($matrix, JSON_UNESCAPED_UNICODE); - set_config('featureaccess', $json, 'meetletacbfree'); - echo json_encode([ - 'success' => true, - 'section' => 'featureaccess', - 'matrix' => $matrix, - 'message' => get_string('reset_success_featureaccess', 'meetletacbfree'), - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - exit; - } - - throw new moodle_exception('invalidparameter'); -} catch (Throwable $e) { - $error = $e->getMessage(); - if ($e instanceof moodle_exception) { - $info = get_exception_info($e); - if (!empty($info->message)) { - $error = (string)$info->message; - } - } - echo json_encode([ - 'success' => false, - 'error' => $error !== '' ? $error : get_string('reset_error', 'meetletacbfree'), - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -} diff --git a/javascript/featureaccess_matrix.js b/amd/src/featureaccess_matrix.js similarity index 64% rename from javascript/featureaccess_matrix.js rename to amd/src/featureaccess_matrix.js index 0c73e42..a732dcf 100644 --- a/javascript/featureaccess_matrix.js +++ b/amd/src/featureaccess_matrix.js @@ -1,8 +1,25 @@ +// 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 . + /** - * MeetLet feature-access matrix: row/column "select all" toggles. - * Page-local (admin settings + mod form). Not AMD. + * @module mod_meetletacbfree/featureaccess_matrix + * @copyright 2026 MeetLet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -(function() { + +define([], function() { const bindMatrix = (matrix) => { if (!matrix || matrix.getAttribute('data-access-bound') === '1') { return; @@ -46,9 +63,5 @@ document.querySelectorAll('[data-meetlet-access-matrix]').forEach(bindMatrix); }; - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); - } else { - init(); - } -})(); + return {init: init}; +}); diff --git a/javascript/meetlet_modal_scroll.js b/amd/src/meetlet_modal_scroll.js similarity index 51% rename from javascript/meetlet_modal_scroll.js rename to amd/src/meetlet_modal_scroll.js index 50b9378..c4f7375 100644 --- a/javascript/meetlet_modal_scroll.js +++ b/amd/src/meetlet_modal_scroll.js @@ -1,19 +1,32 @@ +// 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 . + /** - * Shared scroll lock for MeetLet modals. - * Uses body overflow + scroll guards — no position:fixed on body (avoids white-page flash). - * - * @package mod_meetletacbfree - * @copyright 2026 MeetLet + * @module mod_meetletacbfree/meetlet_modal_scroll + * @copyright 2026 MeetLet * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -(function (global) { + +define([], function() { 'use strict'; var scrollLockY = 0; var guardsActive = false; function scrollbarWidth() { - return Math.max(0, global.innerWidth - document.documentElement.clientWidth); + return Math.max(0, window.innerWidth - document.documentElement.clientWidth); } function focusNoScroll(el) { @@ -29,13 +42,13 @@ function restoreScroll() { if (scrollLockY > 0) { - global.scrollTo(0, scrollLockY); + window.scrollTo(0, scrollLockY); } } function onScrollGuard() { - if (scrollLockY && global.scrollY !== scrollLockY) { - global.scrollTo(0, scrollLockY); + if (scrollLockY && window.scrollY !== scrollLockY) { + window.scrollTo(0, scrollLockY); } } @@ -51,9 +64,9 @@ return; } guardsActive = true; - global.addEventListener('scroll', onScrollGuard, true); - global.addEventListener('wheel', onMoveGuard, {passive: false, capture: true}); - global.addEventListener('touchmove', onMoveGuard, {passive: false, capture: true}); + window.addEventListener('scroll', onScrollGuard, true); + window.addEventListener('wheel', onMoveGuard, {passive: false, capture: true}); + window.addEventListener('touchmove', onMoveGuard, {passive: false, capture: true}); } function unbindGuards() { @@ -61,24 +74,24 @@ return; } guardsActive = false; - global.removeEventListener('scroll', onScrollGuard, true); - global.removeEventListener('wheel', onMoveGuard, true); - global.removeEventListener('touchmove', onMoveGuard, true); + window.removeEventListener('scroll', onScrollGuard, true); + window.removeEventListener('wheel', onMoveGuard, true); + window.removeEventListener('touchmove', onMoveGuard, true); } function lockPageScroll() { if (document.documentElement.classList.contains('meetlet-modal-open')) { return; } - scrollLockY = global.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0; + scrollLockY = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0; var pad = scrollbarWidth(); if (pad > 0) { document.body.style.paddingRight = pad + 'px'; } document.documentElement.classList.add('meetlet-modal-open'); restoreScroll(); - if (typeof global.requestAnimationFrame === 'function') { - global.requestAnimationFrame(restoreScroll); + if (typeof window.requestAnimationFrame === 'function') { + window.requestAnimationFrame(restoreScroll); } bindGuards(); } @@ -93,18 +106,19 @@ document.body.style.paddingRight = ''; scrollLockY = 0; if (y) { - global.scrollTo(0, y); - if (typeof global.requestAnimationFrame === 'function') { - global.requestAnimationFrame(function () { - global.scrollTo(0, y); + window.scrollTo(0, y); + if (typeof window.requestAnimationFrame === 'function') { + window.requestAnimationFrame(function() { + window.scrollTo(0, y); }); } } } - global.MeetletModalScroll = { + return { + init: function() {}, lock: lockPageScroll, unlock: unlockPageScroll, - focusNoScroll: focusNoScroll, + focusNoScroll: focusNoScroll }; -}(typeof window !== 'undefined' ? window : this)); +}); diff --git a/javascript/modform.js b/amd/src/modform.js similarity index 94% rename from javascript/modform.js rename to amd/src/modform.js index 82fea1b..846324b 100644 --- a/javascript/modform.js +++ b/amd/src/modform.js @@ -1,7 +1,26 @@ +// 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 . + /** - * MeetLet mod form participants — page-local only (not AMD). + * @module mod_meetletacbfree/modform + * @copyright 2026 MeetLet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -require(['core/str', 'core/notification', 'core/templates', 'core/ajax'], function(Str, Notification, Templates, Ajax) { + +define(['core/str', 'core/notification', 'core/templates', 'core/ajax'], function(Str, Notification, Templates, Ajax) { + function init() { const ELEMENT_SELECTOR = { participantData: () => document.querySelector('[data-participant-data]'), participantList: () => document.getElementsByName('participants')[0], @@ -477,4 +496,9 @@ require(['core/str', 'core/notification', 'core/templates', 'core/ajax'], functi }); updateSelectionFromType(typeSelect); participantListInit(); + } + + return { + init: init + }; }); diff --git a/javascript/settings_access_reset.js b/amd/src/settings_access_reset.js similarity index 90% rename from javascript/settings_access_reset.js rename to amd/src/settings_access_reset.js index e0c1b43..a420073 100644 --- a/javascript/settings_access_reset.js +++ b/amd/src/settings_access_reset.js @@ -1,14 +1,28 @@ +// 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 . + /** - * Reset-to-default for site role mapping and feature-access matrix (admin settings). - * - * @package mod_meetletacbfree - * @copyright 2026 MeetLet + * @module mod_meetletacbfree/settings_access_reset + * @copyright 2026 MeetLet * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -(function () { + +define(['core/ajax', 'mod_meetletacbfree/meetlet_modal_scroll'], function(Ajax, MeetletModalScroll) { 'use strict'; - var modalState = { +var modalState = { open: false, resolve: null, lastFocus: null, @@ -18,15 +32,7 @@ }; function modalScroll() { - return window.MeetletModalScroll || { - lock: function () {}, - unlock: function () {}, - focusNoScroll: function (el) { - if (el && typeof el.focus === 'function') { - el.focus(); - } - }, - }; + return MeetletModalScroll; } function byId(id) { @@ -292,22 +298,18 @@ } function postReset(root, section, str) { - var endpoint = root.getAttribute('data-endpoint') || ''; - var sesskey = root.getAttribute('data-sesskey') || ''; - if (!endpoint || !sesskey) { - return Promise.reject(new Error(str.error || 'Error')); - } - var url = endpoint + (endpoint.indexOf('?') >= 0 ? '&' : '?') + - 'sesskey=' + encodeURIComponent(sesskey) + - '§ion=' + encodeURIComponent(section); - return fetch(url, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Accept': 'application/json', - }, - }).then(function (resp) { - return resp.json(); + return Ajax.call([{ + methodname: 'mod_meetletacbfree_reset_access_settings', + args: {section: section} + }])[0].then(function(data) { + if (data && data.matrixjson) { + try { + data.matrix = JSON.parse(data.matrixjson); + } catch (e) { + data.matrix = null; + } + } + return data; }); } @@ -412,9 +414,5 @@ }); } - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); - } else { - init(); - } -})(); + return {init: init}; +}); diff --git a/javascript/settings_conntest.js b/amd/src/settings_conntest.js similarity index 90% rename from javascript/settings_conntest.js rename to amd/src/settings_conntest.js index 4f9874b..657e7f1 100644 --- a/javascript/settings_conntest.js +++ b/amd/src/settings_conntest.js @@ -1,7 +1,28 @@ -(function () { +// 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 . + +/** + * @module mod_meetletacbfree/settings_conntest + * @copyright 2026 MeetLet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['core/ajax'], function(Ajax) { 'use strict'; - function ready(fn) { +function ready(fn) { if (document.readyState !== 'loading') { fn(); } else { @@ -131,7 +152,7 @@ }); } - ready(function () { + function init() { var SAVE_ICON = ''; @@ -191,9 +212,6 @@ if (!btn || !grid) { return; } - - var endpoint = root.getAttribute('data-endpoint'); - var sesskey = root.getAttribute('data-sesskey'); var steps = (root.getAttribute('data-steps') || 'login,folders,user,sso,cleanup') .split(',') .map(function (s) { return s.trim(); }) @@ -250,15 +268,12 @@ setCardState(card, 'running', strings, ''); setProgress(root, completed, steps.length, strings); - var url = endpoint + - (endpoint.indexOf('?') === -1 ? '?' : '&') + - 'step=' + encodeURIComponent(step) + - '&sesskey=' + encodeURIComponent(sesskey); - var response = await fetch(url, { - credentials: 'same-origin', - headers: { 'Accept': 'application/json' } + var result = await new Promise(function(resolve, reject) { + Ajax.call([{ + methodname: 'mod_meetletacbfree_run_conntest_step', + args: {step: step} + }])[0].then(resolve).catch(reject); }); - var result = await response.json(); setCardState( card, result.ok ? 'ok' : 'fail', @@ -303,5 +318,7 @@ } }); }); - }); -})(); + } + + return {init: init}; +}); diff --git a/javascript/settings_registration.js b/amd/src/settings_registration.js similarity index 81% rename from javascript/settings_registration.js rename to amd/src/settings_registration.js index defe0c8..3d736dc 100644 --- a/javascript/settings_registration.js +++ b/amd/src/settings_registration.js @@ -1,14 +1,28 @@ +// 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 . + /** - * Inline Ajax site registration on plugin settings tab (Marketplace free edition). - * - * @package mod_meetletacbfree - * @copyright 2026 MeetLet + * @module mod_meetletacbfree/settings_registration + * @copyright 2026 MeetLet * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -(function () { + +define(['core/ajax', 'mod_meetletacbfree/meetlet_modal_scroll'], function(Ajax, MeetletModalScroll) { 'use strict'; - function byId(id) { +function byId(id) { return document.getElementById(id); } @@ -98,15 +112,7 @@ } function modalScroll() { - return window.MeetletModalScroll || { - lock: function () {}, - unlock: function () {}, - focusNoScroll: function (el) { - if (el && typeof el.focus === 'function') { - el.focus(); - } - }, - }; + return MeetletModalScroll; } function closeRegistrationModal(result) { @@ -217,39 +223,39 @@ if (!cfg) { return; } - var body = new URLSearchParams(); - body.set('action', action); - body.set('sesskey', cfg.dataset.sesskey || ''); - body.set('registration_api_url', currentApiUrl(cfg)); - setFlash(flash, true, '…'); - fetch(cfg.dataset.ajaxurl, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, - credentials: 'same-origin', - body: body.toString(), - }) - .then(function (res) { - return res.json(); - }) - .then(function (data) { - if (data.summary) { - renderSummary(data.summary); + Ajax.call([{ + methodname: 'mod_meetletacbfree_registration_action', + args: { + action: action, + registration_api_url: currentApiUrl(cfg) + } + }])[0].then(function(data) { + data.summary = { + active: !!data.active, + status: data.status, + status_label: data.status_label, + product_label: data.product_label, + domain: data.domain, + installation_id: data.installation_id, + can_deactivate: !!data.can_deactivate + }; + if (data.summary) { + renderSummary(data.summary); + } + if (data.success) { + setFlash(flash, true, data.message); + if (action === 'activate' || action === 'deactivate') { + window.setTimeout(function() { + window.location.reload(); + }, 600); } - if (data.success) { - setFlash(flash, true, data.message); - if (action === 'activate' || action === 'deactivate') { - window.setTimeout(function () { - window.location.reload(); - }, 600); - } - } else { - setFlash(flash, false, data.error || 'Request failed'); - } - }) - .catch(function (err) { - setFlash(flash, false, err && err.message ? err.message : 'Request failed'); - }); + } else { + setFlash(flash, false, data.error || 'Request failed'); + } + }).catch(function(err) { + setFlash(flash, false, err && err.message ? err.message : 'Request failed'); + }); } function bindModalChrome() { @@ -326,9 +332,5 @@ bindDeactivateButton(deactivate); } - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', bind); - } else { - bind(); - } -})(); + return {init: bind}; +}); diff --git a/javascript/settings_toggles.js b/amd/src/settings_toggles.js similarity index 85% rename from javascript/settings_toggles.js rename to amd/src/settings_toggles.js index d5fdf08..727ffa4 100644 --- a/javascript/settings_toggles.js +++ b/amd/src/settings_toggles.js @@ -1,10 +1,28 @@ +// 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 . + /** - * Replace Moodle checkboxes with MeetLet pill switches (check / cross). + * @module mod_meetletacbfree/settings_toggles + * @copyright 2026 MeetLet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -(function () { + +define([], function() { 'use strict'; - function enhanceInput(input) { +function enhanceInput(input) { if (!input || input.getAttribute('data-meetlet-switch') === '1') { return; } @@ -138,9 +156,5 @@ syncHoursEnabled(); } - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', run); - } else { - run(); - } -})(); + return {init: run}; +}); diff --git a/javascript/view.js b/amd/src/view.js similarity index 97% rename from javascript/view.js rename to amd/src/view.js index b5a748d..bfc4348 100644 --- a/javascript/view.js +++ b/amd/src/view.js @@ -1,8 +1,26 @@ +// 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 . + /** - * MeetLet activity view — page-local only (not AMD). - * Loaded only on mod/meetletacbfree/view.php via $PAGE->requires->js(). + * @module mod_meetletacbfree/view + * @copyright 2026 MeetLet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -require(['core/ajax', 'core/notification', 'core/str'], function(Ajax, Notification, Str) { + +define(['core/ajax', 'core/notification', 'core/str'], function(Ajax, Notification, Str) { + function init() { const SELECTORS = { config: '[data-meetlet-config]', app: '.meetlet-app', @@ -1026,4 +1044,9 @@ require(['core/ajax', 'core/notification', 'core/str'], function(Ajax, Notificat startStatusTicker(); loadTab('details', false); + } + + return { + init: init + }; }); diff --git a/classes/admin/access_reset_ui.php b/classes/admin/access_reset_ui.php index 4279eff..83c14f0 100644 --- a/classes/admin/access_reset_ui.php +++ b/classes/admin/access_reset_ui.php @@ -25,7 +25,6 @@ namespace mod_meetletacbfree\admin; use mod_meetletacbfree\local\icons; -use moodle_url; defined('MOODLE_INTERNAL') || die(); @@ -65,7 +64,7 @@ class access_reset_ui { /** * Modal + config root (render once on the access settings tab). */ - public static function page_assets(moodle_url $ajaxurl): string { + public static function page_assets(): string { if (self::$assetsrendered) { return ''; } @@ -75,8 +74,6 @@ class access_reset_ui { $html = \html_writer::div('', '', [ 'id' => 'meetlet-access-reset-root', 'hidden' => 'hidden', - 'data-endpoint' => $ajaxurl->out(false), - 'data-sesskey' => sesskey(), 'data-strings' => json_encode($strings, JSON_UNESCAPED_UNICODE), ]); $html .= self::modal_html(); diff --git a/classes/admin/setting_registration.php b/classes/admin/setting_registration.php index 8720e19..7b2b5cd 100644 --- a/classes/admin/setting_registration.php +++ b/classes/admin/setting_registration.php @@ -45,16 +45,9 @@ class setting_registration extends admin_setting { $summary = manager::status_summary(); $active = !empty($summary['active']); $candeactivate = !empty($summary['can_deactivate']); - $ajaxurl = new \moodle_url('/mod/meetletacbfree/ajax/registration.php'); - $scrolljsurl = new \moodle_url('/mod/meetletacbfree/javascript/meetlet_modal_scroll.js', [ - 'v' => '2026090146', - ]); - $jsurl = new \moodle_url('/mod/meetletacbfree/javascript/settings_registration.js', [ - 'v' => '2026090146', - ]); $privacytaburl = manager::privacy_tab_url(); - $PAGE->requires->js($scrolljsurl); - $PAGE->requires->js($jsurl); + $PAGE->requires->js_call_amd('mod_meetletacbfree/meetlet_modal_scroll', 'init'); + $PAGE->requires->js_call_amd('mod_meetletacbfree/settings_registration', 'init'); $statusbadge = \html_writer::span( s($summary['status_label'] ?? ($active @@ -169,8 +162,6 @@ class setting_registration extends admin_setting { \html_writer::div('', 'mt-3 d-none', ['id' => 'meetlet-registration-flash']) . \html_writer::div('', '', [ 'id' => 'meetlet-registration-config', - 'data-ajaxurl' => $ajaxurl->out(false), - 'data-sesskey' => sesskey(), 'data-api-input' => 'id_s_meetletacbfree_registration_api_url', 'data-label-active' => get_string('registration_status_active', 'meetletacbfree'), 'data-label-inactive' => get_string('registration_status_inactive', 'meetletacbfree'), diff --git a/classes/admin/settingspage_tabs.php b/classes/admin/settingspage_tabs.php index 0defa21..6b2c138 100644 --- a/classes/admin/settingspage_tabs.php +++ b/classes/admin/settingspage_tabs.php @@ -44,6 +44,22 @@ class settingspage_tabs extends \theme_boost_admin_settingspage_tabs { * @return string */ public function output_html() { - return $this->herohtml . parent::output_html(); + global $PAGE; + if ($PAGE) { + $PAGE->requires->js_call_amd('mod_meetletacbfree/meetlet_modal_scroll', 'init'); + $PAGE->requires->js_call_amd('mod_meetletacbfree/settings_registration', 'init'); + $PAGE->requires->js_call_amd('mod_meetletacbfree/settings_conntest', 'init'); + $PAGE->requires->js_call_amd('mod_meetletacbfree/settings_toggles', 'init'); + $PAGE->requires->js_call_amd('mod_meetletacbfree/featureaccess_matrix', 'init'); + $PAGE->requires->js_call_amd('mod_meetletacbfree/settings_access_reset', 'init'); + } + // Scope MeetLet admin CSS under this root (Marketplace CSS guideline). + // dir attribute is not processed by Moodle RTLCSS (unlike styles.css values). + $attrs = right_to_left() ? ['dir' => 'rtl'] : []; + return \html_writer::div( + $this->herohtml . parent::output_html(), + 'meetlet-admin-settings', + $attrs + ); } } diff --git a/classes/api/client.php b/classes/api/client.php index efaba67..0182b7b 100644 --- a/classes/api/client.php +++ b/classes/api/client.php @@ -213,7 +213,7 @@ class client { curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']); } if ($this->https) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); } if (!empty($CFG->proxyhost)) { @@ -363,7 +363,7 @@ class client { curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']); } if ($this->https) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); } if (!empty($CFG->proxyhost)) { diff --git a/classes/api/service.php b/classes/api/service.php index be1e5e1..112be48 100644 --- a/classes/api/service.php +++ b/classes/api/service.php @@ -1386,7 +1386,8 @@ class service { CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_FOLLOWLOCATION => true, - CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_TIMEOUT => 12, CURLOPT_COOKIE => $cookie !== '' ? ('BREEZESESSION=' . $cookie) : '', ]); diff --git a/classes/external/registration_action.php b/classes/external/registration_action.php new file mode 100644 index 0000000..7d9b76e --- /dev/null +++ b/classes/external/registration_action.php @@ -0,0 +1,124 @@ +. + +/** + * External: activate / recheck / deactivate MeetLet site registration. + * + * @package mod_meetletacbfree + * @copyright 2026 MeetLet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_meetletacbfree\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_value; +use mod_meetletacbfree\registration\error_mapper; +use mod_meetletacbfree\registration\manager; +use mod_meetletacbfree\registration\registration_exception; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Ajax-capable registration actions for admin settings. + */ +class registration_action extends external_api { + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'action' => new external_value(PARAM_ALPHA, 'activate|recheck|deactivate|status'), + 'registration_api_url' => new external_value(PARAM_URL, 'Optional API base URL', VALUE_DEFAULT, ''), + ]); + } + + public static function execute(string $action, string $registrationapiurl = ''): array { + $params = self::validate_parameters(self::execute_parameters(), [ + 'action' => $action, + 'registration_api_url' => $registrationapiurl, + ]); + + $context = \context_system::instance(); + self::validate_context($context); + require_capability('moodle/site:config', $context); + + try { + if ($params['registration_api_url'] !== '') { + set_config(manager::CONFIG_API_URL, rtrim($params['registration_api_url'], '/'), 'meetletacbfree'); + } + + $message = ''; + if ($params['action'] === 'activate') { + manager::activate(); + $message = get_string('registration_activated', 'meetletacbfree'); + } else if (in_array($params['action'], ['recheck', 'refresh', 'validate'], true)) { + manager::recheck(true); + $message = get_string('registration_rechecked', 'meetletacbfree'); + } else if ($params['action'] === 'deactivate') { + manager::deactivate(); + $message = get_string('registration_deactivated', 'meetletacbfree'); + } else if ($params['action'] === 'status') { + $message = ''; + } else { + throw new \moodle_exception('invalidparameter'); + } + + return self::pack_response(true, $message, ''); + } catch (\Throwable $e) { + $ex = registration_exception::from_throwable($e); + $mapped = error_mapper::from_exception($ex); + return self::pack_response( + false, + '', + (string)($mapped['message'] ?? $ex->get_api_message()) + ); + } + } + + /** + * @return array + */ + protected static function pack_response(bool $success, string $message, string $error): array { + $summary = manager::status_summary(); + return [ + 'success' => $success, + 'message' => $message, + 'error' => $error, + 'active' => !empty($summary['active']), + 'status' => (string)($summary['status'] ?? ''), + 'status_label' => (string)($summary['status_label'] ?? ''), + 'product_label' => (string)($summary['product_label'] ?? ''), + 'domain' => (string)($summary['domain'] ?? ''), + 'installation_id' => (string)($summary['installation_id'] ?? ''), + 'can_deactivate' => !empty($summary['can_deactivate']), + ]; + } + + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'success' => new external_value(PARAM_BOOL, 'Whether the action succeeded'), + 'message' => new external_value(PARAM_TEXT, 'Success message'), + 'error' => new external_value(PARAM_TEXT, 'Error message when unsuccessful'), + 'active' => new external_value(PARAM_BOOL, 'Whether activation is active'), + 'status' => new external_value(PARAM_ALPHANUMEXT, 'Raw status code'), + 'status_label' => new external_value(PARAM_TEXT, 'Human status label'), + 'product_label' => new external_value(PARAM_TEXT, 'Product label'), + 'domain' => new external_value(PARAM_TEXT, 'Site domain'), + 'installation_id' => new external_value(PARAM_TEXT, 'Installation id'), + 'can_deactivate' => new external_value(PARAM_BOOL, 'Whether deactivate is allowed'), + ]); + } +} diff --git a/classes/external/reset_access_settings.php b/classes/external/reset_access_settings.php new file mode 100644 index 0000000..cb6e2e5 --- /dev/null +++ b/classes/external/reset_access_settings.php @@ -0,0 +1,89 @@ +. + +/** + * External: reset site role mapping or feature-access matrix to defaults. + * + * @package mod_meetletacbfree + * @copyright 2026 MeetLet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_meetletacbfree\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_value; +use mod_meetletacbfree\local\featureaccess; +use mod_meetletacbfree\local\roles; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Ajax-capable reset of access settings defaults. + */ +class reset_access_settings extends external_api { + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'section' => new external_value(PARAM_ALPHA, 'Section: rolemapping or featureaccess'), + ]); + } + + public static function execute(string $section): array { + $params = self::validate_parameters(self::execute_parameters(), ['section' => $section]); + $context = \context_system::instance(); + self::validate_context($context); + require_capability('moodle/site:config', $context); + + \mod_meetletacbfree\registration\manager::require_registered(); + + if ($params['section'] === 'rolemapping') { + $rules = roles::builtin_site_defaults(); + $json = json_encode($rules, JSON_UNESCAPED_UNICODE); + set_config('default_participants', $json, 'meetletacbfree'); + return [ + 'success' => true, + 'section' => 'rolemapping', + 'matrixjson' => json_encode(roles::matrix_from_rules($rules), JSON_UNESCAPED_UNICODE), + 'message' => get_string('reset_success_rolemapping', 'meetletacbfree'), + ]; + } + + if ($params['section'] === 'featureaccess') { + $matrix = featureaccess::builtin_defaults(); + $json = json_encode($matrix, JSON_UNESCAPED_UNICODE); + set_config('featureaccess', $json, 'meetletacbfree'); + return [ + 'success' => true, + 'section' => 'featureaccess', + 'matrixjson' => json_encode($matrix, JSON_UNESCAPED_UNICODE), + 'message' => get_string('reset_success_featureaccess', 'meetletacbfree'), + ]; + } + + throw new \moodle_exception('invalidparameter'); + } + + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'success' => new external_value(PARAM_BOOL, 'Whether reset succeeded'), + 'section' => new external_value(PARAM_ALPHA, 'Section that was reset'), + 'matrixjson' => new external_value(PARAM_RAW, 'JSON-encoded matrix payload'), + 'message' => new external_value(PARAM_TEXT, 'Success message'), + ]); + } +} diff --git a/classes/external/run_conntest_step.php b/classes/external/run_conntest_step.php new file mode 100644 index 0000000..c6c2aba --- /dev/null +++ b/classes/external/run_conntest_step.php @@ -0,0 +1,97 @@ +. + +/** + * External: run one Adobe Connect connection-test step. + * + * @package mod_meetletacbfree + * @copyright 2026 MeetLet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_meetletacbfree\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_multiple_structure; +use core_external\external_single_structure; +use core_external\external_value; +use mod_meetletacbfree\local\conntest_runner; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Ajax-capable connection test step runner for admin settings. + */ +class run_conntest_step extends external_api { + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'step' => new external_value(PARAM_ALPHA, 'Connection test step name'), + ]); + } + + public static function execute(string $step): array { + $params = self::validate_parameters(self::execute_parameters(), ['step' => $step]); + $context = \context_system::instance(); + self::validate_context($context); + require_capability('moodle/site:config', $context); + + \mod_meetletacbfree\registration\manager::require_registered(); + + $result = conntest_runner::run_step($params['step']); + $subresults = []; + if (!empty($result['subresults']) && is_array($result['subresults'])) { + foreach ($result['subresults'] as $sub) { + $subresults[] = [ + 'label' => (string)($sub['label'] ?? ''), + 'ok' => !empty($sub['ok']), + 'detail' => (string)($sub['detail'] ?? ''), + ]; + } + } + + return [ + 'step' => (string)($result['step'] ?? $params['step']), + 'label' => (string)($result['label'] ?? ''), + 'ok' => !empty($result['ok']), + 'detail' => (string)($result['detail'] ?? ''), + 'done' => !empty($result['done']), + 'abort' => !empty($result['abort']), + 'subresults' => $subresults, + ]; + } + + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'step' => new external_value(PARAM_ALPHANUMEXT, 'Step id'), + 'label' => new external_value(PARAM_TEXT, 'Step label'), + 'ok' => new external_value(PARAM_BOOL, 'Whether the step passed'), + 'detail' => new external_value(PARAM_RAW, 'Detail message'), + 'done' => new external_value(PARAM_BOOL, 'Whether the run is finished'), + 'abort' => new external_value(PARAM_BOOL, 'Whether remaining steps should abort'), + 'subresults' => new external_multiple_structure( + new external_single_structure([ + 'label' => new external_value(PARAM_TEXT, 'Sub-step label'), + 'ok' => new external_value(PARAM_BOOL, 'Sub-step ok'), + 'detail' => new external_value(PARAM_RAW, 'Sub-step detail'), + ]), + 'Optional nested results', + VALUE_DEFAULT, + [] + ), + ]); + } +} diff --git a/classes/local/content.php b/classes/local/content.php index bdc1976..9c605bb 100644 --- a/classes/local/content.php +++ b/classes/local/content.php @@ -161,7 +161,8 @@ class content { curl_setopt_array($ch, [ CURLOPT_FILE => $fp, CURLOPT_FOLLOWLOCATION => true, - CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_TIMEOUT => 300, CURLOPT_COOKIE => $session !== '' ? ('BREEZESESSION=' . $session) : '', ]); diff --git a/classes/registration/registration_exception.php b/classes/registration/registration_exception.php index 39d19bc..8e4ba1b 100644 --- a/classes/registration/registration_exception.php +++ b/classes/registration/registration_exception.php @@ -1,11 +1,24 @@ . /** * Structured registration API / local client exception. * * @package mod_meetletacbfree - * @copyright 2026 MeetLet + * @copyright 2026 MeetLet * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/conntest.php b/conntest.php index a62a78a..18bf154 100644 --- a/conntest.php +++ b/conntest.php @@ -1,5 +1,26 @@ . + +/** + * Standalone Adobe Connect connection test page. + * + * @package mod_meetletacbfree + * @copyright 2026 MeetLet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ require_once('../../config.php'); @@ -15,7 +36,6 @@ $PAGE->set_context(context_system::instance()); $PAGE->set_url(new moodle_url('/mod/meetletacbfree/conntest.php')); $PAGE->set_title(get_string('conntesttitle', 'meetletacbfree')); $PAGE->set_pagelayout('admin'); -$PAGE->requires->css(new moodle_url('/mod/meetletacbfree/styles.css', ['v' => '2026081415'])); $results = []; $push = static function(string $label, bool $ok, string $detail = '') use (&$results): void { diff --git a/db/services.php b/db/services.php index 1a3a66d..cf96d9d 100644 --- a/db/services.php +++ b/db/services.php @@ -81,4 +81,28 @@ $functions = [ 'ajax' => true, 'capabilities' => 'mod/meetletacbfree:host', ], + 'mod_meetletacbfree_run_conntest_step' => [ + 'classname' => 'mod_meetletacbfree\external\run_conntest_step', + 'methodname' => 'execute', + 'description' => 'Run one Adobe Connect connection-test step (admin)', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'moodle/site:config', + ], + 'mod_meetletacbfree_reset_access_settings' => [ + 'classname' => 'mod_meetletacbfree\external\reset_access_settings', + 'methodname' => 'execute', + 'description' => 'Reset site role mapping or feature-access matrix to defaults (admin)', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'moodle/site:config', + ], + 'mod_meetletacbfree_registration_action' => [ + 'classname' => 'mod_meetletacbfree\external\registration_action', + 'methodname' => 'execute', + 'description' => 'Activate, recheck, or deactivate MeetLet plugin registration (admin)', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'moodle/site:config', + ], ]; diff --git a/lang/en/meetletacbfree.php b/lang/en/meetletacbfree.php index 8322233..4c37267 100644 --- a/lang/en/meetletacbfree.php +++ b/lang/en/meetletacbfree.php @@ -176,6 +176,9 @@ $string['testsso'] = 'Single sign-on (SSO)'; $string['testsso_adminok'] = 'Admin SSO succeeded'; $string['testsso_userok'] = 'Test-user SSO succeeded'; $string['testsso_userwarn'] = 'Test-user SSO warned (admin SSO infrastructure is healthy)'; +$string['testsso_direct_ok'] = 'Admin SSO from Moodle server succeeded'; +$string['testsso_direct_fail'] = 'Direct HTTP SSO from Moodle failed — {$a}'; +$string['testmeeting'] = 'Create and remove test meeting'; $string['tabinfo'] = 'Overview'; $string['tabdetails'] = 'Session details'; @@ -334,6 +337,7 @@ $string['emptytab'] = 'Nothing to show'; $string['cachedef_folders'] = 'Adobe Connect folder sco-ids'; $string['cachedef_meetinginfo'] = 'Adobe Connect meeting info'; $string['cachedef_templates'] = 'Adobe Connect meeting templates'; +$string['cachedef_license_webhook_nonces'] = 'MeetLet license webhook nonce cache'; $string['tasksync'] = 'Sync MeetLet recordings'; $string['brand'] = 'MeetLet'; $string['subtitle'] = 'Adobe Connect'; @@ -352,6 +356,10 @@ $string['enable_contenttab_desc'] = 'Master switch for the content tab. Roles st $string['allow_override_contenttab'] = 'Allow per-activity content tab setting'; $string['allow_override_contenttab_desc'] = 'Teachers may enable/disable the content tab for each activity.'; $string['showrecordingstab'] = 'Show recordings tab'; +$string['offlinesettings'] = 'Offline session video'; +$string['autooffline'] = 'Automatically create offline video after the Adobe Connect class ends'; +$string['autooffline_help'] = 'When this class’s Adobe Connect room had people in it and then becomes empty, recordings are sent for MP4 creation. This is independent of the optional Moodle end time (which only blocks joining).'; +$string['showzip_help'] = 'When ZIP links are enabled in site settings, use this to turn the ZIP link on or off for this activity only.'; $string['showcontenttab'] = 'Show content tab'; $string['activityfeatures'] = 'Links and tabs visibility'; $string['featuredisabled'] = 'This section has been disabled by the site administrator.'; @@ -551,6 +559,7 @@ $string['registration_err_misconfigured'] = 'Activation request misconfigured.'; $string['registration_err_bad_signature'] = 'Invalid authorization signature from MeetLet.'; $string['registration_err_network'] = 'Could not reach the MeetLet activation server.'; $string['registration_err_unknown'] = 'An unknown activation error occurred.'; +$string['registration_api_error'] = 'MeetLet activation API error: {$a}'; $string['registration_crypto_error'] = 'Activation cryptography error.'; $string['registration_sodium_required'] = 'PHP sodium extension is required for plugin activation.'; $string['task_registration_heartbeat'] = 'MeetLet plugin activation heartbeat'; diff --git a/lang/fa/meetletacbfree.php b/lang/fa/meetletacbfree.php index b4da938..8ec92bb 100644 --- a/lang/fa/meetletacbfree.php +++ b/lang/fa/meetletacbfree.php @@ -176,6 +176,9 @@ $string['testsso'] = 'ورود یکپارچه (SSO)'; $string['testsso_adminok'] = 'SSO مدیر موفق بود'; $string['testsso_userok'] = 'SSO کاربر آزمایشی موفق بود'; $string['testsso_userwarn'] = 'SSO کاربر آزمایشی هشدار داد (زیرساخت SSO مدیر سالم است)'; +$string['testsso_direct_ok'] = 'SSO مدیر از سرور مودل موفق بود'; +$string['testsso_direct_fail'] = 'SSO مستقیم HTTP از مودل ناموفق بود — {$a}'; +$string['testmeeting'] = 'ایجاد و حذف جلسه آزمایشی'; $string['tabinfo'] = 'نمای کلی'; $string['tabdetails'] = 'توضیحات جلسه'; @@ -336,6 +339,7 @@ $string['emptytab'] = 'موردی برای نمایش نیست'; $string['cachedef_folders'] = 'شناسه پوشه‌های Adobe Connect'; $string['cachedef_meetinginfo'] = 'اطلاعات جلسه Adobe Connect'; $string['cachedef_templates'] = 'قالب‌های جلسه Adobe Connect'; +$string['cachedef_license_webhook_nonces'] = 'کش nonce وب‌هوک لایسنس MeetLet'; $string['tasksync'] = 'همگام‌سازی ضبط‌های MeetLet'; $string['brand'] = 'MeetLet'; $string['subtitle'] = 'Adobe Connect'; @@ -354,6 +358,10 @@ $string['enable_contenttab_desc'] = 'کلید اصلی تب محتوا. نقش $string['allow_override_contenttab'] = 'اجازه تنظیم تب محتوا در هر فعالیت'; $string['allow_override_contenttab_desc'] = 'مدرس بتواند برای هر جلسه نمایش تب محتوا را جداگانه کنترل کند.'; $string['showrecordingstab'] = 'نمایش تب ضبط‌ها'; +$string['offlinesettings'] = 'ویدیوی آفلاین جلسه'; +$string['autooffline'] = 'ساخت خودکار ویدیوی آفلاین پس از پایان کلاس Adobe Connect'; +$string['autooffline_help'] = 'وقتی اتاق Adobe Connect این کلاس خالی شود (پس از حضور شرکت‌کنندگان)، ضبط‌ها برای ساخت MP4 ارسال می‌شوند. این مستقل از زمان پایان اختیاری در مودل است.'; +$string['showzip_help'] = 'اگر لینک ZIP در تنظیمات سایت فعال باشد، با این گزینه می‌توانید لینک ZIP را فقط برای این فعالیت روشن یا خاموش کنید.'; $string['showcontenttab'] = 'نمایش تب محتوا'; $string['activityfeatures'] = 'نمایش لینک‌ها و تب‌ها'; $string['featuredisabled'] = 'این بخش توسط مدیر سایت غیرفعال شده است.'; @@ -553,6 +561,7 @@ $string['registration_err_misconfigured'] = 'درخواست فعال‌سازی $string['registration_err_bad_signature'] = 'امضای مجوز از MeetLet نامعتبر است.'; $string['registration_err_network'] = 'اتصال به سرور فعال‌سازی MeetLet برقرار نشد.'; $string['registration_err_unknown'] = 'خطای ناشناخته در فعال‌سازی رخ داد.'; +$string['registration_api_error'] = 'خطای API فعال‌سازی MeetLet: {$a}'; $string['registration_crypto_error'] = 'خطای رمزنگاری فعال‌سازی.'; $string['registration_sodium_required'] = 'افزونه sodium در PHP برای فعال‌سازی پلاگین لازم است.'; $string['task_registration_heartbeat'] = 'بررسی روزانه فعال‌سازی پلاگین MeetLet'; diff --git a/mod_form.php b/mod_form.php index 430960c..df50936 100644 --- a/mod_form.php +++ b/mod_form.php @@ -1,5 +1,27 @@ . + +/** + * 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'); @@ -12,10 +34,6 @@ use mod_meetletacbfree\registration\manager as registration_manager; /** * MeetLet mod form. - * - * @package mod_meetletacbfree - * @copyright 2026 MeetLet - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_meetletacbfree_mod_form extends moodleform_mod { public function definition() { @@ -34,10 +52,9 @@ class mod_meetletacbfree_mod_form extends moodleform_mod { return; } - $PAGE->requires->js(new moodle_url('/mod/meetletacbfree/javascript/modform.js')); - $PAGE->requires->js(new moodle_url('/mod/meetletacbfree/javascript/featureaccess_matrix.js')); - $PAGE->requires->js(new moodle_url('/mod/meetletacbfree/javascript/settings_toggles.js')); - $PAGE->requires->css(new moodle_url('/mod/meetletacbfree/styles.css', ['v' => '2026081913'])); + $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']); diff --git a/settings.php b/settings.php index b1dbbb3..74319a5 100644 --- a/settings.php +++ b/settings.php @@ -46,620 +46,6 @@ if ($ADMIN->fulltree) { ); $logo = $OUTPUT->image_url('adobeconnect', 'meetletacbfree'); - $stylesurl = new moodle_url('/mod/meetletacbfree/styles.css', ['v' => '2026090146']); - $jsurl = new moodle_url('/mod/meetletacbfree/javascript/settings_conntest.js', ['v' => '2026082135']); - $togglejsurl = new moodle_url('/mod/meetletacbfree/javascript/settings_toggles.js', ['v' => '2026081907']); - $matrixjsurl = new moodle_url('/mod/meetletacbfree/javascript/featureaccess_matrix.js'); - $accessresetjsurl = new moodle_url('/mod/meetletacbfree/javascript/settings_access_reset.js', ['v' => '2026090115']); - $resetajaxurl = new moodle_url('/mod/meetletacbfree/ajax/settings_reset.php'); - $modalscrolljsurl = new moodle_url('/mod/meetletacbfree/javascript/meetlet_modal_scroll.js', ['v' => '2026082239']); - $registrationjsurl = new moodle_url('/mod/meetletacbfree/javascript/settings_registration.js', ['v' => '2026090144']); - $cronjsurl = null; - $ajaxurl = new moodle_url('/mod/meetletacbfree/ajax/conntest.php'); - - // Load styles.css explicitly on admin tabs (theme concatenate alone is not enough for switch CSS). - $styles = ' - - - - - - - -'; $plugin = new stdClass(); include(__DIR__ . '/version.php'); @@ -673,7 +59,7 @@ html[dir="rtl"] .path-admin #adminsettings .form-buttons { $versionbadge = $release !== '' ? html_writer::span(get_string('settingsversion', 'meetletacbfree', $release), 'meetlet-settings-version') : ''; - $hero = $styles . html_writer::div( + $hero = html_writer::div( html_writer::img($logo, 'Adobe Connect', ['alt' => 'Adobe Connect']) . html_writer::div( html_writer::tag('h3', get_string('pluginname', 'meetletacbfree')) . @@ -828,8 +214,6 @@ html[dir="rtl"] .path-admin #adminsettings .form-buttons { 'meetlet-conntest', [ 'id' => 'meetlet-acb-conntest', - 'data-endpoint' => $ajaxurl->out(false), - 'data-sesskey' => sesskey(), 'data-steps' => implode(',', conntest_runner::STEPS), 'data-step-labels' => json_encode($steplabels, JSON_UNESCAPED_UNICODE), 'data-str-ok' => get_string('testok', 'meetletacbfree'), @@ -923,7 +307,7 @@ html[dir="rtl"] .path-admin #adminsettings .form-buttons { $page->add(new admin_setting_heading( 'meetletacbfree/accessresetassets', '', - \mod_meetletacbfree\admin\access_reset_ui::page_assets($resetajaxurl) + \mod_meetletacbfree\admin\access_reset_ui::page_assets() )); $settings->add($page); diff --git a/styles.css b/styles.css index bef9d05..a5372c5 100644 --- a/styles.css +++ b/styles.css @@ -1185,11 +1185,13 @@ background: #c0392b; border-color: #c0392b; } +/* rtl:begin:ignore */ html[dir="rtl"] .meetlet-modal--confirm .meetlet-modal__title, html[dir="rtl"] .meetlet-modal--confirm .meetlet-modal__body, html[dir="rtl"] .meetlet-modal--confirm .meetlet-modal__lead { text-align: right; } +/* rtl:end:ignore */ /* Marker class on html; lock scroll on body only — overflow+height on html resets scroll position. */ html.meetlet-modal-open body { overflow: hidden; @@ -1349,7 +1351,7 @@ html.meetlet-modal-open body { transition: background .18s ease; overflow: hidden; box-sizing: border-box; - direction: ltr; /* Keep thumb travel physical LTR; icons stay readable. */ + /* rtl:ignore */ direction: ltr; /* Keep thumb travel physical LTR; icons stay readable. */ } .meetlet-switch__thumb { position: absolute; @@ -1405,20 +1407,20 @@ html.meetlet-modal-open body { .meetlet-access-cell .meetlet-switch { justify-content: center; } -.path-admin #adminsettings .form-item:has(.meetlet-switch) { +.meetlet-admin-settings .form-item:has(.meetlet-switch) { align-items: flex-start; } -.path-admin #adminsettings .form-item:has(.meetlet-switch) > .form-setting { +.meetlet-admin-settings .form-item:has(.meetlet-switch) > .form-setting { display: flex; flex-direction: column; /* Toward the label column: left in LTR, right in RTL. */ align-items: start; } -.path-admin #adminsettings .form-item:has(.meetlet-switch) > .form-label, -.path-admin #adminsettings .form-item:has(.meetlet-switch) > .form-setting { +.meetlet-admin-settings .form-item:has(.meetlet-switch) > .form-label, +.meetlet-admin-settings .form-item:has(.meetlet-switch) > .form-setting { padding-top: .4rem; } -.path-admin #adminsettings .form-item:has(.meetlet-switch) .form-label > label { +.meetlet-admin-settings .form-item:has(.meetlet-switch) .form-label > label { margin: 0; padding: 0; line-height: 22px; @@ -1426,9 +1428,9 @@ html.meetlet-modal-open body { align-items: center; min-height: 22px; } -.path-admin #adminsettings .form-item:has(.meetlet-switch) .form-check, -.path-admin #adminsettings .form-item:has(.meetlet-switch) .checkbox, -.path-admin #adminsettings .form-item:has(.meetlet-switch) .form-checkbox { +.meetlet-admin-settings .form-item:has(.meetlet-switch) .form-check, +.meetlet-admin-settings .form-item:has(.meetlet-switch) .checkbox, +.meetlet-admin-settings .form-item:has(.meetlet-switch) .form-checkbox { display: inline-flex; flex-direction: row; align-items: center; @@ -1441,14 +1443,16 @@ html.meetlet-modal-open body { width: auto; max-width: 100%; } -html[dir="rtl"] .path-admin #adminsettings .form-item:has(.meetlet-switch) .form-check, -html[dir="rtl"] .path-admin #adminsettings .form-item:has(.meetlet-switch) .checkbox, -html[dir="rtl"] .path-admin #adminsettings .form-item:has(.meetlet-switch) .form-checkbox { +/* rtl:begin:ignore */ +html[dir="rtl"] .meetlet-admin-settings .form-item:has(.meetlet-switch) .form-check, +html[dir="rtl"] .meetlet-admin-settings .form-item:has(.meetlet-switch) .checkbox, +html[dir="rtl"] .meetlet-admin-settings .form-item:has(.meetlet-switch) .form-checkbox { /* Keep switch on the label side (visual right), same as pre-compact layout. */ flex-direction: row-reverse; justify-content: flex-end; text-align: right; } +/* rtl:end:ignore */ /* Site admin: Moodle role → Adobe Connect role mapping (chip radios). */ .meetlet-rolemapping-table .meetlet-rolemapping-cell { @@ -1637,19 +1641,19 @@ html[dir="rtl"] .path-admin #adminsettings .form-item:has(.meetlet-switch) .form } /* Cron & video-delivery dashboard (plugin settings) */ -.path-admin #adminsettings .form-item:has(#meetlet-cronmon) { +.meetlet-admin-settings .form-item:has(#meetlet-cronmon) { display: block; padding: 0; border: 0; background: transparent; } -.path-admin #adminsettings .form-item:has(#meetlet-cronmon) > .form-label, -.path-admin #adminsettings .form-item:has(#meetlet-cronmon) .form-description, -.path-admin #adminsettings .form-item:has(#meetlet-cronmon) .form-defaultinfo, -.path-admin #adminsettings .form-item:has(#meetlet-cronmon) .form-setting > .form-shortname { +.meetlet-admin-settings .form-item:has(#meetlet-cronmon) > .form-label, +.meetlet-admin-settings .form-item:has(#meetlet-cronmon) .form-description, +.meetlet-admin-settings .form-item:has(#meetlet-cronmon) .form-defaultinfo, +.meetlet-admin-settings .form-item:has(#meetlet-cronmon) .form-setting > .form-shortname { display: none !important; } -.path-admin #adminsettings .form-item:has(#meetlet-cronmon) > .form-setting { +.meetlet-admin-settings .form-item:has(#meetlet-cronmon) > .form-setting { max-width: none; width: 100%; float: none; @@ -2049,20 +2053,26 @@ html[dir="rtl"] .path-admin #adminsettings .form-item:has(.meetlet-switch) .form } } -html[dir="rtl"] .path-admin #adminsettings #id_s_meetletacbfree_agent_dispatch, -html[dir="rtl"] .path-admin #adminsettings select[name="s_meetletacbfree_agent_dispatch"] { +/* rtl:begin:ignore */ +html[dir="rtl"] .meetlet-admin-settings #id_s_meetletacbfree_agent_dispatch, +html[dir="rtl"] .meetlet-admin-settings select[name="s_meetletacbfree_agent_dispatch"] { direction: rtl; text-align: right; } -html[dir="rtl"] .path-admin #adminsettings #id_s_meetletacbfree_agent_dispatch option, -html[dir="rtl"] .path-admin #adminsettings select[name="s_meetletacbfree_agent_dispatch"] option { +/* rtl:end:ignore */ +/* rtl:begin:ignore */ +html[dir="rtl"] .meetlet-admin-settings #id_s_meetletacbfree_agent_dispatch option, +html[dir="rtl"] .meetlet-admin-settings select[name="s_meetletacbfree_agent_dispatch"] option { direction: rtl; text-align: right; } +/* rtl:end:ignore */ +/* rtl:begin:ignore */ html[dir="rtl"] .path-admin .meetlet-hourpicker, html[dir="rtl"] .path-admin .meetlet-hourpresets { direction: rtl; } +/* rtl:end:ignore */ .meetlet-gate-msg { white-space: pre-wrap; @@ -2200,3 +2210,508 @@ html[dir="rtl"] .path-admin .meetlet-hourpresets { .activityiconcontainer .activityicon[src*="mod/meetletacbfree"] { filter: none !important; } + +/* ===== MeetLet admin settings (scoped under .meetlet-admin-settings) ===== */ +.meetlet-admin-settings .settingsform > h2 { + display: none; +} +.meetlet-admin-settings .meetlet-settings-hero { + display:flex; gap:1rem; align-items:center; + padding:1.15rem 1.25rem; margin:0 0 1rem; + border-radius:16px; + background: + radial-gradient(700px 180px at 100% 0%, rgba(82,204,163,.22), transparent 55%), + linear-gradient(135deg, #00261a 0%, #0a3d2e 60%, #145c45 100%); + color:#fff; + border:1px solid rgba(82,204,163,.25); +} +.meetlet-admin-settings .meetlet-settings-hero img { + width:56px; height:56px; object-fit:contain; + background:rgba(255,255,255,.95); border-radius:12px; padding:6px; +} +.meetlet-admin-settings .meetlet-settings-hero h3 { + margin:0 0 .2rem; font-size:1.2rem; font-weight:700; color:#fff; + display:flex; flex-wrap:wrap; align-items:center; gap:.45rem .55rem; +} +/* rtl:begin:ignore */ +.meetlet-admin-settings .meetlet-settings-hero .meetlet-settings-version { + display:inline-block; + flex:0 0 auto; + align-self:center; + margin-inline-start:auto; + padding:.12rem .55rem; + font-size:.72rem; font-weight:650; line-height:1.35; + letter-spacing:.02em; + border-radius:999px; + background:rgba(82,204,163,.18); + border:1px solid rgba(82,204,163,.35); + color:#b8f5df; + white-space:nowrap; + direction:ltr; + unicode-bidi:isolate; +} +/* rtl:end:ignore */ +.meetlet-admin-settings .meetlet-settings-hero p { + margin:0; opacity:.85; color:#e6faf3; + white-space:nowrap; + max-width:none; +} +.meetlet-admin-settings .meetlet-settings-copy { + min-width:0; flex:1 1 auto; +} +@media (max-width: 900px) { + .meetlet-admin-settings .meetlet-settings-hero p { + white-space:normal; + } +} +.meetlet-admin-settings .nav-tabs { + border-bottom:2px solid #cfe8de; + gap:.2rem; +} +.meetlet-admin-settings .nav-tabs .nav-link { + border:0 !important; + border-radius:6px !important; + padding:.45rem .9rem; + color:#00261a; + font-weight:650; + box-shadow:none !important; + outline:none !important; +} +.meetlet-admin-settings .nav-tabs .nav-link:hover { + background:#e6faf3; color:#00261a; +} +.meetlet-admin-settings .nav-tabs .nav-link.active { + background:#52cca3; color:#00261a; +} +.meetlet-admin-settings .nav-tabs .nav-link:focus, +.meetlet-admin-settings .nav-tabs .nav-link:focus-visible, +.meetlet-admin-settings .nav-tabs .nav-link:active { + outline:none !important; + box-shadow:none !important; + border:0 !important; +} +.meetlet-admin-settings .meetlet-feature-group { + margin:1rem 0 .35rem; + padding:1rem 1.1rem .35rem; + border:1px solid #cfe8de; + border-radius:12px; + background:linear-gradient(180deg, #f3fbf8 0%, #ffffff 55%); +} +.meetlet-admin-settings .meetlet-feature-group h4 { + margin:0 0 .25rem; color:#00261a; font-size:1.02rem; font-weight:700; +} +.meetlet-admin-settings .meetlet-feature-group p { + margin:0 0 .5rem; color:#4a6b5f; font-size:.9rem; +} +.meetlet-admin-settings .meetlet-settings-card { + background:#fff; border:1px solid #cfe8de; border-radius:14px; + padding:1rem 1.15rem; margin:.35rem 0 1rem; +} +.meetlet-admin-settings .meetlet-license-badge { + font-weight: 700; + border-radius: 999px; + padding: .35rem .65rem; +} +.meetlet-admin-settings .meetlet-license-badge--active { + background: var(--ml-green, #52cca3); + color: var(--ml-green-deep, #00261a); +} +.meetlet-admin-settings .meetlet-license-badge--inactive { + background: #fff4d6; + color: #5a4700; +} +.meetlet-admin-settings #meetlet-license-modal.meetlet-modal, +.meetlet-admin-settings #meetlet-registration-modal.meetlet-modal, +.meetlet-admin-settings #meetlet-cronmon-modal.meetlet-modal, +.meetlet-admin-settings #meetlet-access-reset-modal.meetlet-modal { + z-index: 20000 !important; +} +.meetlet-admin-settings .meetlet-access-reset-toolbar { + display: flex; + justify-content: flex-end; + margin: 0 0 .65rem; +} +/* Cron videos ops cell: no extra cell/wrapper border (icon buttons keep theirs). */ +.meetlet-admin-settings .meetlet-cronmon__table td.meetlet-cronmon__dl { + border-left: 0 !important; + border-right: 0 !important; + border-top: 0 !important; + background: transparent !important; + box-shadow: none !important; +} +.meetlet-admin-settings .meetlet-cronmon__dl .meetlet-iconactions { + border: 0 !important; + background: transparent !important; + box-shadow: none !important; + padding: 0 !important; + margin: 0 !important; +} +.meetlet-admin-settings .meetlet-modal--confirm .meetlet-modal__dialog { + width: min(480px, 100%); + max-height: min(86vh, 640px); + overflow: hidden; + display: flex; + flex-direction: column; +} +.meetlet-admin-settings .meetlet-modal--confirm .meetlet-modal__head { + padding: 1.2rem 1.15rem 1rem; + border-radius: 18px 18px 0 0; +} +.meetlet-admin-settings .meetlet-modal__lead { + margin: 0 0 .85rem; + font-size: 1rem; + font-weight: 650; + line-height: 1.65; + color: #00261a; +} +.meetlet-admin-settings .meetlet-modal__callout { + margin: 0 0 .9rem; + padding: .85rem 1rem .7rem; + border-radius: 12px; + border: 1px solid #e6c56a; + background: #fff8e6; + color: #5a4700; +} +.meetlet-admin-settings .meetlet-modal__callout-title { + margin: 0 0 .4rem; + font-weight: 700; +} +.meetlet-admin-settings .meetlet-modal__limits { + margin: 0 0 .55rem; + padding-inline-start: 1.2rem; +} +.meetlet-admin-settings .meetlet-modal__keep { + margin: 0; + color: #334942; + line-height: 1.65; +} +.meetlet-admin-settings .meetlet-modal__foot { + display: flex; + justify-content: flex-end; + align-items: center; + gap: .6rem; + flex-wrap: wrap; + padding: .9rem 1.15rem 1.05rem; + border-top: 1px solid #cfe8de; + background: linear-gradient(180deg, #f7fcfa 0%, #ffffff 70%); +} +.meetlet-admin-settings .meetlet-modal__foot .btn { + border-radius: 10px; + font-weight: 700; + padding: .5rem 1.15rem; +} +.meetlet-admin-settings .meetlet-modal__foot .btn-success { + background: #52cca3 !important; + border-color: #3db892 !important; + color: #00261a !important; +} +.meetlet-admin-settings .meetlet-modal__foot .btn-outline-danger { + color: #a33b3b !important; + border-color: #d9a0a0 !important; + background: #fff !important; +} +.meetlet-admin-settings .meetlet-modal__foot .btn-outline-danger:hover, +.meetlet-admin-settings .meetlet-modal__foot .btn-outline-danger:focus { + color: #fff !important; + background: #c0392b !important; + border-color: #c0392b !important; +} +.meetlet-admin-settings .meetlet-about { + display:grid; gap:1.1rem; +} +.meetlet-admin-settings .meetlet-about__brand { + display:flex; justify-content:center; align-items:center; + padding:1.35rem 1.2rem; + border-radius:14px; + background:#f0f2f4; + border:1px solid #e2e6ea; +} +.meetlet-admin-settings .meetlet-about__brand img { + max-width:240px; width:100%; height:auto; display:block; margin:0 auto; +} +.meetlet-admin-settings .meetlet-about h4 { margin:0 0 .55rem; color:#00261a; } +.meetlet-admin-settings .meetlet-about p { + margin:0 0 .85rem; color:#334942; line-height:1.75; text-align:start; +} +.meetlet-admin-settings .meetlet-guide { display:grid; gap:1rem; } +.meetlet-admin-settings .meetlet-guide__heading { margin:0; color:#00261a; font-size:1.15rem; } +.meetlet-admin-settings .meetlet-guide__intro { + margin:0; color:#334942; line-height:1.7; +} +.meetlet-admin-settings .meetlet-guide__callout { + padding:.85rem 1rem; border-radius:10px; line-height:1.65; + border:1px solid #e6c56a; background:#fff8e6; color:#5a4700; +} +.meetlet-admin-settings .meetlet-guide__section { + padding:1rem 1.05rem; border-radius:12px; + border:1px solid #cfe8de; background:#f7fcfa; +} +.meetlet-admin-settings .meetlet-guide__section--current { background:#fff; } +.meetlet-admin-settings .meetlet-guide__section h4 { + margin:0 0 .55rem; color:#00261a; font-size:1.02rem; font-weight:700; +} +.meetlet-admin-settings .meetlet-guide__section h5 { + margin:.85rem 0 .35rem; color:#0a3d2e; font-size:.95rem; font-weight:700; +} +.meetlet-admin-settings .meetlet-guide__body, +.meetlet-admin-settings .meetlet-guide__body p, +.meetlet-admin-settings .meetlet-guide__body li { + color:#334942; line-height:1.7; +} +.meetlet-admin-settings .meetlet-guide__body p { margin:0 0 .55rem; } +.meetlet-admin-settings .meetlet-guide__body ol, +.meetlet-admin-settings .meetlet-guide__body ul { margin:0 0 .35rem; padding-inline-start:1.25rem; } +.meetlet-admin-settings .meetlet-guide__body li { margin:0 0 .35rem; } +.meetlet-admin-settings .meetlet-guide__body code, +.meetlet-admin-settings .meetlet-guide__table code { + font-size:.88em; padding:.05rem .3rem; border-radius:4px; + background:#e6faf3; color:#00261a; +} +.meetlet-admin-settings .meetlet-guide__table { + width:100%; margin:.35rem 0 0; border-collapse:collapse; +} +.meetlet-admin-settings .meetlet-guide__table th, +.meetlet-admin-settings .meetlet-guide__table td { + padding:.45rem .55rem; border:1px solid #cfe8de; vertical-align:top; + text-align:start; +} +.meetlet-admin-settings .meetlet-guide__table thead th, +.meetlet-admin-settings .meetlet-guide__table tbody th { + background:#e6faf3; color:#00261a; font-weight:650; +} +/* rtl:begin:ignore */ +.meetlet-admin-settings .meetlet-guide__code { + margin:.35rem 0 0; padding:.75rem .9rem; overflow-x:auto; + border-radius:10px; border:1px solid #cfe8de; + background:#0a3d2e; color:#e6faf3; + font-size:.82rem; line-height:1.45; direction:ltr; text-align:left; + white-space:pre; +} +/* rtl:end:ignore */ +.meetlet-admin-settings .meetlet-guide__code-cell code { + font-weight:800; letter-spacing:.02em; direction:ltr; unicode-bidi:isolate; +} +.meetlet-admin-settings .meetlet-guide__table td ol, +.meetlet-admin-settings .meetlet-guide__table td ul { + margin:.15rem 0 0; padding-inline-start:1.15rem; +} +.meetlet-admin-settings .meetlet-guide__table td li { margin:0 0 .25rem; } +.meetlet-admin-settings .meetlet-hourpicker { max-width: 42rem; } +.meetlet-admin-settings .meetlet-hourpicker.is-dimmed, +.meetlet-admin-settings .form-item.is-dimmed { + opacity: .45; + pointer-events: none; +} +.meetlet-admin-settings .meetlet-hourpicker__tz { + margin: 0 0 .55rem; color:#4a6b5f; font-size:.88rem; +} +.meetlet-admin-settings .meetlet-hourpresets { + display:flex; flex-wrap:wrap; gap:.4rem; margin:0 0 .65rem; +} +.meetlet-admin-settings .meetlet-hourpreset { + border:1px solid #9fdcc4; background:#fff; color:#00261a; + border-radius:999px; padding:.28rem .7rem; font-size:.8rem; font-weight:650; + cursor:pointer; +} +.meetlet-admin-settings .meetlet-hourpreset:hover { background:#e6faf3; } +.meetlet-admin-settings .meetlet-hourpreset--ghost { border-color:#cfe8de; color:#4a6b5f; } +.meetlet-admin-settings .meetlet-hourgrid { + display:grid; grid-template-columns:repeat(8, minmax(0,1fr)); gap:.35rem; +} +.meetlet-admin-settings .meetlet-hourchip { + border:1px solid #cfe8de; background:#f7fcfa; color:#00261a; + border-radius:10px; padding:.45rem 0; font-size:.82rem; font-weight:750; + cursor:pointer; +} +.meetlet-admin-settings .meetlet-hourchip.is-on { + background:#52cca3; border-color:#0d9f6e; color:#00261a; +} +.meetlet-admin-settings .meetlet-hourchip.is-now { box-shadow:0 0 0 2px rgba(13,159,110,.35); } +@media (max-width: 720px) { + .meetlet-admin-settings .meetlet-hourgrid { grid-template-columns:repeat(6, minmax(0,1fr)); } +} +.meetlet-admin-settings .meetlet-about__cta-wrap { + display:flex; justify-content:center; margin-top:.35rem; padding-top:.35rem; +} +.meetlet-admin-settings .meetlet-about__cta { + display:inline-flex; align-items:center; justify-content:center; + min-width:220px; padding:.7rem 1.35rem; border-radius:10px; + background:#52cca3; color:#00261a !important; + text-decoration:none !important; font-weight:700; + border:1px solid #3db892; + box-shadow:0 6px 16px rgba(0,38,26,.08); +} +.meetlet-admin-settings .meetlet-about__cta:hover { + background:#6fd9b5; color:#00261a !important; +} +.meetlet-admin-settings .meetlet-settings-btn { + display:inline-flex; align-items:center; justify-content:center; gap:.45rem; + padding:.6rem 1.15rem; border-radius:10px; border:0; cursor:pointer; + background:#52cca3; color:#00261a !important; + text-decoration:none !important; font-weight:700; +} +.meetlet-admin-settings .meetlet-settings-btn:hover { background:#6fd9b5; color:#00261a !important; } +.meetlet-admin-settings .meetlet-settings-btn:disabled { opacity:.65; cursor:wait; } +.meetlet-admin-settings .meetlet-settings-btn svg { + width:1.1rem; height:1.1rem; flex:0 0 auto; display:block; +} +.meetlet-admin-settings .meetlet-license-actions { + display:flex; flex-wrap:wrap; gap:.55rem; align-items:center; +} +.meetlet-admin-settings .meetlet-license-actions .btn { + display:inline-flex; align-items:center; justify-content:center; gap:.45rem; +} +.meetlet-admin-settings .meetlet-license-actions .btn svg { + width:1.05rem; height:1.05rem; flex:0 0 auto; display:block; +} +.meetlet-admin-settings .meetlet-license-btn--free { + background: linear-gradient(180deg, #e6faf3 0%, #d4f5ea 100%) !important; + border: 2px solid #52cca3 !important; + color: #00261a !important; + border-radius: 10px !important; + font-weight: 700 !important; + padding: .6rem 1.15rem !important; + box-shadow: 0 4px 12px rgba(82, 204, 163, 0.18) !important; +} +.meetlet-admin-settings .meetlet-license-btn--free:hover, +.meetlet-admin-settings .meetlet-license-btn--free:focus { + background: linear-gradient(180deg, #6fd9b5 0%, #52cca3 100%) !important; + border-color: #3db892 !important; + color: #00261a !important; + box-shadow: 0 6px 16px rgba(82, 204, 163, 0.28) !important; +} +.meetlet-admin-settings .meetlet-conntest { max-width:none; margin:0; } +.meetlet-admin-settings .meetlet-conntest__actions { + display:flex; flex-wrap:wrap; gap:.75rem; align-items:center; + margin:.85rem 0 1rem; +} +.meetlet-admin-settings .meetlet-conntest__status { color:#4a6b5f; font-size:.9rem; } +.meetlet-admin-settings .meetlet-conntest__progress { + margin:0 0 1rem; padding:.85rem 1rem; + border:1px solid #cfe8de; border-radius:12px; background:#f7fcfa; +} +.meetlet-admin-settings .meetlet-conntest__progress-label { + display:flex; justify-content:space-between; gap:.75rem; + margin:0 0 .45rem; color:#00261a; font-weight:650; font-size:.92rem; +} +.meetlet-admin-settings .meetlet-conntest__progress-track { + height:10px; border-radius:999px; background:#d9eee5; overflow:hidden; +} +.meetlet-admin-settings .meetlet-conntest__progress-fill { + height:100%; width:0; border-radius:999px; + background:linear-gradient(90deg, #2f9f7a, #52cca3); + transition:width .28s ease; +} +.meetlet-admin-settings .meetlet-conntest__num { + min-width:1.7rem; height:1.7rem; border-radius:8px; + display:inline-flex; align-items:center; justify-content:center; + background:#e6faf3; color:#00261a; font-weight:750; font-size:.85rem; +} +.meetlet-admin-settings .meetlet-conntest__card--pending { opacity:.78; background:#fbfefe; } +.meetlet-admin-settings .meetlet-conntest__card--running { + border-color:#e6b422; + box-shadow:0 0 0 2px rgba(230,180,34,.22); + background:#fff8e6; +} +.meetlet-admin-settings .meetlet-conntest__card--running .meetlet-conntest__num { + background:#ffe08a; color:#5c4700; +} +.meetlet-admin-settings .meetlet-conntest__card--ok { + border-color:#9fdcc4; background:#f3fbf8; +} +.meetlet-admin-settings .meetlet-conntest__card--fail { + border-color:#ead3d0; background:#fff8f7; +} +.meetlet-admin-settings .meetlet-conntest__card--skipped { opacity:.55; } +.meetlet-admin-settings .meetlet-dot--pending { background:#9bb7ac; box-shadow:0 0 0 4px rgba(155,183,172,.18); } +.meetlet-admin-settings .meetlet-dot--running { + background:#e6b422; box-shadow:0 0 0 4px rgba(230,180,34,.28); + animation:meetletPulse 1s ease-in-out infinite; +} +.meetlet-admin-settings .meetlet-dot--skipped { background:#b7c4be; box-shadow:none; } +.meetlet-admin-settings .meetlet-pill--pending { background:#e8efec; color:#456257; } +.meetlet-admin-settings .meetlet-pill--running { background:#ffe08a; color:#5c4700; } +.meetlet-admin-settings .meetlet-pill--skipped { background:#eef1f0; color:#667a72; } +@keyframes meetletPulse { + 0%, 100% { transform:scale(1); opacity:1; } + 50% { transform:scale(1.15); opacity:.75; } +} +.meetlet-admin-settings form.settingsform { + padding-bottom:1rem; +} +/* Single save row: spacing only — no stacked border/gradient bands. */ +.meetlet-admin-settings .settingsform > .row:has(.btn-primary), +.meetlet-admin-settings .form-buttons, +.meetlet-admin-settings .btns-group { + margin-top:1.25rem !important; + padding:0; + border:0; + background:transparent; + border-radius:0; +} +.meetlet-admin-settings .meetlet-savebar { + margin-top: 1.25rem !important; + margin-right: 0 !important; + margin-left: 0 !important; + margin-inline: 0 !important; + flex: 0 0 100% !important; + max-width: 100% !important; + width: 100% !important; + padding: .35rem 0 !important; + border: 0 !important; + background: transparent !important; + border-radius: 0 !important; +} +@media (min-width: 576px) { + .meetlet-admin-settings .meetlet-savebar.offset-sm-3, + .meetlet-admin-settings .meetlet-savebar { + margin-right: 0 !important; + margin-left: 0 !important; + } +} +.meetlet-admin-settings .btn-primary, +.meetlet-admin-settings input[type="submit"].btn-primary, +.meetlet-admin-settings button[type="submit"].btn-primary, +.meetlet-admin-settings .meetlet-btn-with-icon.btn-primary { + background:#52cca3 !important; + border-color:#3db892 !important; + color:#00261a !important; + font-weight:700 !important; + border-radius:10px !important; + padding:.55rem 1.35rem !important; + box-shadow:none !important; +} +.meetlet-admin-settings .btn-primary:hover, +.meetlet-admin-settings input[type="submit"].btn-primary:hover, +.meetlet-admin-settings button[type="submit"].btn-primary:hover { + background:#6fd9b5 !important; + border-color:#52cca3 !important; + color:#00261a !important; +} +.meetlet-admin-settings .meetlet-btn-with-icon { + display:inline-flex !important; + align-items:center; + justify-content:center; + gap:.45rem; +} +.meetlet-admin-settings .meetlet-btn-with-icon svg { + width:1.1rem; + height:1.1rem; + flex:0 0 auto; + display:block; +} +.meetlet-admin-settings .meetlet-btn-with-icon__label { + line-height:1.2; +} + +.meetlet-admin-settings .meetlet-server-group { + margin:1.1rem 0 .4rem; padding:1rem 1.1rem .2rem; + border:1px solid #cfe8de; border-radius:12px; + background:linear-gradient(180deg, #f3fbf8 0%, #ffffff 60%); +} +.meetlet-admin-settings .meetlet-server-group h4 { + margin:0 0 .25rem; color:#00261a; font-size:1.02rem; font-weight:700; +} +.meetlet-admin-settings .meetlet-server-group p { + margin:0 0 .55rem; color:#4a6b5f; font-size:.9rem; line-height:1.55; +} diff --git a/templates/participant_form_add.mustache b/templates/participant_form_add.mustache index a9b7ed0..f563ac8 100644 --- a/templates/participant_form_add.mustache +++ b/templates/participant_form_add.mustache @@ -1,3 +1,23 @@ +{{! + 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 . + + @template mod_meetletacbfree/participant_form_add + @copyright 2026 MeetLet + @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +}} {{participanttype}} {{participantvalue}} diff --git a/templates/participant_single_select.mustache b/templates/participant_single_select.mustache index 033c487..fd1a658 100644 --- a/templates/participant_single_select.mustache +++ b/templates/participant_single_select.mustache @@ -1,3 +1,23 @@ +{{! + 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 . + + @template mod_meetletacbfree/participant_single_select + @copyright 2026 MeetLet + @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +}}
{{#label}}