From b2b195cfff98d31d7b63a31b93f2ae3032d9f796 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Mon, 18 Aug 2025 15:15:30 +0700 Subject: [PATCH] MDL-85727 tiny_premium: Support On-Premise version Users can now select whether to use the Cloud-hosted version or the On-Premise version of Premium plugins, providing flexibility ased on their deployment preferences --- .upgradenotes/MDL-85727-2025081810204729.yml | 8 +++++ .../plugins/premium/amd/build/plugin.min.js | 2 +- .../premium/amd/build/plugin.min.js.map | 2 +- .../tiny/plugins/premium/amd/src/plugin.js | 6 ++-- .../premium/classes/external/get_api_key.php | 3 ++ .../admin_setting_tiny_premium_plugins.php | 3 +- .../tiny/plugins/premium/classes/manager.php | 4 +++ .../plugins/premium/lang/en/tiny_premium.php | 5 +++ .../editor/tiny/plugins/premium/settings.php | 33 +++++++++++++++++++ 9 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 .upgradenotes/MDL-85727-2025081810204729.yml diff --git a/.upgradenotes/MDL-85727-2025081810204729.yml b/.upgradenotes/MDL-85727-2025081810204729.yml new file mode 100644 index 00000000000..c5239aa7684 --- /dev/null +++ b/.upgradenotes/MDL-85727-2025081810204729.yml @@ -0,0 +1,8 @@ +issueNumber: MDL-85727 +notes: + tiny_premium: + - message: > + The `tiny_premium_get_api_key` web service now returns an additional + field `usecloud` to indicate whether the cloud version or self-hosted + version of Tiny Premium plugins should be used. + type: improved diff --git a/public/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js b/public/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js index e63cf49e282..18cf2e4ee2d 100644 --- a/public/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js +++ b/public/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js @@ -5,6 +5,6 @@ define("tiny_premium/plugin",["exports","editor_tiny/loader","editor_tiny/utils" * @module tiny_premium/plugin * @copyright 2023 David Woloszyn * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */(Configuration);const currentContextId=(_config=(obj=_config)&&obj.__esModule?obj:{default:obj}).default.contextid;var _default=new Promise((async resolve=>{const[tinyMCE,pluginMetadata,externalData]=await Promise.all([(0,_loader.getTinyMCE)(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName),(0,_external.getApiKey)(currentContextId)]);tinyMCE.PluginManager.add("".concat(_common.component,"/plugin"),(()=>pluginMetadata)),await getTinyPremium(tinyMCE,externalData.apikey),resolve(["".concat(_common.component,"/plugin"),Configuration])}));let tinyPremiumPromise;_exports.default=_default;const getTinyPremium=(tinyMCE,apikey)=>tinyPremiumPromise||(tinyPremiumPromise=new Promise(((resolve,reject)=>{const head=document.querySelector("head"),script=document.createElement("script"),tinyVersion="".concat(tinyMCE.majorVersion,".").concat(tinyMCE.minorVersion);script.dataset.tinymce="premium",script.src="https://cdn.tiny.cloud/1/".concat(apikey,"/tinymce/").concat(tinyVersion,"/plugins.min.js"),script.referrerpolicy="origin",script.addEventListener("load",(()=>{resolve()}),!1),script.addEventListener("error",(err=>{reject(err)}),!1),head.append(script)})),tinyPremiumPromise);return _exports.default})); + */(Configuration);const currentContextId=(_config=(obj=_config)&&obj.__esModule?obj:{default:obj}).default.contextid;var _default=new Promise((async resolve=>{const[tinyMCE,pluginMetadata,externalData]=await Promise.all([(0,_loader.getTinyMCE)(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName),(0,_external.getApiKey)(currentContextId)]);tinyMCE.PluginManager.add("".concat(_common.component,"/plugin"),(()=>pluginMetadata)),externalData.usecloud&&await getTinyPremium(tinyMCE,externalData.apikey),resolve(["".concat(_common.component,"/plugin"),Configuration])}));let tinyPremiumPromise;_exports.default=_default;const getTinyPremium=(tinyMCE,apikey)=>tinyPremiumPromise||(tinyPremiumPromise=new Promise(((resolve,reject)=>{const head=document.querySelector("head"),script=document.createElement("script"),tinyVersion="".concat(tinyMCE.majorVersion,".").concat(tinyMCE.minorVersion);script.dataset.tinymce="premium",script.src="https://cdn.tiny.cloud/1/".concat(apikey,"/tinymce/").concat(tinyVersion,"/plugins.min.js"),script.referrerpolicy="origin",script.addEventListener("load",(()=>{resolve()}),!1),script.addEventListener("error",(err=>{reject(err)}),!1),head.append(script)})),tinyPremiumPromise);return _exports.default})); //# sourceMappingURL=plugin.min.js.map \ No newline at end of file diff --git a/public/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js.map b/public/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js.map index 3ca269fc261..0845139972f 100644 --- a/public/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js.map +++ b/public/lib/editor/tiny/plugins/premium/amd/build/plugin.min.js.map @@ -1 +1 @@ -{"version":3,"file":"plugin.min.js","sources":["../src/plugin.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 plugin for Moodle.\n *\n * @module tiny_premium/plugin\n * @copyright 2023 David Woloszyn \n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getTinyMCE} from 'editor_tiny/loader';\nimport {getPluginMetadata} from 'editor_tiny/utils';\nimport {component, pluginName} from 'tiny_premium/common';\nimport * as Configuration from 'tiny_premium/configuration';\nimport {getApiKey} from 'tiny_premium/external';\nimport Config from 'core/config';\n\nconst currentContextId = Config.contextid;\n\n// eslint-disable-next-line no-async-promise-executor\nexport default new Promise(async(resolve) => {\n const [\n tinyMCE,\n pluginMetadata,\n externalData\n ] = await Promise.all([\n getTinyMCE(),\n getPluginMetadata(component, pluginName),\n getApiKey(currentContextId)\n ]);\n\n tinyMCE.PluginManager.add(`${component}/plugin`, () => {\n return pluginMetadata;\n });\n\n // Load the Tiny Premium script using the provided API key.\n await getTinyPremium(tinyMCE, externalData.apikey);\n\n resolve([`${component}/plugin`, Configuration]);\n});\n\nlet tinyPremiumPromise;\n\n/**\n * Promise for Tiny Premium plugins script.\n *\n * @param {TinyMCE} tinyMCE\n * @param {string} apikey\n * @return {Promise}\n */\nconst getTinyPremium = (tinyMCE, apikey) => {\n if (tinyPremiumPromise) {\n return tinyPremiumPromise;\n }\n\n tinyPremiumPromise = new Promise((resolve, reject) => {\n const head = document.querySelector('head');\n const script = document.createElement('script');\n const tinyVersion = `${tinyMCE.majorVersion}.${tinyMCE.minorVersion}`;\n\n script.dataset.tinymce = 'premium';\n script.src = `https://cdn.tiny.cloud/1/${apikey}/tinymce/${tinyVersion}/plugins.min.js`;\n script.referrerpolicy = \"origin\";\n\n script.addEventListener('load', () => {\n resolve();\n }, false);\n\n script.addEventListener('error', (err) => {\n reject(err);\n }, false);\n\n head.append(script);\n });\n\n return tinyPremiumPromise;\n};\n"],"names":["currentContextId","contextid","Promise","async","tinyMCE","pluginMetadata","externalData","all","component","pluginName","PluginManager","add","getTinyPremium","apikey","resolve","Configuration","tinyPremiumPromise","reject","head","document","querySelector","script","createElement","tinyVersion","majorVersion","minorVersion","dataset","tinymce","src","referrerpolicy","addEventListener","err","append"],"mappings":";;;;;;;2BA8BMA,mFAA0BC,uBAGjB,IAAIC,SAAQC,MAAAA,gBAEnBC,QACAC,eACAC,oBACMJ,QAAQK,IAAI,EAClB,yBACA,4BAAkBC,kBAAWC,qBAC7B,uBAAUT,oBAGdI,QAAQM,cAAcC,cAAOH,8BAAoB,IACtCH,uBAILO,eAAeR,QAASE,aAAaO,QAE3CC,QAAQ,WAAIN,6BAAoBO,uBAGhCC,mDASEJ,eAAiB,CAACR,QAASS,SACzBG,qBAIJA,mBAAqB,IAAId,SAAQ,CAACY,QAASG,gBACjCC,KAAOC,SAASC,cAAc,QAC9BC,OAASF,SAASG,cAAc,UAChCC,sBAAiBnB,QAAQoB,yBAAgBpB,QAAQqB,cAEvDJ,OAAOK,QAAQC,QAAU,UACzBN,OAAOO,uCAAkCf,2BAAkBU,+BAC3DF,OAAOQ,eAAiB,SAExBR,OAAOS,iBAAiB,QAAQ,KAC5BhB,aACD,GAEHO,OAAOS,iBAAiB,SAAUC,MAC9Bd,OAAOc,QACR,GAEHb,KAAKc,OAAOX,WAGTL"} \ No newline at end of file +{"version":3,"file":"plugin.min.js","sources":["../src/plugin.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 plugin for Moodle.\n *\n * @module tiny_premium/plugin\n * @copyright 2023 David Woloszyn \n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getTinyMCE} from 'editor_tiny/loader';\nimport {getPluginMetadata} from 'editor_tiny/utils';\nimport {component, pluginName} from 'tiny_premium/common';\nimport * as Configuration from 'tiny_premium/configuration';\nimport {getApiKey} from 'tiny_premium/external';\nimport Config from 'core/config';\n\nconst currentContextId = Config.contextid;\n\n// eslint-disable-next-line no-async-promise-executor\nexport default new Promise(async(resolve) => {\n const [\n tinyMCE,\n pluginMetadata,\n externalData\n ] = await Promise.all([\n getTinyMCE(),\n getPluginMetadata(component, pluginName),\n getApiKey(currentContextId)\n ]);\n\n tinyMCE.PluginManager.add(`${component}/plugin`, () => {\n return pluginMetadata;\n });\n\n if (externalData.usecloud) {\n // Load the Tiny Premium script using the provided API key.\n await getTinyPremium(tinyMCE, externalData.apikey);\n }\n\n resolve([`${component}/plugin`, Configuration]);\n});\n\nlet tinyPremiumPromise;\n\n/**\n * Promise for Tiny Premium plugins script.\n *\n * @param {TinyMCE} tinyMCE\n * @param {string} apikey\n * @return {Promise}\n */\nconst getTinyPremium = (tinyMCE, apikey) => {\n if (tinyPremiumPromise) {\n return tinyPremiumPromise;\n }\n\n tinyPremiumPromise = new Promise((resolve, reject) => {\n const head = document.querySelector('head');\n const script = document.createElement('script');\n const tinyVersion = `${tinyMCE.majorVersion}.${tinyMCE.minorVersion}`;\n\n script.dataset.tinymce = 'premium';\n script.src = `https://cdn.tiny.cloud/1/${apikey}/tinymce/${tinyVersion}/plugins.min.js`;\n script.referrerpolicy = \"origin\";\n\n script.addEventListener('load', () => {\n resolve();\n }, false);\n\n script.addEventListener('error', (err) => {\n reject(err);\n }, false);\n\n head.append(script);\n });\n\n return tinyPremiumPromise;\n};\n"],"names":["currentContextId","contextid","Promise","async","tinyMCE","pluginMetadata","externalData","all","component","pluginName","PluginManager","add","usecloud","getTinyPremium","apikey","resolve","Configuration","tinyPremiumPromise","reject","head","document","querySelector","script","createElement","tinyVersion","majorVersion","minorVersion","dataset","tinymce","src","referrerpolicy","addEventListener","err","append"],"mappings":";;;;;;;2BA8BMA,mFAA0BC,uBAGjB,IAAIC,SAAQC,MAAAA,gBAEnBC,QACAC,eACAC,oBACMJ,QAAQK,IAAI,EAClB,yBACA,4BAAkBC,kBAAWC,qBAC7B,uBAAUT,oBAGdI,QAAQM,cAAcC,cAAOH,8BAAoB,IACtCH,iBAGPC,aAAaM,gBAEPC,eAAeT,QAASE,aAAaQ,QAG/CC,QAAQ,WAAIP,6BAAoBQ,uBAGhCC,mDASEJ,eAAiB,CAACT,QAASU,SACzBG,qBAIJA,mBAAqB,IAAIf,SAAQ,CAACa,QAASG,gBACjCC,KAAOC,SAASC,cAAc,QAC9BC,OAASF,SAASG,cAAc,UAChCC,sBAAiBpB,QAAQqB,yBAAgBrB,QAAQsB,cAEvDJ,OAAOK,QAAQC,QAAU,UACzBN,OAAOO,uCAAkCf,2BAAkBU,+BAC3DF,OAAOQ,eAAiB,SAExBR,OAAOS,iBAAiB,QAAQ,KAC5BhB,aACD,GAEHO,OAAOS,iBAAiB,SAAUC,MAC9Bd,OAAOc,QACR,GAEHb,KAAKc,OAAOX,WAGTL"} \ No newline at end of file diff --git a/public/lib/editor/tiny/plugins/premium/amd/src/plugin.js b/public/lib/editor/tiny/plugins/premium/amd/src/plugin.js index b73155667d6..d200945f506 100644 --- a/public/lib/editor/tiny/plugins/premium/amd/src/plugin.js +++ b/public/lib/editor/tiny/plugins/premium/amd/src/plugin.js @@ -46,8 +46,10 @@ export default new Promise(async(resolve) => { return pluginMetadata; }); - // Load the Tiny Premium script using the provided API key. - await getTinyPremium(tinyMCE, externalData.apikey); + if (externalData.usecloud) { + // Load the Tiny Premium script using the provided API key. + await getTinyPremium(tinyMCE, externalData.apikey); + } resolve([`${component}/plugin`, Configuration]); }); diff --git a/public/lib/editor/tiny/plugins/premium/classes/external/get_api_key.php b/public/lib/editor/tiny/plugins/premium/classes/external/get_api_key.php index fc3e9a65987..4a2c4c635ac 100644 --- a/public/lib/editor/tiny/plugins/premium/classes/external/get_api_key.php +++ b/public/lib/editor/tiny/plugins/premium/classes/external/get_api_key.php @@ -21,6 +21,7 @@ use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; +use tiny_premium\manager; /** * External API for Tiny Premium. @@ -61,6 +62,7 @@ class get_api_key extends external_api { self::validate_context($context); return [ 'apikey' => get_config('tiny_premium', 'apikey'), + 'usecloud' => get_config('tiny_premium', 'plugin_source') !== manager::PACKAGE_SELF_HOSTED, ]; } @@ -73,6 +75,7 @@ class get_api_key extends external_api { public static function execute_returns(): external_single_structure { return new external_single_structure([ 'apikey' => new external_value(PARAM_ALPHANUM, 'The API key for Tiny Premium'), + 'usecloud' => new external_value(PARAM_BOOL, 'Whether to use the cloud version of Tiny Premium plugins or self-hosted'), ]); } } 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 7482be4020e..6566d9bd54f 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 @@ -72,7 +72,8 @@ class admin_setting_tiny_premium_plugins extends \admin_setting { $return = ''; // Warn users about an empty API key when displaying enabled plugins. - if (empty(get_config('tiny_premium', 'apikey')) && !empty(manager::get_enabled_plugins())) { + if (get_config('tiny_premium', 'plugin_source') == manager::PACKAGE_CLOUD && empty(get_config('tiny_premium', 'apikey')) && + !empty(manager::get_enabled_plugins())) { $return .= \core\notification::warning(get_string('emptyapikeywarning', 'tiny_premium')); } diff --git a/public/lib/editor/tiny/plugins/premium/classes/manager.php b/public/lib/editor/tiny/plugins/premium/classes/manager.php index 6560170c0f2..17554ed206e 100644 --- a/public/lib/editor/tiny/plugins/premium/classes/manager.php +++ b/public/lib/editor/tiny/plugins/premium/classes/manager.php @@ -31,6 +31,10 @@ class manager { /** @var string Tiny Premium plugin prefix. */ const PLUGIN_NAME_PREFIX = 'tiny_premium_'; + /** @var int Indicates usage of self-hosted TinyMCE packages. */ + public const PACKAGE_SELF_HOSTED = 2; + /** @var int Indicates usage of cloud-hosted TinyMCE packages. */ + public const PACKAGE_CLOUD = 1; /** * Get all Tiny Premium plugins currently supported. 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 aee5d927748..dee7d2aa4b3 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 @@ -30,8 +30,13 @@ $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'] = 'Wrong URL'; +$string['invalidpath'] = 'You have not provided a valid path to the TinyMCE standalone package. Please check the TinyMCE General settings page.'; $string['pluginname'] = 'TinyMCE Premium'; $string['pluginnotfound'] = 'Tiny Premium plugin {$a} not found.'; +$string['pluginsource'] = 'Plugin source'; +$string['pluginsource:cloud'] = 'Tiny Cloud'; +$string['pluginsource:selfhosted'] = 'Self-hosted'; +$string['pluginsource_desc'] = 'Select the source of the Tiny Premium plugins. If you select \'Tiny Cloud\', you will need to provide an API key. If you select \'Self-hosted\', you will need to provide a valid path to TinyMCE in the TinyMCE General settings page.'; $string['premium:use'] = 'Use TinyMCE Premium'; $string['premium:usea11ychecker'] = 'Use TinyMCE Premium Accessibility Checker'; $string['premium:useadvtable'] = 'Use TinyMCE Premium Advanced Table'; diff --git a/public/lib/editor/tiny/plugins/premium/settings.php b/public/lib/editor/tiny/plugins/premium/settings.php index 90d3e222fd4..692ee5df294 100644 --- a/public/lib/editor/tiny/plugins/premium/settings.php +++ b/public/lib/editor/tiny/plugins/premium/settings.php @@ -23,11 +23,38 @@ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use tiny_premium\manager; + defined('MOODLE_INTERNAL') || die(); if ($hassiteconfig) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf if ($ADMIN->fulltree) { + $sourcepathurl = new moodle_url('/admin/settings.php', ['section' => 'editorsettingstiny']); + $setting = new admin_setting_configselect( + 'tiny_premium/plugin_source', + new lang_string('pluginsource', 'tiny_premium'), + new lang_string('pluginsource_desc', 'tiny_premium', $sourcepathurl->out(false)), + manager::PACKAGE_CLOUD, + [ + manager::PACKAGE_CLOUD => new lang_string('pluginsource:cloud', 'tiny_premium'), + manager::PACKAGE_SELF_HOSTED => new lang_string('pluginsource:selfhosted', 'tiny_premium'), + ], + ); + $setting->set_validate_function(function(int $value): string { + if ($value == manager::PACKAGE_SELF_HOSTED) { + // If the self-hosted package is selected, we need to check if the standalone path is set. + // If not, we return a warning message. + $packagesource = get_config('editor_tiny', 'package_source'); + $standalonepath = get_config('editor_tiny', 'package_source_standalone_path'); + if ($packagesource != manager::PACKAGE_SELF_HOSTED || empty($standalonepath)) { + return get_string('package_source_standalone_path_invalid', 'editor_tiny'); + } + } + return ''; + }); + $settings->add($setting); + // Set API key. $setting = new admin_setting_configpasswordunmask( 'tiny_premium/apikey', @@ -36,6 +63,12 @@ if ($hassiteconfig) { '', ); $settings->add($setting); + $settings->hide_if( + 'tiny_premium/apikey', + 'tiny_premium/plugin_source', + 'eq', + manager::PACKAGE_SELF_HOSTED, + ); // Set individual Tiny Premium plugins. $settings->add(new \tiny_premium\local\admin_setting_tiny_premium_plugins());