diff --git a/public/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js b/public/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js
index c06698bd6f1..e4ee8ae11f2 100644
--- a/public/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js
+++ b/public/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js
@@ -1,3 +1,3 @@
-define("tiny_premium/configuration",["exports","editor_tiny/utils","editor_tiny/options"],(function(_exports,_utils,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=(instanceConfig,options)=>{const enabledPremiumPlugins=(0,_options.getInitialPluginConfiguration)(options)[(0,_options.getPluginOptionName)("tiny_premium/plugin","premiumplugins")].split(",");let plugins=instanceConfig.plugins,menu=instanceConfig.menu,toolbar=(toolbar=>(toolbar=(0,_utils.addToolbarSection)(toolbar,"premium_a","advanced",!0),(0,_utils.addToolbarSection)(toolbar,"premium_b","formatting",!0)))(instanceConfig.toolbar),contextmenu=instanceConfig.contextmenu;return-1!==enabledPremiumPlugins.indexOf("advtable")&&(plugins+=" advtable",menu=(0,_utils.addMenubarItem)(menu,"table","| advtablerownumbering","advtablesort")),-1!==enabledPremiumPlugins.indexOf("editimage")&&(plugins+=" editimage",toolbar=(0,_utils.addToolbarButton)(toolbar,"content","editimage","tiny_media_image"),instanceConfig.editimage_toolbar="rotateleft rotateright flipv fliph editimage"),-1!==enabledPremiumPlugins.indexOf("export")&&(plugins+=" export",menu=(0,_utils.addMenubarItem)(menu,"tools","| export")),-1!==enabledPremiumPlugins.indexOf("pageembed")&&(plugins+=" pageembed",toolbar=(0,_utils.addToolbarButton)(toolbar,"content","pageembed","tiny_media_video")),-1!==enabledPremiumPlugins.indexOf("typography")&&(plugins+=" typography",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_b","typography")),-1!==enabledPremiumPlugins.indexOf("casechange")&&(plugins+=" casechange",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","casechange")),-1!==enabledPremiumPlugins.indexOf("checklist")&&(plugins+=" checklist",toolbar=(0,_utils.addToolbarButton)(toolbar,"lists","checklist")),-1!==enabledPremiumPlugins.indexOf("tinymcespellchecker")&&(plugins+=" tinymcespellchecker",menu=(0,_utils.addMenubarItem)(menu,"tools","spellcheckdialog","spellcheckerlanguage"),contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"spellchecker"),toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","spellcheckdialog")),-1!==enabledPremiumPlugins.indexOf("autocorrect")&&(plugins+=" autocorrect",menu=(0,_utils.addMenubarItem)(menu,"tools","| autocorrect capitalization","spellcheckdialog")),-1!==enabledPremiumPlugins.indexOf("permanentpen")&&(plugins+=" permanentpen",menu=(0,_utils.addMenubarItem)(menu,"format","| permanentpen configurepermanentpen"),toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","permanentpen"),contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"configurepermanentpen")),-1!==enabledPremiumPlugins.indexOf("formatpainter")&&(plugins+=" formatpainter",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","formatpainter")),-1!==enabledPremiumPlugins.indexOf("linkchecker")&&(plugins+=" linkchecker",contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"linkchecker")),-1!==enabledPremiumPlugins.indexOf("tableofcontents")&&(plugins+=" tableofcontents",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","tableofcontents")),-1!==enabledPremiumPlugins.indexOf("math")&&(plugins+=" math",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","math"),menu=(0,_utils.addMenubarItem)(menu,"insert","math","tableofcontents")),-1!==enabledPremiumPlugins.indexOf("footnotes")&&(plugins+=" footnotes",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","footnotes"),menu=(0,_utils.addMenubarItem)(menu,"insert","footnotes","tableofcontents")),-1!==enabledPremiumPlugins.indexOf("powerpaste")&&(plugins+=" powerpaste"),-1!==enabledPremiumPlugins.indexOf("a11ychecker")&&(plugins+=" a11ychecker",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","a11ycheck"),menu=(0,_utils.removeMenubarItem)(menu,"tools","a11ycheck"),menu=(0,_utils.addMenubarItem)(menu,"tools","a11ycheck","count"),menu=(0,_utils.removeMenubarItem)(menu,"tools","tiny_accessibilitychecker")),{plugins:plugins,toolbar:toolbar,menu:menu,contextmenu:contextmenu}}}));
+define("tiny_premium/configuration",["exports","editor_tiny/utils","editor_tiny/options"],(function(_exports,_utils,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=(instanceConfig,options)=>{const pluginOptions=(0,_options.getInitialPluginConfiguration)(options),enabledPremiumPlugins=pluginOptions[(0,_options.getPluginOptionName)("tiny_premium/plugin","premiumplugins")].split(",");let serviceUrls=pluginOptions[(0,_options.getPluginOptionName)("tiny_premium/plugin","serviceurls")],plugins=instanceConfig.plugins,menu=instanceConfig.menu,toolbar=(toolbar=>(toolbar=(0,_utils.addToolbarSection)(toolbar,"premium_a","advanced",!0),(0,_utils.addToolbarSection)(toolbar,"premium_b","formatting",!0)))(instanceConfig.toolbar),contextmenu=instanceConfig.contextmenu,pluginsettings={};return pluginsettings={...pluginsettings,...serviceUrls},-1!==enabledPremiumPlugins.indexOf("advtable")&&(plugins+=" advtable",menu=(0,_utils.addMenubarItem)(menu,"table","| advtablerownumbering","advtablesort")),-1!==enabledPremiumPlugins.indexOf("editimage")&&(plugins+=" editimage",toolbar=(0,_utils.addToolbarButton)(toolbar,"content","editimage","tiny_media_image"),instanceConfig.editimage_toolbar="rotateleft rotateright flipv fliph editimage"),-1!==enabledPremiumPlugins.indexOf("export")&&(plugins+=" export",menu=(0,_utils.addMenubarItem)(menu,"tools","| export")),-1!==enabledPremiumPlugins.indexOf("pageembed")&&(plugins+=" pageembed",toolbar=(0,_utils.addToolbarButton)(toolbar,"content","pageembed","tiny_media_video")),-1!==enabledPremiumPlugins.indexOf("typography")&&(plugins+=" typography",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_b","typography")),-1!==enabledPremiumPlugins.indexOf("casechange")&&(plugins+=" casechange",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","casechange")),-1!==enabledPremiumPlugins.indexOf("checklist")&&(plugins+=" checklist",toolbar=(0,_utils.addToolbarButton)(toolbar,"lists","checklist")),-1!==enabledPremiumPlugins.indexOf("tinymcespellchecker")&&(plugins+=" tinymcespellchecker",menu=(0,_utils.addMenubarItem)(menu,"tools","spellcheckdialog","spellcheckerlanguage"),contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"spellchecker"),toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","spellcheckdialog")),-1!==enabledPremiumPlugins.indexOf("autocorrect")&&(plugins+=" autocorrect",menu=(0,_utils.addMenubarItem)(menu,"tools","| autocorrect capitalization","spellcheckdialog")),-1!==enabledPremiumPlugins.indexOf("permanentpen")&&(plugins+=" permanentpen",menu=(0,_utils.addMenubarItem)(menu,"format","| permanentpen configurepermanentpen"),toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","permanentpen"),contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"configurepermanentpen")),-1!==enabledPremiumPlugins.indexOf("formatpainter")&&(plugins+=" formatpainter",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","formatpainter")),-1!==enabledPremiumPlugins.indexOf("linkchecker")&&(plugins+=" linkchecker",contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"linkchecker")),-1!==enabledPremiumPlugins.indexOf("tableofcontents")&&(plugins+=" tableofcontents",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","tableofcontents")),-1!==enabledPremiumPlugins.indexOf("math")&&(plugins+=" math",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","math"),menu=(0,_utils.addMenubarItem)(menu,"insert","math","tableofcontents")),-1!==enabledPremiumPlugins.indexOf("footnotes")&&(plugins+=" footnotes",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","footnotes"),menu=(0,_utils.addMenubarItem)(menu,"insert","footnotes","tableofcontents")),-1!==enabledPremiumPlugins.indexOf("powerpaste")&&(plugins+=" powerpaste"),-1!==enabledPremiumPlugins.indexOf("a11ychecker")&&(plugins+=" a11ychecker",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","a11ycheck"),menu=(0,_utils.removeMenubarItem)(menu,"tools","a11ycheck"),menu=(0,_utils.addMenubarItem)(menu,"tools","a11ycheck","count"),menu=(0,_utils.removeMenubarItem)(menu,"tools","tiny_accessibilitychecker")),{plugins:plugins,toolbar:toolbar,menu:menu,contextmenu:contextmenu,...pluginsettings}}}));
//# sourceMappingURL=configuration.min.js.map
\ No newline at end of file
diff --git a/public/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js.map b/public/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js.map
index 74966e22b45..dde8e2c63ec 100644
--- a/public/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js.map
+++ b/public/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"configuration.min.js","sources":["../src/configuration.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 * Tiny Premium configuration.\n *\n * @module tiny_premium/configuration\n * @copyright 2023 David Woloszyn \n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {\n addToolbarButton,\n addMenubarItem,\n addToolbarSection,\n addContextmenuItem,\n removeMenubarItem,\n} from 'editor_tiny/utils';\nimport {\n getInitialPluginConfiguration,\n getPluginOptionName\n} from 'editor_tiny/options';\n\nconst configureToolbar = (toolbar) => {\n // Add premium toolbar sections to house all the plugins with no natural home.\n toolbar = addToolbarSection(toolbar, 'premium_a', 'advanced', true);\n toolbar = addToolbarSection(toolbar, 'premium_b', 'formatting', true);\n return toolbar;\n};\n\nexport const configure = (instanceConfig, options) => {\n // Get the namespaced options for Tiny Premium before they are officially initialised.\n // Due to the timing the plugin options are available, we need to get at the options in this slightly unconventional way.\n const pluginOptions = getInitialPluginConfiguration(options);\n const enabledPremiumPlugins = pluginOptions[getPluginOptionName('tiny_premium/plugin', 'premiumplugins')].split(',');\n\n let plugins = instanceConfig.plugins;\n let menu = instanceConfig.menu;\n let toolbar = configureToolbar(instanceConfig.toolbar);\n let contextmenu = instanceConfig.contextmenu;\n let pluginsettings = {};\n\n // Advanced Table.\n if (enabledPremiumPlugins.indexOf('advtable') !== -1) {\n plugins += ` advtable`;\n menu = addMenubarItem(menu, 'table', '| advtablerownumbering', 'advtablesort');\n }\n // Enhanced Image Editing.\n if (enabledPremiumPlugins.indexOf('editimage') !== -1) {\n plugins += ` editimage`;\n toolbar = addToolbarButton(toolbar, 'content', 'editimage', 'tiny_media_image');\n // Remove the duplicate image button from the quickbar toolbar by redefining the values without 'imageoptions'.\n // eslint-disable-next-line camelcase\n instanceConfig.editimage_toolbar = 'rotateleft rotateright flipv fliph editimage';\n }\n // Export.\n if (enabledPremiumPlugins.indexOf('export') !== -1) {\n plugins += ` export`;\n menu = addMenubarItem(menu, 'tools', '| export');\n }\n // Page Embed.\n if (enabledPremiumPlugins.indexOf('pageembed') !== -1) {\n plugins += ` pageembed`;\n toolbar = addToolbarButton(toolbar, 'content', 'pageembed', 'tiny_media_video');\n }\n // Advanced Typography.\n if (enabledPremiumPlugins.indexOf('typography') !== -1) {\n plugins += ` typography`;\n toolbar = addToolbarButton(toolbar, 'premium_b', 'typography');\n }\n // Case Change.\n if (enabledPremiumPlugins.indexOf('casechange') !== -1) {\n plugins += ` casechange`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'casechange');\n }\n // Checklist.\n if (enabledPremiumPlugins.indexOf('checklist') !== -1) {\n plugins += ` checklist`;\n toolbar = addToolbarButton(toolbar, 'lists', 'checklist');\n }\n // Spell Checker Pro.\n if (enabledPremiumPlugins.indexOf('tinymcespellchecker') !== -1) {\n plugins += ` tinymcespellchecker`;\n menu = addMenubarItem(menu, 'tools', 'spellcheckdialog', 'spellcheckerlanguage');\n contextmenu = addContextmenuItem(contextmenu, 'spellchecker');\n toolbar = addToolbarButton(toolbar, 'premium_a', 'spellcheckdialog');\n }\n // Spelling Autocorrect.\n if (enabledPremiumPlugins.indexOf('autocorrect') !== -1) {\n plugins += ` autocorrect`;\n menu = addMenubarItem(menu, 'tools', '| autocorrect capitalization', 'spellcheckdialog');\n }\n // Permanent Pen.\n if (enabledPremiumPlugins.indexOf('permanentpen') !== -1) {\n plugins += ` permanentpen`;\n menu = addMenubarItem(menu, 'format', '| permanentpen configurepermanentpen');\n toolbar = addToolbarButton(toolbar, 'premium_a', 'permanentpen');\n contextmenu = addContextmenuItem(contextmenu, 'configurepermanentpen');\n }\n // Format Painter.\n if (enabledPremiumPlugins.indexOf('formatpainter') !== -1) {\n plugins += ` formatpainter`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'formatpainter');\n }\n // Link Checker.\n if (enabledPremiumPlugins.indexOf('linkchecker') !== -1) {\n plugins += ` linkchecker`;\n contextmenu = addContextmenuItem(contextmenu, 'linkchecker');\n }\n // Table of Contents.\n if (enabledPremiumPlugins.indexOf('tableofcontents') !== -1) {\n plugins += ` tableofcontents`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'tableofcontents');\n }\n // MathML\n if (enabledPremiumPlugins.indexOf('math') !== -1) {\n plugins += ` math`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'math');\n menu = addMenubarItem(menu, 'insert', 'math', 'tableofcontents');\n }\n // Footnotes.\n if (enabledPremiumPlugins.indexOf('footnotes') !== -1) {\n plugins += ` footnotes`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'footnotes');\n menu = addMenubarItem(menu, 'insert', 'footnotes', 'tableofcontents');\n }\n // Powerpaste.\n if (enabledPremiumPlugins.indexOf('powerpaste') !== -1) {\n plugins += ` powerpaste`;\n }\n // Accessibility Checker.\n if (enabledPremiumPlugins.indexOf('a11ychecker') !== -1) {\n plugins += ` a11ychecker`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'a11ycheck');\n // Tiny's allychecker plugin is listed in the menu by default. Let's remove it and add it where we want it.\n menu = removeMenubarItem(menu, 'tools', 'a11ycheck');\n // Positioning the a11ychecker in the same position as tiny_accessibilitychecker (for familiarity).\n menu = addMenubarItem(menu, 'tools', 'a11ycheck', 'count');\n // Finally, hide tiny_accessibilitychecker so there is no confusion.\n menu = removeMenubarItem(menu, 'tools', 'tiny_accessibilitychecker');\n }\n\n return {\n plugins,\n toolbar,\n menu,\n contextmenu,\n ...pluginsettings\n };\n};\n"],"names":["instanceConfig","options","enabledPremiumPlugins","split","plugins","menu","toolbar","configureToolbar","contextmenu","indexOf","editimage_toolbar"],"mappings":"mOA0CyB,CAACA,eAAgBC,iBAIhCC,uBADgB,0CAA8BD,UACR,gCAAoB,sBAAuB,mBAAmBE,MAAM,SAE5GC,QAAUJ,eAAeI,QACzBC,KAAOL,eAAeK,KACtBC,QAfkBA,CAAAA,UAEtBA,SAAU,4BAAkBA,QAAS,YAAa,YAAY,IACpD,4BAAkBA,QAAS,YAAa,cAAc,IAYlDC,CAAiBP,eAAeM,SAC1CE,YAAcR,eAAeQ,mBAIkB,IAA/CN,sBAAsBO,QAAQ,cAC9BL,qBACAC,MAAO,yBAAeA,KAAM,QAAS,yBAA0B,kBAGf,IAAhDH,sBAAsBO,QAAQ,eAC9BL,sBACAE,SAAU,2BAAiBA,QAAS,UAAW,YAAa,oBAG5DN,eAAeU,kBAAoB,iDAGU,IAA7CR,sBAAsBO,QAAQ,YAC9BL,mBACAC,MAAO,yBAAeA,KAAM,QAAS,cAGW,IAAhDH,sBAAsBO,QAAQ,eAC9BL,sBACAE,SAAU,2BAAiBA,QAAS,UAAW,YAAa,sBAGX,IAAjDJ,sBAAsBO,QAAQ,gBAC9BL,uBACAE,SAAU,2BAAiBA,QAAS,YAAa,gBAGA,IAAjDJ,sBAAsBO,QAAQ,gBAC9BL,uBACAE,SAAU,2BAAiBA,QAAS,YAAa,gBAGD,IAAhDJ,sBAAsBO,QAAQ,eAC9BL,sBACAE,SAAU,2BAAiBA,QAAS,QAAS,eAGa,IAA1DJ,sBAAsBO,QAAQ,yBAC9BL,gCACAC,MAAO,yBAAeA,KAAM,QAAS,mBAAoB,wBACzDG,aAAc,6BAAmBA,YAAa,gBAC9CF,SAAU,2BAAiBA,QAAS,YAAa,sBAGC,IAAlDJ,sBAAsBO,QAAQ,iBAC9BL,wBACAC,MAAO,yBAAeA,KAAM,QAAS,+BAAgC,sBAGlB,IAAnDH,sBAAsBO,QAAQ,kBAC9BL,yBACAC,MAAO,yBAAeA,KAAM,SAAU,wCACtCC,SAAU,2BAAiBA,QAAS,YAAa,gBACjDE,aAAc,6BAAmBA,YAAa,2BAGM,IAApDN,sBAAsBO,QAAQ,mBAC9BL,0BACAE,SAAU,2BAAiBA,QAAS,YAAa,mBAGC,IAAlDJ,sBAAsBO,QAAQ,iBAC9BL,wBACAI,aAAc,6BAAmBA,YAAa,iBAGQ,IAAtDN,sBAAsBO,QAAQ,qBAC9BL,4BACAE,SAAU,2BAAiBA,QAAS,YAAa,qBAGN,IAA3CJ,sBAAsBO,QAAQ,UAC9BL,iBACAE,SAAU,2BAAiBA,QAAS,YAAa,QACjDD,MAAO,yBAAeA,KAAM,SAAU,OAAQ,qBAGE,IAAhDH,sBAAsBO,QAAQ,eAC9BL,sBACAE,SAAU,2BAAiBA,QAAS,YAAa,aACjDD,MAAO,yBAAeA,KAAM,SAAU,YAAa,qBAGF,IAAjDH,sBAAsBO,QAAQ,gBAC9BL,yBAGkD,IAAlDF,sBAAsBO,QAAQ,iBAC9BL,wBACAE,SAAU,2BAAiBA,QAAS,YAAa,aAEjDD,MAAO,4BAAkBA,KAAM,QAAS,aAExCA,MAAO,yBAAeA,KAAM,QAAS,YAAa,SAElDA,MAAO,4BAAkBA,KAAM,QAAS,8BAGrC,CACHD,QAAAA,QACAE,QAAAA,QACAD,KAAAA,KACAG,YAAAA"}
\ No newline at end of file
+{"version":3,"file":"configuration.min.js","sources":["../src/configuration.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 * Tiny Premium configuration.\n *\n * @module tiny_premium/configuration\n * @copyright 2023 David Woloszyn \n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {\n addToolbarButton,\n addMenubarItem,\n addToolbarSection,\n addContextmenuItem,\n removeMenubarItem,\n} from 'editor_tiny/utils';\nimport {\n getInitialPluginConfiguration,\n getPluginOptionName\n} from 'editor_tiny/options';\n\nconst configureToolbar = (toolbar) => {\n // Add premium toolbar sections to house all the plugins with no natural home.\n toolbar = addToolbarSection(toolbar, 'premium_a', 'advanced', true);\n toolbar = addToolbarSection(toolbar, 'premium_b', 'formatting', true);\n return toolbar;\n};\n\nexport const configure = (instanceConfig, options) => {\n // Get the namespaced options for Tiny Premium before they are officially initialised.\n // Due to the timing the plugin options are available, we need to get at the options in this slightly unconventional way.\n const pluginOptions = getInitialPluginConfiguration(options);\n const enabledPremiumPlugins = pluginOptions[getPluginOptionName('tiny_premium/plugin', 'premiumplugins')].split(',');\n let serviceUrls = pluginOptions[getPluginOptionName('tiny_premium/plugin', 'serviceurls')];\n\n let plugins = instanceConfig.plugins;\n let menu = instanceConfig.menu;\n let toolbar = configureToolbar(instanceConfig.toolbar);\n let contextmenu = instanceConfig.contextmenu;\n let pluginsettings = {};\n pluginsettings = {...pluginsettings, ...serviceUrls};\n\n // Advanced Table.\n if (enabledPremiumPlugins.indexOf('advtable') !== -1) {\n plugins += ` advtable`;\n menu = addMenubarItem(menu, 'table', '| advtablerownumbering', 'advtablesort');\n }\n // Enhanced Image Editing.\n if (enabledPremiumPlugins.indexOf('editimage') !== -1) {\n plugins += ` editimage`;\n toolbar = addToolbarButton(toolbar, 'content', 'editimage', 'tiny_media_image');\n // Remove the duplicate image button from the quickbar toolbar by redefining the values without 'imageoptions'.\n // eslint-disable-next-line camelcase\n instanceConfig.editimage_toolbar = 'rotateleft rotateright flipv fliph editimage';\n }\n // Export.\n if (enabledPremiumPlugins.indexOf('export') !== -1) {\n plugins += ` export`;\n menu = addMenubarItem(menu, 'tools', '| export');\n }\n // Page Embed.\n if (enabledPremiumPlugins.indexOf('pageembed') !== -1) {\n plugins += ` pageembed`;\n toolbar = addToolbarButton(toolbar, 'content', 'pageembed', 'tiny_media_video');\n }\n // Advanced Typography.\n if (enabledPremiumPlugins.indexOf('typography') !== -1) {\n plugins += ` typography`;\n toolbar = addToolbarButton(toolbar, 'premium_b', 'typography');\n }\n // Case Change.\n if (enabledPremiumPlugins.indexOf('casechange') !== -1) {\n plugins += ` casechange`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'casechange');\n }\n // Checklist.\n if (enabledPremiumPlugins.indexOf('checklist') !== -1) {\n plugins += ` checklist`;\n toolbar = addToolbarButton(toolbar, 'lists', 'checklist');\n }\n // Spell Checker Pro.\n if (enabledPremiumPlugins.indexOf('tinymcespellchecker') !== -1) {\n plugins += ` tinymcespellchecker`;\n menu = addMenubarItem(menu, 'tools', 'spellcheckdialog', 'spellcheckerlanguage');\n contextmenu = addContextmenuItem(contextmenu, 'spellchecker');\n toolbar = addToolbarButton(toolbar, 'premium_a', 'spellcheckdialog');\n }\n // Spelling Autocorrect.\n if (enabledPremiumPlugins.indexOf('autocorrect') !== -1) {\n plugins += ` autocorrect`;\n menu = addMenubarItem(menu, 'tools', '| autocorrect capitalization', 'spellcheckdialog');\n }\n // Permanent Pen.\n if (enabledPremiumPlugins.indexOf('permanentpen') !== -1) {\n plugins += ` permanentpen`;\n menu = addMenubarItem(menu, 'format', '| permanentpen configurepermanentpen');\n toolbar = addToolbarButton(toolbar, 'premium_a', 'permanentpen');\n contextmenu = addContextmenuItem(contextmenu, 'configurepermanentpen');\n }\n // Format Painter.\n if (enabledPremiumPlugins.indexOf('formatpainter') !== -1) {\n plugins += ` formatpainter`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'formatpainter');\n }\n // Link Checker.\n if (enabledPremiumPlugins.indexOf('linkchecker') !== -1) {\n plugins += ` linkchecker`;\n contextmenu = addContextmenuItem(contextmenu, 'linkchecker');\n }\n // Table of Contents.\n if (enabledPremiumPlugins.indexOf('tableofcontents') !== -1) {\n plugins += ` tableofcontents`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'tableofcontents');\n }\n // MathML\n if (enabledPremiumPlugins.indexOf('math') !== -1) {\n plugins += ` math`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'math');\n menu = addMenubarItem(menu, 'insert', 'math', 'tableofcontents');\n }\n // Footnotes.\n if (enabledPremiumPlugins.indexOf('footnotes') !== -1) {\n plugins += ` footnotes`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'footnotes');\n menu = addMenubarItem(menu, 'insert', 'footnotes', 'tableofcontents');\n }\n // Powerpaste.\n if (enabledPremiumPlugins.indexOf('powerpaste') !== -1) {\n plugins += ` powerpaste`;\n }\n // Accessibility Checker.\n if (enabledPremiumPlugins.indexOf('a11ychecker') !== -1) {\n plugins += ` a11ychecker`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'a11ycheck');\n // Tiny's allychecker plugin is listed in the menu by default. Let's remove it and add it where we want it.\n menu = removeMenubarItem(menu, 'tools', 'a11ycheck');\n // Positioning the a11ychecker in the same position as tiny_accessibilitychecker (for familiarity).\n menu = addMenubarItem(menu, 'tools', 'a11ycheck', 'count');\n // Finally, hide tiny_accessibilitychecker so there is no confusion.\n menu = removeMenubarItem(menu, 'tools', 'tiny_accessibilitychecker');\n }\n\n return {\n plugins,\n toolbar,\n menu,\n contextmenu,\n ...pluginsettings\n };\n};\n"],"names":["instanceConfig","options","pluginOptions","enabledPremiumPlugins","split","serviceUrls","plugins","menu","toolbar","configureToolbar","contextmenu","pluginsettings","indexOf","editimage_toolbar"],"mappings":"mOA0CyB,CAACA,eAAgBC,iBAGhCC,eAAgB,0CAA8BD,SAC9CE,sBAAwBD,eAAc,gCAAoB,sBAAuB,mBAAmBE,MAAM,SAC5GC,YAAcH,eAAc,gCAAoB,sBAAuB,gBAEvEI,QAAUN,eAAeM,QACzBC,KAAOP,eAAeO,KACtBC,QAhBkBA,CAAAA,UAEtBA,SAAU,4BAAkBA,QAAS,YAAa,YAAY,IACpD,4BAAkBA,QAAS,YAAa,cAAc,IAalDC,CAAiBT,eAAeQ,SAC1CE,YAAcV,eAAeU,YAC7BC,eAAiB,UACrBA,eAAiB,IAAIA,kBAAmBN,cAGW,IAA/CF,sBAAsBS,QAAQ,cAC9BN,qBACAC,MAAO,yBAAeA,KAAM,QAAS,yBAA0B,kBAGf,IAAhDJ,sBAAsBS,QAAQ,eAC9BN,sBACAE,SAAU,2BAAiBA,QAAS,UAAW,YAAa,oBAG5DR,eAAea,kBAAoB,iDAGU,IAA7CV,sBAAsBS,QAAQ,YAC9BN,mBACAC,MAAO,yBAAeA,KAAM,QAAS,cAGW,IAAhDJ,sBAAsBS,QAAQ,eAC9BN,sBACAE,SAAU,2BAAiBA,QAAS,UAAW,YAAa,sBAGX,IAAjDL,sBAAsBS,QAAQ,gBAC9BN,uBACAE,SAAU,2BAAiBA,QAAS,YAAa,gBAGA,IAAjDL,sBAAsBS,QAAQ,gBAC9BN,uBACAE,SAAU,2BAAiBA,QAAS,YAAa,gBAGD,IAAhDL,sBAAsBS,QAAQ,eAC9BN,sBACAE,SAAU,2BAAiBA,QAAS,QAAS,eAGa,IAA1DL,sBAAsBS,QAAQ,yBAC9BN,gCACAC,MAAO,yBAAeA,KAAM,QAAS,mBAAoB,wBACzDG,aAAc,6BAAmBA,YAAa,gBAC9CF,SAAU,2BAAiBA,QAAS,YAAa,sBAGC,IAAlDL,sBAAsBS,QAAQ,iBAC9BN,wBACAC,MAAO,yBAAeA,KAAM,QAAS,+BAAgC,sBAGlB,IAAnDJ,sBAAsBS,QAAQ,kBAC9BN,yBACAC,MAAO,yBAAeA,KAAM,SAAU,wCACtCC,SAAU,2BAAiBA,QAAS,YAAa,gBACjDE,aAAc,6BAAmBA,YAAa,2BAGM,IAApDP,sBAAsBS,QAAQ,mBAC9BN,0BACAE,SAAU,2BAAiBA,QAAS,YAAa,mBAGC,IAAlDL,sBAAsBS,QAAQ,iBAC9BN,wBACAI,aAAc,6BAAmBA,YAAa,iBAGQ,IAAtDP,sBAAsBS,QAAQ,qBAC9BN,4BACAE,SAAU,2BAAiBA,QAAS,YAAa,qBAGN,IAA3CL,sBAAsBS,QAAQ,UAC9BN,iBACAE,SAAU,2BAAiBA,QAAS,YAAa,QACjDD,MAAO,yBAAeA,KAAM,SAAU,OAAQ,qBAGE,IAAhDJ,sBAAsBS,QAAQ,eAC9BN,sBACAE,SAAU,2BAAiBA,QAAS,YAAa,aACjDD,MAAO,yBAAeA,KAAM,SAAU,YAAa,qBAGF,IAAjDJ,sBAAsBS,QAAQ,gBAC9BN,yBAGkD,IAAlDH,sBAAsBS,QAAQ,iBAC9BN,wBACAE,SAAU,2BAAiBA,QAAS,YAAa,aAEjDD,MAAO,4BAAkBA,KAAM,QAAS,aAExCA,MAAO,yBAAeA,KAAM,QAAS,YAAa,SAElDA,MAAO,4BAAkBA,KAAM,QAAS,8BAGrC,CACHD,QAAAA,QACAE,QAAAA,QACAD,KAAAA,KACAG,YAAAA,eACGC"}
\ No newline at end of file
diff --git a/public/lib/editor/tiny/plugins/premium/amd/src/configuration.js b/public/lib/editor/tiny/plugins/premium/amd/src/configuration.js
index d51c572ac60..54fed7e9cae 100644
--- a/public/lib/editor/tiny/plugins/premium/amd/src/configuration.js
+++ b/public/lib/editor/tiny/plugins/premium/amd/src/configuration.js
@@ -45,12 +45,14 @@ export const configure = (instanceConfig, options) => {
// Due to the timing the plugin options are available, we need to get at the options in this slightly unconventional way.
const pluginOptions = getInitialPluginConfiguration(options);
const enabledPremiumPlugins = pluginOptions[getPluginOptionName('tiny_premium/plugin', 'premiumplugins')].split(',');
+ let serviceUrls = pluginOptions[getPluginOptionName('tiny_premium/plugin', 'serviceurls')];
let plugins = instanceConfig.plugins;
let menu = instanceConfig.menu;
let toolbar = configureToolbar(instanceConfig.toolbar);
let contextmenu = instanceConfig.contextmenu;
let pluginsettings = {};
+ pluginsettings = {...pluginsettings, ...serviceUrls};
// Advanced Table.
if (enabledPremiumPlugins.indexOf('advtable') !== -1) {
diff --git a/public/lib/editor/tiny/plugins/premium/classes/form/tiny_premium_settings_form.php b/public/lib/editor/tiny/plugins/premium/classes/form/tiny_premium_settings_form.php
new file mode 100644
index 00000000000..c5201acb35a
--- /dev/null
+++ b/public/lib/editor/tiny/plugins/premium/classes/form/tiny_premium_settings_form.php
@@ -0,0 +1,126 @@
+.
+
+namespace tiny_premium\form;
+
+use moodleform;
+
+/**
+ * Form for configuring Tiny Premium plugin settings.
+ *
+ * @package tiny_premium
+ * @copyright 2025 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tiny_premium_settings_form extends moodleform {
+
+ #[\Override]
+ protected function definition(): void {
+ $mform = $this->_form;
+ $returnurl = $this->_customdata['returnurl'] ?? null;
+ $plugin = $this->_customdata['plugin'] ?? '';
+ $this->add_settings($mform, $plugin);
+
+ $mform->addElement('hidden', 'plugin', $plugin);
+ $mform->setType('plugin', PARAM_ALPHA);
+ if ($returnurl) {
+ $mform->addElement('hidden', 'returnurl', $returnurl);
+ $mform->setType('returnurl', PARAM_LOCALURL);
+ }
+
+ $this->add_action_buttons();
+ }
+
+ /**
+ * Add settings to the form.
+ * This method can be overridden to add additional settings for specific plugins.
+ *
+ * @param \MoodleQuickForm $mform The Moodle QuickForm instance to which settings will be added.
+ * @param string $plugin The plugin name for which settings are being added.
+ */
+ protected function add_settings(
+ \MoodleQuickForm $mform,
+ string $plugin = '',
+ ): void {
+ $pluginname = '';
+ if (!empty($plugin)) {
+ $configs = \tiny_premium\manager::get_plugin_config($plugin);
+ $pluginname = get_string('premiumplugin:' . $plugin, 'tiny_premium');
+ }
+
+ $mform->addElement('header', 'serversidesettingssheader', get_string('serverside:service', 'tiny_premium'));
+ $settingshelp = \html_writer::tag('p', get_string('serverside:desc', 'tiny_premium', $pluginname));
+ $mform->addElement('html', $settingshelp);
+
+ $radiogroup = [
+ $mform->createElement('radio', 'server_side_service', '', get_string('serverside:service:tinycloud', 'tiny_premium'),
+ '1'),
+ $mform->createElement('radio', 'server_side_service', '', get_string('serverside:service:selfhosted', 'tiny_premium'),
+ '2'),
+ ];
+
+ $mform->addGroup($radiogroup, 'server_side_service_group', get_string('serverside:service', 'tiny_premium'), '
',
+ false);
+ if (empty($configs->service_url)) {
+ $mform->setDefault('server_side_service', 1);
+ } else {
+ $mform->setDefault('server_side_service', 2);
+ }
+
+ $mform->addElement('text', 'service_url', get_string('serviceurl', 'tiny_premium'));
+ $mform->setType('service_url', PARAM_TEXT);
+ $mform->hideIf('service_url', 'server_side_service', 'eq', '1');
+ if (isset($configs->service_url)) {
+ $mform->setDefault('service_url', $configs->service_url);
+ }
+
+ $mform->addElement('static', 'description', '', get_string('serviceurl:desc', 'tiny_premium', $pluginname));
+ $mform->hideIf('description', 'server_side_service', 'eq', '1');
+ }
+
+ #[\Override]
+ public function validation($data, $files): array {
+ $errors = parent::validation($data, $files);
+
+ // Validate the service URL if self-hosted service is selected.
+ if (isset($data['server_side_service']) && $data['server_side_service'] == 2) {
+ if (empty($data['service_url'])) {
+ $errors['service_url'] = get_string('required');
+ } else {
+ // Validate the URL format.
+ if (!filter_var($data['service_url'], FILTER_VALIDATE_URL)) {
+ $errors['service_url'] = get_string('invalidurl', 'tiny_premium');
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+ #[\Override]
+ public function get_data(): ?\stdClass {
+ $data = parent::get_data();
+
+ if ($data) {
+ // If the server-side service is Tiny Cloud, unset the service URL.
+ if (isset($data->server_side_service) && $data->server_side_service == 1) {
+ unset($data->service_url);
+ }
+ }
+
+ return $data;
+ }
+}
diff --git a/public/lib/editor/tiny/plugins/premium/classes/local/admin_setting_tiny_premium_plugins.php b/public/lib/editor/tiny/plugins/premium/classes/local/admin_setting_tiny_premium_plugins.php
index 18a32e51ee7..7482be4020e 100644
--- a/public/lib/editor/tiny/plugins/premium/classes/local/admin_setting_tiny_premium_plugins.php
+++ b/public/lib/editor/tiny/plugins/premium/classes/local/admin_setting_tiny_premium_plugins.php
@@ -101,6 +101,7 @@ class admin_setting_tiny_premium_plugins extends \admin_setting {
$table->head = [
get_string('name'),
get_string('enable'),
+ get_string('settings'),
];
$table->colclasses = [
'leftalign',
@@ -112,6 +113,7 @@ class admin_setting_tiny_premium_plugins extends \admin_setting {
$plugins = manager::get_plugins();
$enabledplugins = manager::get_enabled_plugins();
$disabledplugins = array_diff($plugins, $enabledplugins);
+ $serversideplugins = manager::get_server_side_plugins();
$plugins = array_merge($enabledplugins, $disabledplugins);
foreach ($plugins as $plugin) {
@@ -137,10 +139,19 @@ class admin_setting_tiny_premium_plugins extends \admin_setting {
]);
$hideshowlink = \html_writer::link($hideshowurl, $icon);
+ $settinglink = '';
+ // Setting link for server-side plugins.
+ if (array_key_exists($plugin, $serversideplugins)) {
+ $settinglink = \html_writer::link(new \moodle_url('/lib/editor/tiny/plugins/premium/extrasettings.php', [
+ 'plugin' => $plugin,
+ ]), get_string('settings'));
+ }
+
// Populate table row.
$row = new \html_table_row([
$pluginname,
$hideshowlink,
+ $settinglink,
]);
$row->attributes['class'] = $class;
$table->data[] = $row;
diff --git a/public/lib/editor/tiny/plugins/premium/classes/manager.php b/public/lib/editor/tiny/plugins/premium/classes/manager.php
index a5e421d89b0..6560170c0f2 100644
--- a/public/lib/editor/tiny/plugins/premium/classes/manager.php
+++ b/public/lib/editor/tiny/plugins/premium/classes/manager.php
@@ -16,6 +16,10 @@
namespace tiny_premium;
+use moodle_url;
+use moodleform;
+use stdClass;
+
/**
* Tiny Premium manager.
*
@@ -25,6 +29,9 @@ namespace tiny_premium;
*/
class manager {
+ /** @var string Tiny Premium plugin prefix. */
+ const PLUGIN_NAME_PREFIX = 'tiny_premium_';
+
/**
* Get all Tiny Premium plugins currently supported.
*
@@ -54,6 +61,24 @@ class manager {
];
}
+ /**
+ * Get all Tiny Premium plugins that require server-side processing.
+ *
+ * This method returns an associative array where the keys are the plugin names
+ * and the values are the corresponding configuration keys for the server-side
+ * processing URLs. These plugins require additional server-side services to
+ * function properly.
+ *
+ * @return string[] An associative array of server-side plugins and their configuration keys.
+ */
+ public static function get_server_side_plugins(): array {
+ return [
+ 'editimage' => 'editimage_proxy_service_url',
+ 'linkchecker' => 'linkchecker_service_url',
+ 'tinymcespellchecker' => 'spellchecker_rpc_url',
+ ];
+ }
+
/**
* Get enabled Tiny Premium plugins.
*
@@ -77,7 +102,8 @@ class manager {
* @return bool Return true if enabled.
*/
public static function is_plugin_enabled(string $plugin): bool {
- $config = get_config('tiny_premium_' . $plugin, 'enabled');
+ $plugin = self::get_formatted_plugin_name($plugin);
+ $config = get_config($plugin, 'enabled');
return ($config == 1);
}
@@ -93,19 +119,97 @@ class manager {
return;
}
- $plugin = 'tiny_premium_' . $plugin;
-
+ $formattedplugin = self::get_formatted_plugin_name($plugin);
+ $unsetconfigs = [];
foreach ($data as $key => $newvalue) {
// Get the old value for the log.
- $oldvalue = get_config($plugin, $key) ?? null;
- add_to_config_log($key, $oldvalue, $newvalue, $plugin);
+ $oldvalue = get_config($formattedplugin, $key) ?? null;
+ add_to_config_log($key, $oldvalue, $newvalue, $formattedplugin);
// If we are disabling the plugin, remove it, otherwise, set the new value.
if ($key === 'enabled' && $newvalue == 0) {
- unset_config($key, $plugin);
+ $unsetconfigs[] = $key;
} else {
- set_config($key, $newvalue, $plugin);
+ set_config($key, $newvalue, $formattedplugin);
}
}
+
+ self::unset_plugin_config($unsetconfigs, $plugin);
+ }
+
+ /**
+ * Get values for a Tiny Premium plugin config.
+ *
+ * @param string $plugin The plugin to get.
+ * @param string|null $name The name of the config to get.
+ * @return mixed hash-like object or single value, return false no config found.
+ */
+ public static function get_plugin_config(
+ string $plugin,
+ ?string $name = null,
+ ): mixed {
+ // Check this is a valid premium plugin.
+ if (!in_array($plugin, self::get_plugins())) {
+ return false;
+ }
+ $plugin = self::get_formatted_plugin_name($plugin);
+ return get_config($plugin, $name);
+ }
+
+ /**
+ * Unset a Tiny Premium plugin configs.
+ *
+ * @param array $names List of config names to unset.
+ * @param string $plugin The plugin to use.
+ */
+ public static function unset_plugin_config(
+ array $names,
+ string $plugin,
+ ): void {
+ // Check this is a valid premium plugin.
+ if (!in_array($plugin, self::get_plugins())) {
+ return;
+ }
+
+ $plugin = self::get_formatted_plugin_name($plugin);
+ foreach ($names as $key) {
+ unset_config($key, $plugin);
+ }
+ }
+
+ /**
+ * Get the settings form for a Tiny Premium plugin.
+ * This method will check if a custom form class exists for the plugin,
+ * otherwise it will use the default settings form.
+ *
+ * @param string $plugin The plugin to get the settings form for.
+ * @param moodle_url|null $return The URL to return to after saving the form.
+ * @return moodleform Settings form for the Tiny Premium plugin.
+ */
+ public static function get_settings_form(
+ string $plugin,
+ ?moodle_url $return = null,
+ ): moodleform {
+ $formclassname = '\tiny_premium\form\tiny_premium_' . $plugin . '_settings_form';
+ if (!class_exists($formclassname)) {
+ $formclassname = '\tiny_premium\form\tiny_premium_settings_form';
+ }
+
+ return new $formclassname(null, [
+ 'plugin' => $plugin,
+ 'returnurl' => $return,
+ ]);
+ }
+
+ /**
+ * Get the formatted plugin name for config.
+ *
+ * @param string $plugin The plugin to format.
+ * @return string The formatted plugin name.
+ */
+ public static function get_formatted_plugin_name(
+ string $plugin,
+ ): string {
+ return self::PLUGIN_NAME_PREFIX . $plugin;
}
}
diff --git a/public/lib/editor/tiny/plugins/premium/classes/plugininfo.php b/public/lib/editor/tiny/plugins/premium/classes/plugininfo.php
index 72711bf1b82..8faf6cdc545 100644
--- a/public/lib/editor/tiny/plugins/premium/classes/plugininfo.php
+++ b/public/lib/editor/tiny/plugins/premium/classes/plugininfo.php
@@ -21,7 +21,6 @@ use editor_tiny\editor;
use editor_tiny\plugin;
use editor_tiny\plugin_with_configuration;
use editor_tiny\plugin_with_configuration_for_external;
-use tiny_premium\manager;
/**
* Tiny Premium plugin.
@@ -58,15 +57,22 @@ class plugininfo extends plugin implements plugin_with_configuration, plugin_wit
?editor $editor = null
): array {
$allowedplugins = [];
+ $serviceurls = [];
+ $serverplugins = manager::get_server_side_plugins();
foreach (manager::get_enabled_plugins() as $plugin) {
if (has_capability("tiny/premium:use{$plugin}", $context)) {
$allowedplugins[] = $plugin;
}
+ $serviceurl = get_config(manager::get_formatted_plugin_name($plugin), 'service_url');
+ if ($serviceurl && isset($serverplugins[$plugin])) {
+ $serviceurls[$serverplugins[$plugin]] = $serviceurl;
+ }
}
return [
'premiumplugins' => implode(',', $allowedplugins),
+ 'serviceurls' => $serviceurls,
];
}
@@ -75,6 +81,7 @@ class plugininfo extends plugin implements plugin_with_configuration, plugin_wit
$settings = self::get_plugin_configuration_for_context($context, [], []);
return [
'premiumplugins' => $settings['premiumplugins'],
+ 'serviceurls' => $settings['serviceurls'],
];
}
}
diff --git a/public/lib/editor/tiny/plugins/premium/extrasettings.php b/public/lib/editor/tiny/plugins/premium/extrasettings.php
new file mode 100644
index 00000000000..c5d8c3aa4dc
--- /dev/null
+++ b/public/lib/editor/tiny/plugins/premium/extrasettings.php
@@ -0,0 +1,66 @@
+.
+
+/**
+ * TinyMCE Premium plugins settings page.
+ *
+ * @package tiny_premium
+ * @copyright 2025 Huong Nguyen
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+// This is an admin page.
+admin_externalpage_setup('tiny_premium_plugin_settings');
+$return = new moodle_url('/admin/settings.php', ['section' => 'tiny_premium_settings']);
+$plugin = optional_param('plugin', '', PARAM_ALPHA);
+
+// Get form class.
+$form = \tiny_premium\manager::get_settings_form(
+ plugin: $plugin,
+ return: $return,
+);
+if ($form->is_cancelled()) {
+ redirect($return);
+} else if ($data = $form->get_data()) {
+ if (isset($data->service_url) && $data->service_url !== '') {
+ \tiny_premium\manager::set_plugin_config(
+ data: ['service_url' => $data->service_url],
+ plugin: $data->plugin,
+ );
+ } else {
+ \tiny_premium\manager::unset_plugin_config(
+ names: ['service_url'],
+ plugin: $data->plugin,
+ );
+ }
+ redirect($return, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
+}
+
+// Get all Tiny Premium plugins.
+$premiumplugins = \tiny_premium\manager::get_plugins();
+if (!in_array($plugin, $premiumplugins)) {
+ throw new moodle_exception('pluginnotfound', 'tiny_premium', $return, $plugin);
+}
+$pluginname = get_string('premiumplugin:' . $plugin, 'tiny_premium');
+
+// Display the page.
+echo $OUTPUT->header();
+echo $OUTPUT->heading($pluginname);
+$form->display();
+echo $OUTPUT->footer();
diff --git a/public/lib/editor/tiny/plugins/premium/lang/en/tiny_premium.php b/public/lib/editor/tiny/plugins/premium/lang/en/tiny_premium.php
index 43845eed011..d6d1e33f7d8 100644
--- a/public/lib/editor/tiny/plugins/premium/lang/en/tiny_premium.php
+++ b/public/lib/editor/tiny/plugins/premium/lang/en/tiny_premium.php
@@ -29,6 +29,7 @@ $string['accessibilitycheckerinfo'] = 'The TinyMCE Premium Accessibility Checker
$string['apikey'] = 'API key';
$string['apikey_desc'] = 'Your API key is available on your Tiny Cloud account page if you have purchased a subscription, or if you are on a free trial.';
$string['emptyapikeywarning'] = 'Enabled TinyMCE Premium plugins will not be available until an API key is added.';
+$string['invalidurl'] = 'Entered URL is invalid';
$string['pluginname'] = 'TinyMCE Premium';
$string['pluginnotfound'] = 'Tiny Premium plugin {$a} not found.';
$string['premium:use'] = 'Use TinyMCE Premium';
@@ -68,7 +69,14 @@ $string['premiumplugin:tinymcespellchecker'] = 'Spell Checker Pro';
$string['premiumplugin:typography'] = 'Advanced Typography';
$string['premiumplugins'] = 'Premium plugins';
$string['premiumplugins_desc'] = 'Access to TinyMCE Premium plugins requires an API key. Not all listed plugins may be available with your TinyMCE Premium subscription. You can check available plugins on your Tiny Cloud account page.';
+$string['premiumplugins_settings'] = 'TinyMCE Premium plugins settings';
$string['privacy:metadata'] = 'The Tiny Premium plugin for TinyMCE does not store any personal data.';
+$string['serverside:desc'] = 'The {$a} plugin uses a server-side service to process data. You can choose to use the Tiny Cloud service, or connect to your own self-hosted instance using a service URL.';
+$string['serverside:service'] = 'Server-side service';
+$string['serverside:service:selfhosted'] = 'Use self-hosted service';
+$string['serverside:service:tinycloud'] = 'Use Tiny Cloud';
+$string['serviceurl'] = 'Service URL';
+$string['serviceurl:desc'] = 'Enter the URL of your self-hosted {$a} service. View setup guide from Tiny.';
// Deprecated since Moodle 4.5.
$string['helplinktext'] = 'Premium plugins';
diff --git a/public/lib/editor/tiny/plugins/premium/settings.php b/public/lib/editor/tiny/plugins/premium/settings.php
index 128b3debb99..90d3e222fd4 100644
--- a/public/lib/editor/tiny/plugins/premium/settings.php
+++ b/public/lib/editor/tiny/plugins/premium/settings.php
@@ -40,4 +40,14 @@ if ($hassiteconfig) {
// Set individual Tiny Premium plugins.
$settings->add(new \tiny_premium\local\admin_setting_tiny_premium_plugins());
}
+
+ // Extra settings for on-premise plugins (hidden, accessed via direct link from the settings page).
+ $page = new admin_externalpage(
+ name: 'tiny_premium_plugin_settings',
+ visiblename: get_string('premiumplugins_settings', 'tiny_premium'),
+ url: new moodle_url('/lib/editor/tiny/plugins/premium/extrasettings.php'),
+ req_capability: 'moodle/site:config',
+ hidden: true,
+ );
+ $ADMIN->add('editortiny', $page);
}
diff --git a/public/lib/editor/tiny/plugins/premium/tests/behat/tiny_premium_settings.feature b/public/lib/editor/tiny/plugins/premium/tests/behat/tiny_premium_settings.feature
index 1232f7e5ae4..87d8f6ed9fc 100644
--- a/public/lib/editor/tiny/plugins/premium/tests/behat/tiny_premium_settings.feature
+++ b/public/lib/editor/tiny/plugins/premium/tests/behat/tiny_premium_settings.feature
@@ -27,3 +27,29 @@ Feature: Check the features of the TinyMCE Premium settings
When I click on "Enable Accessibility Checker" "link"
Then I should see "Accessibility Checker enabled."
And I should see "The TinyMCE Premium Accessibility Checker will override the default Accessibility Checker for users who have access to it."
+
+ @javascript
+ Scenario Outline: I can set service URL for the TinyMCE Premium plugins
+ Given the following config values are set as admin:
+ | apikey | "123456" | tiny_premium |
+ And "Settings" "link" should exist in the "" "table_row"
+ When I click on "Settings" "link" in the "" "table_row"
+ Then I should see "The plugin uses a server-side service to process data. You can choose to use the Tiny Cloud service, or connect to your own self-hosted instance using a service URL."
+ And I should see "Use Tiny Cloud"
+ And I should see "Use self-hosted service"
+ And "Service URL" "field" should not be visible
+ And I click on "Use self-hosted service" "radio"
+ And "Service URL" "field" should be visible
+ And I should see "Enter the URL of your self-hosted service"
+ And I set the field "Service URL" to ""
+ And I click on "Save changes" "button"
+ And I should see "Changes saved"
+ And I click on "Settings" "link" in the "" "table_row"
+ And "Service URL" "field" should be visible
+ And the field "Service URL" matches value ""
+
+ Examples:
+ | plugin | serviceurl |
+ | Enhanced Image Editing | http://moodle.test:8080/ephox-image-proxy |
+ | Link Checker | http://moodle.test:8080/ephox-hyperlinking |
+ | Spell Checker Pro | http://moodle.test:8080/ephox-spelling |
diff --git a/public/lib/editor/tiny/plugins/premium/tests/plugininfo_test.php b/public/lib/editor/tiny/plugins/premium/tests/plugininfo_test.php
index 11831b68dde..155019bb3eb 100644
--- a/public/lib/editor/tiny/plugins/premium/tests/plugininfo_test.php
+++ b/public/lib/editor/tiny/plugins/premium/tests/plugininfo_test.php
@@ -55,9 +55,9 @@ final class plugininfo_test extends advanced_testcase {
$context = \context_system::instance();
$this->setUser($user);
- $this->assertEquals(
- ['premiumplugins' => implode(',', \tiny_premium\manager::get_plugins())],
- plugininfo::get_plugin_configuration_for_external($context)
- );
+ $configs = plugininfo::get_plugin_configuration_for_external($context);
+ $this->assertArrayHasKey('premiumplugins', $configs);
+ $this->assertArrayHasKey('serviceurls', $configs);
+ $this->assertEquals(implode(',', \tiny_premium\manager::get_plugins()), $configs['premiumplugins']);
}
}