From 181902853c8149dad765396bfda210215b99b9a0 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Wed, 8 Nov 2023 12:58:55 +0800 Subject: [PATCH] MDL-80037 theme: Indicate when $CFG->theme is defined in config.php * And prevent selection of theme if $CFG->theme is defined in config.php --- admin/amd/build/themeselector/preview_modal.min.js | 2 +- .../build/themeselector/preview_modal.min.js.map | 2 +- admin/amd/src/themeselector/preview_modal.js | 6 +++++- admin/classes/output/theme_selector.php | 8 +++++++- admin/templates/themeselector/theme_card.mustache | 3 +++ .../themeselector/theme_preview_modal.mustache | 5 ++++- .../templates/themeselector/theme_selector.mustache | 2 +- admin/themeselector.php | 13 ++++++++++--- lang/en/admin.php | 1 + 9 files changed, 33 insertions(+), 9 deletions(-) diff --git a/admin/amd/build/themeselector/preview_modal.min.js b/admin/amd/build/themeselector/preview_modal.min.js index 370331cb848..93974fb77ad 100644 --- a/admin/amd/build/themeselector/preview_modal.min.js +++ b/admin/amd/build/themeselector/preview_modal.min.js @@ -5,6 +5,6 @@ define("core_admin/themeselector/preview_modal",["exports","core/modal_events"," * @module core_admin/themeselector/preview_modal * @copyright 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modal_events=_interopRequireDefault(_modal_events),_modal_cancel=_interopRequireDefault(_modal_cancel),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_notification=_interopRequireDefault(_notification),_templates=_interopRequireDefault(_templates);const SELECTORS_PREVIEW='[data-action="preview"]';_exports.init=()=>{registerListenerEvents()};const registerListenerEvents=()=>{document.addEventListener("click",(e=>{const preview=e.target.closest(SELECTORS_PREVIEW);preview&&buildModal(preview).catch(_notification.default.exception)}))},buildModal=async element=>{let description=await(0,_str.getString)("choosereadme","theme_"+element.getAttribute("data-choose"));const data={name:element.getAttribute("data-name"),image:element.getAttribute("data-image"),description:description.replace(/<[^>]+>/g," "),current:element.getAttribute("data-current"),actionurl:element.getAttribute("data-actionurl"),choose:element.getAttribute("data-choose"),sesskey:element.getAttribute("data-sesskey")};let modalTemplate=_modal_save_cancel.default;data.current&&(modalTemplate=_modal_cancel.default);const modal=await modalTemplate.create({title:data.name,body:_templates.default.render("core_admin/themeselector/theme_preview_modal",data),large:!0,buttons:{save:(0,_str.getString)("selecttheme","moodle"),cancel:(0,_str.getString)("closebuttontitle","moodle")},show:!0});modal.getRoot().on(_modal_events.default.save,(()=>{modal.getRoot().find("form").submit()}))}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modal_events=_interopRequireDefault(_modal_events),_modal_cancel=_interopRequireDefault(_modal_cancel),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_notification=_interopRequireDefault(_notification),_templates=_interopRequireDefault(_templates);const SELECTORS_THEMES_CONTAINER="themelist",SELECTORS_PREVIEW='[data-action="preview"]';_exports.init=()=>{registerListenerEvents()};const registerListenerEvents=()=>{document.addEventListener("click",(e=>{const preview=e.target.closest(SELECTORS_PREVIEW);preview&&buildModal(preview).catch(_notification.default.exception)}))},buildModal=async element=>{let description=await(0,_str.getString)("choosereadme","theme_"+element.getAttribute("data-choose"));const themesContainer=document.getElementById(SELECTORS_THEMES_CONTAINER),definedInConfig=parseInt(themesContainer.dataset.definedinconfig),data={name:element.getAttribute("data-name"),image:element.getAttribute("data-image"),description:description.replace(/<[^>]+>/g," "),current:element.getAttribute("data-current"),actionurl:element.getAttribute("data-actionurl"),choose:element.getAttribute("data-choose"),sesskey:element.getAttribute("data-sesskey"),definedinconfig:definedInConfig};let modalTemplate=_modal_save_cancel.default;(data.current||data.definedinconfig)&&(modalTemplate=_modal_cancel.default);const modal=await modalTemplate.create({title:data.name,body:_templates.default.render("core_admin/themeselector/theme_preview_modal",data),large:!0,buttons:{save:(0,_str.getString)("selecttheme","moodle"),cancel:(0,_str.getString)("closebuttontitle","moodle")},show:!0});modal.getRoot().on(_modal_events.default.save,(()=>{modal.getRoot().find("form").submit()}))}})); //# sourceMappingURL=preview_modal.min.js.map \ No newline at end of file diff --git a/admin/amd/build/themeselector/preview_modal.min.js.map b/admin/amd/build/themeselector/preview_modal.min.js.map index 2e19cc298ff..2bc795e3d2a 100644 --- a/admin/amd/build/themeselector/preview_modal.min.js.map +++ b/admin/amd/build/themeselector/preview_modal.min.js.map @@ -1 +1 @@ -{"version":3,"file":"preview_modal.min.js","sources":["../../src/themeselector/preview_modal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Modal for theme previews.\n *\n * @module core_admin/themeselector/preview_modal\n * @copyright 2023 David Woloszyn \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalEvents from 'core/modal_events';\nimport ModalCancel from 'core/modal_cancel';\nimport ModalSaveCancel from 'core/modal_save_cancel';\nimport Notification from 'core/notification';\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\n\nconst SELECTORS = {\n PREVIEW: '[data-action=\"preview\"]',\n};\n\n/**\n * Entrypoint of the js.\n *\n * @method init\n */\nexport const init = () => {\n registerListenerEvents();\n};\n\n/**\n * Register theme related event listeners.\n *\n * @method registerListenerEvents\n */\nconst registerListenerEvents = () => {\n document.addEventListener('click', (e) => {\n const preview = e.target.closest(SELECTORS.PREVIEW);\n if (preview) {\n buildModal(preview).catch(Notification.exception);\n }\n });\n};\n\n/**\n * Build the modal with the provided data.\n *\n * @method buildModal\n * @param {object} element\n */\nconst buildModal = async(element) => {\n\n // This string can be long. We will fetch it with JS as opposed to passing it as an attribute.\n let description = await getString('choosereadme', 'theme_' + element.getAttribute('data-choose'));\n\n // Prepare data for modal.\n const data = {\n name: element.getAttribute('data-name'),\n image: element.getAttribute('data-image'),\n description: description.replace(/<[^>]+>/g, ' '), // Strip out HTML tags.\n current: element.getAttribute('data-current'),\n actionurl: element.getAttribute('data-actionurl'),\n choose: element.getAttribute('data-choose'),\n sesskey: element.getAttribute('data-sesskey'),\n };\n\n // Determine which modal template we should use.\n let modalTemplate = ModalSaveCancel;\n if (data.current) {\n modalTemplate = ModalCancel;\n }\n\n const modal = await modalTemplate.create({\n title: data.name,\n body: Templates.render('core_admin/themeselector/theme_preview_modal', data),\n large: true,\n buttons: {\n 'save': getString('selecttheme', 'moodle'),\n 'cancel': getString('closebuttontitle', 'moodle'),\n },\n show: true,\n });\n\n modal.getRoot().on(ModalEvents.save, () => {\n modal.getRoot().find('form').submit();\n });\n};\n"],"names":["SELECTORS","registerListenerEvents","document","addEventListener","e","preview","target","closest","buildModal","catch","Notification","exception","async","description","element","getAttribute","data","name","image","replace","current","actionurl","choose","sesskey","modalTemplate","ModalSaveCancel","ModalCancel","modal","create","title","body","Templates","render","large","buttons","show","getRoot","on","ModalEvents","save","find","submit"],"mappings":";;;;;;;gWA8BMA,kBACO,wCAQO,KAChBC,gCAQEA,uBAAyB,KAC3BC,SAASC,iBAAiB,SAAUC,UAC1BC,QAAUD,EAAEE,OAAOC,QAAQP,mBAC7BK,SACAG,WAAWH,SAASI,MAAMC,sBAAaC,eAW7CH,WAAaI,MAAAA,cAGXC,kBAAoB,kBAAU,eAAgB,SAAWC,QAAQC,aAAa,sBAG5EC,KAAO,CACTC,KAAMH,QAAQC,aAAa,aAC3BG,MAAOJ,QAAQC,aAAa,cAC5BF,YAAaA,YAAYM,QAAQ,WAAY,KAC7CC,QAASN,QAAQC,aAAa,gBAC9BM,UAAWP,QAAQC,aAAa,kBAChCO,OAAQR,QAAQC,aAAa,eAC7BQ,QAAST,QAAQC,aAAa,qBAI9BS,cAAgBC,2BAChBT,KAAKI,UACLI,cAAgBE,6BAGdC,YAAcH,cAAcI,OAAO,CACrCC,MAAOb,KAAKC,KACZa,KAAMC,mBAAUC,OAAO,+CAAgDhB,MACvEiB,OAAO,EACPC,QAAS,OACG,kBAAU,cAAe,kBACvB,kBAAU,mBAAoB,WAE5CC,MAAM,IAGVR,MAAMS,UAAUC,GAAGC,sBAAYC,MAAM,KACjCZ,MAAMS,UAAUI,KAAK,QAAQC"} \ No newline at end of file +{"version":3,"file":"preview_modal.min.js","sources":["../../src/themeselector/preview_modal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Modal for theme previews.\n *\n * @module core_admin/themeselector/preview_modal\n * @copyright 2023 David Woloszyn \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalEvents from 'core/modal_events';\nimport ModalCancel from 'core/modal_cancel';\nimport ModalSaveCancel from 'core/modal_save_cancel';\nimport Notification from 'core/notification';\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\n\nconst SELECTORS = {\n THEMES_CONTAINER: 'themelist',\n PREVIEW: '[data-action=\"preview\"]',\n};\n\n/**\n * Entrypoint of the js.\n *\n * @method init\n */\nexport const init = () => {\n registerListenerEvents();\n};\n\n/**\n * Register theme related event listeners.\n *\n * @method registerListenerEvents\n */\nconst registerListenerEvents = () => {\n document.addEventListener('click', (e) => {\n const preview = e.target.closest(SELECTORS.PREVIEW);\n if (preview) {\n buildModal(preview).catch(Notification.exception);\n }\n });\n};\n\n/**\n * Build the modal with the provided data.\n *\n * @method buildModal\n * @param {object} element\n */\nconst buildModal = async(element) => {\n\n // This string can be long. We will fetch it with JS as opposed to passing it as an attribute.\n let description = await getString('choosereadme', 'theme_' + element.getAttribute('data-choose'));\n\n const themesContainer = document.getElementById(SELECTORS.THEMES_CONTAINER);\n const definedInConfig = parseInt(themesContainer.dataset.definedinconfig);\n // Prepare data for modal.\n const data = {\n name: element.getAttribute('data-name'),\n image: element.getAttribute('data-image'),\n description: description.replace(/<[^>]+>/g, ' '), // Strip out HTML tags.\n current: element.getAttribute('data-current'),\n actionurl: element.getAttribute('data-actionurl'),\n choose: element.getAttribute('data-choose'),\n sesskey: element.getAttribute('data-sesskey'),\n definedinconfig: definedInConfig,\n };\n\n // Determine which modal template we should use.\n let modalTemplate = ModalSaveCancel;\n if (data.current || data.definedinconfig) {\n modalTemplate = ModalCancel;\n }\n\n const modal = await modalTemplate.create({\n title: data.name,\n body: Templates.render('core_admin/themeselector/theme_preview_modal', data),\n large: true,\n buttons: {\n 'save': getString('selecttheme', 'moodle'),\n 'cancel': getString('closebuttontitle', 'moodle'),\n },\n show: true,\n });\n\n modal.getRoot().on(ModalEvents.save, () => {\n modal.getRoot().find('form').submit();\n });\n};\n"],"names":["SELECTORS","registerListenerEvents","document","addEventListener","e","preview","target","closest","buildModal","catch","Notification","exception","async","description","element","getAttribute","themesContainer","getElementById","definedInConfig","parseInt","dataset","definedinconfig","data","name","image","replace","current","actionurl","choose","sesskey","modalTemplate","ModalSaveCancel","ModalCancel","modal","create","title","body","Templates","render","large","buttons","show","getRoot","on","ModalEvents","save","find","submit"],"mappings":";;;;;;;gWA8BMA,2BACgB,YADhBA,kBAEO,wCAQO,KAChBC,gCAQEA,uBAAyB,KAC3BC,SAASC,iBAAiB,SAAUC,UAC1BC,QAAUD,EAAEE,OAAOC,QAAQP,mBAC7BK,SACAG,WAAWH,SAASI,MAAMC,sBAAaC,eAW7CH,WAAaI,MAAAA,cAGXC,kBAAoB,kBAAU,eAAgB,SAAWC,QAAQC,aAAa,sBAE5EC,gBAAkBd,SAASe,eAAejB,4BAC1CkB,gBAAkBC,SAASH,gBAAgBI,QAAQC,iBAEnDC,KAAO,CACTC,KAAMT,QAAQC,aAAa,aAC3BS,MAAOV,QAAQC,aAAa,cAC5BF,YAAaA,YAAYY,QAAQ,WAAY,KAC7CC,QAASZ,QAAQC,aAAa,gBAC9BY,UAAWb,QAAQC,aAAa,kBAChCa,OAAQd,QAAQC,aAAa,eAC7Bc,QAASf,QAAQC,aAAa,gBAC9BM,gBAAiBH,qBAIjBY,cAAgBC,4BAChBT,KAAKI,SAAWJ,KAAKD,mBACrBS,cAAgBE,6BAGdC,YAAcH,cAAcI,OAAO,CACrCC,MAAOb,KAAKC,KACZa,KAAMC,mBAAUC,OAAO,+CAAgDhB,MACvEiB,OAAO,EACPC,QAAS,OACG,kBAAU,cAAe,kBACvB,kBAAU,mBAAoB,WAE5CC,MAAM,IAGVR,MAAMS,UAAUC,GAAGC,sBAAYC,MAAM,KACjCZ,MAAMS,UAAUI,KAAK,QAAQC"} \ No newline at end of file diff --git a/admin/amd/src/themeselector/preview_modal.js b/admin/amd/src/themeselector/preview_modal.js index 9ae089be841..f1d162a66c5 100644 --- a/admin/amd/src/themeselector/preview_modal.js +++ b/admin/amd/src/themeselector/preview_modal.js @@ -29,6 +29,7 @@ import Templates from 'core/templates'; import {getString} from 'core/str'; const SELECTORS = { + THEMES_CONTAINER: 'themelist', PREVIEW: '[data-action="preview"]', }; @@ -66,6 +67,8 @@ const buildModal = async(element) => { // This string can be long. We will fetch it with JS as opposed to passing it as an attribute. let description = await getString('choosereadme', 'theme_' + element.getAttribute('data-choose')); + const themesContainer = document.getElementById(SELECTORS.THEMES_CONTAINER); + const definedInConfig = parseInt(themesContainer.dataset.definedinconfig); // Prepare data for modal. const data = { name: element.getAttribute('data-name'), @@ -75,11 +78,12 @@ const buildModal = async(element) => { actionurl: element.getAttribute('data-actionurl'), choose: element.getAttribute('data-choose'), sesskey: element.getAttribute('data-sesskey'), + definedinconfig: definedInConfig, }; // Determine which modal template we should use. let modalTemplate = ModalSaveCancel; - if (data.current) { + if (data.current || data.definedinconfig) { modalTemplate = ModalCancel; } diff --git a/admin/classes/output/theme_selector.php b/admin/classes/output/theme_selector.php index 6fb69707929..5a63f48f361 100644 --- a/admin/classes/output/theme_selector.php +++ b/admin/classes/output/theme_selector.php @@ -34,13 +34,18 @@ class theme_selector implements renderable, templatable { /** @var array $themedata Theme data to pass to the template. */ private $themedata = null; + /** @var bool Whether $CFG->theme is defined in config.php. */ + private $definedinconfig; + /** * Constructor. * * @param array $themedata Theme data used for template. + * @param bool $definedinconfig Whether $CFG->theme is defined in config.php. */ - public function __construct(array $themedata) { + public function __construct(array $themedata, bool $definedinconfig = false) { $this->themedata = $themedata; + $this->definedinconfig = $definedinconfig; } /** @@ -59,6 +64,7 @@ class theme_selector implements renderable, templatable { $resetbutton = new \single_button($reseturl, get_string('themeresetcaches', 'admin'), 'post', \single_button::BUTTON_SECONDARY); $data->resetbutton = $resetbutton->export_for_template($output); + $data->definedinconfig = $this->definedinconfig; return $data; } diff --git a/admin/templates/themeselector/theme_card.mustache b/admin/templates/themeselector/theme_card.mustache index 9cb3bb6fea7..4346123c37e 100644 --- a/admin/templates/themeselector/theme_card.mustache +++ b/admin/templates/themeselector/theme_card.mustache @@ -58,6 +58,9 @@
{{#current}} {{#str}}currenttheme, moodle{{/str}} + {{#definedinconfig}} +
{{#str}}configoverride, admin{{/str}}
+ {{/definedinconfig}} {{/current}} {{^current}} {{#actionurl}} diff --git a/admin/templates/themeselector/theme_preview_modal.mustache b/admin/templates/themeselector/theme_preview_modal.mustache index 605f6762906..b3f425050b3 100644 --- a/admin/templates/themeselector/theme_preview_modal.mustache +++ b/admin/templates/themeselector/theme_preview_modal.mustache @@ -39,9 +39,12 @@

{{#str}}themepreviewdescription, moodle, {{name}}{{/str}}

-
+
{{#current}} {{#str}}currenttheme, moodle{{/str}} + {{#definedinconfig}} +
{{#str}}configoverride, admin{{/str}}
+ {{/definedinconfig}} {{/current}}
diff --git a/admin/templates/themeselector/theme_selector.mustache b/admin/templates/themeselector/theme_selector.mustache index ffefda6cd45..6970b829008 100644 --- a/admin/templates/themeselector/theme_selector.mustache +++ b/admin/templates/themeselector/theme_selector.mustache @@ -62,7 +62,7 @@ {{>core/single_button}}
{{/resetbutton}} -
+
{{#themes}} {{>core_admin/themeselector/theme_card}} {{/themes}} diff --git a/admin/themeselector.php b/admin/themeselector.php index d83959b0d75..f8e2b1cf00c 100644 --- a/admin/themeselector.php +++ b/admin/themeselector.php @@ -42,8 +42,15 @@ if ($reset && confirm_sesskey()) { theme_reset_all_caches(); } +$definedinconfig = array_key_exists('theme', $CFG->config_php_settings); +if ($definedinconfig) { + $forcedthemename = get_string('pluginname', 'theme_'.$CFG->theme); + // Show a notification that the theme is defined in config.php. + \core\notification::info(get_string('themedefinedinconfigphp', 'admin', $forcedthemename)); +} + // Change theme. -if (!empty($choose) && confirm_sesskey()) { +if (!$definedinconfig && !empty($choose) && confirm_sesskey()) { // Load the theme to make sure it is valid. $theme = theme_config::load($choose); @@ -105,7 +112,7 @@ foreach ($themes as $themename => $themedir) { if ($themename === $CFG->theme) { $themedata['current'] = true; $currentthemeindex = $index; - } else { + } else if (!$definedinconfig) { // Form params. $actionurl = new moodle_url('/admin/themeselector.php'); $themedata['actionurl'] = $actionurl; @@ -124,7 +131,7 @@ if (isset($data[$currentthemeindex])) { } // Show theme selector. -$renderable = new \core_admin\output\theme_selector($data); +$renderable = new \core_admin\output\theme_selector($data, $definedinconfig); $renderer = $PAGE->get_renderer('core', 'admin'); echo $renderer->theme_selector_list($renderable); diff --git a/lang/en/admin.php b/lang/en/admin.php index 05e79407832..cc0855dd783 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -1452,6 +1452,7 @@ $string['testoutgoingmailconf_sentmail'] = 'This site has successfully sent a te $string['testoutgoingmailconf_subject'] = '{$a->site}: test message. {$a->additional} Sent: {$a->time}'; $string['testoutgoingmailconf_subjectadditional'] = 'Additional subject'; $string['testoutgoingmailconf_toemail'] = 'To email address'; +$string['themedefinedinconfigphp'] = 'The theme cannot be changed because it is set to {$a} in config.php.'; $string['themedesignermode'] = 'Theme designer mode'; $string['themedesignermodewarning'] = 'Theme designer mode is enabled. This should not be enabled on production sites as it can significantly reduce performance.'; $string['themelist'] = 'Theme list';