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
This commit is contained in:
Huong Nguyen
2025-08-28 11:07:22 +07:00
parent 5ed43130d6
commit b2b195cfff
9 changed files with 61 additions and 5 deletions
@@ -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
@@ -5,6 +5,6 @@ define("tiny_premium/plugin",["exports","editor_tiny/loader","editor_tiny/utils"
* @module tiny_premium/plugin
* @copyright 2023 David Woloszyn <david.woloszyn@moodle.com>
* @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
@@ -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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Premium plugin for Moodle.\n *\n * @module tiny_premium/plugin\n * @copyright 2023 David Woloszyn <[email protected]>\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"}
{"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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Premium plugin for Moodle.\n *\n * @module tiny_premium/plugin\n * @copyright 2023 David Woloszyn <[email protected]>\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"}
@@ -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]);
});
@@ -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'),
]);
}
}
@@ -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'));
}
@@ -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.
@@ -30,8 +30,13 @@ $string['apikey'] = 'API key';
$string['apikey_desc'] = 'Your API key is available on your <a href="https://www.tiny.cloud">Tiny Cloud</a> 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 <a href="{$a}">TinyMCE General settings</a> 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 <a href="{$a}">TinyMCE General settings</a> page.';
$string['premium:use'] = 'Use TinyMCE Premium';
$string['premium:usea11ychecker'] = 'Use TinyMCE Premium Accessibility Checker';
$string['premium:useadvtable'] = 'Use TinyMCE Premium Advanced Table';
@@ -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());