mirror of
https://github.com/moodle/moodle.git
synced 2025-03-20 07:30:01 +01:00
MDL-75271 editor_tiny: Add a cache-busting loader for TinyMCE
Part of MDL-75966 This commit adds a cache-busting loader API for use in the TinyMCE plugin. This is not for use in any TinyMCE subplugins at this time as we have no use-case outside of AMD modules. This loader ensures that only files within the js/tiny directory are loaded, and it only supports either .js or .css files at this time. The client-side of the loader makes use of the jsrevision as a cache-buster, including for CSS files included with TinyMCE. If the revision is negative, then files are not cached. If the revision is positive, then the requested file is cached in a candidate file and served using aggressive cache headers.
This commit is contained in:
parent
d8cf77a127
commit
90c40fba5d
2
lib/editor/tiny/amd/build/editor.min.js
vendored
2
lib/editor/tiny/amd/build/editor.min.js
vendored
@ -1,3 +1,3 @@
|
||||
define("editor_tiny/editor",["exports","./loader","core/pending"],(function(_exports,_loader,_pending){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupForTarget=_exports.setupForElementId=_exports.getInstanceForElementId=_exports.getInstanceForElement=_exports.getAllInstances=_exports.configureDefaultEditor=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};const instanceMap=new Map;let defaultOptions={};const importPluginList=async pluginList=>{const pluginHandlers=await Promise.all(pluginList.map((pluginPath=>-1===pluginPath.indexOf("/")?Promise.resolve(pluginPath):"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginPath],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginPath)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginPath])))),pluginNames=pluginHandlers.map((pluginConfig=>"string"==typeof pluginConfig?pluginConfig:Array.isArray(pluginConfig)?pluginConfig[0]:null)).filter((value=>value));return{pluginNames:pluginNames,pluginConfig:pluginHandlers.map((pluginConfig=>Array.isArray(pluginConfig)?pluginConfig[1]:null)).filter((value=>value))}};_exports.getAllInstances=()=>new Map(instanceMap.entries());_exports.getInstanceForElementId=elementId=>getInstanceForElement(document.getElementById(elementId));const getInstanceForElement=element=>{const instance=instanceMap.get(element);if(!instance||!instance.removed)return instance;instanceMap.remove(element)};_exports.getInstanceForElement=getInstanceForElement;_exports.setupForElementId=_ref=>{let{elementId:elementId,options:options}=_ref;const target=document.getElementById(elementId);return setupForTarget(target,options)};(async()=>{const lang=document.querySelector("html").lang,[tinyMCE,langData]=await Promise.all([(0,_loader.getTinyMCE)(),(language=lang,fetch("".concat(M.cfg.wwwroot,"/lib/editor/tiny/lang.php/").concat(M.cfg.langrev,"/").concat(language)).then((response=>response.json())))]);var language;tinyMCE.addI18n(lang,langData)})();const getPlugins=options=>options.plugins?options.plugins:defaultOptions.plugins?defaultOptions.plugins:{},getStandardConfig=(target,tinyMCE,options,plugins)=>({target:target,language:document.querySelector("html").lang,content_css:[options.css],convert_urls:!1,a11y_advanced_options:!0,toolbar_mode:"sliding",toolbar:[{name:"history",items:["undo","redo"]},{name:"styles",items:["styles"]},{name:"formatting",items:["bold","italic"]},{name:"alignment",items:["alignleft","aligncenter","alignright","alignjustify"]},{name:"indentation",items:["outdent","indent"]},{name:"comments",items:["addcomment"]}],menu:{},plugins:[...plugins],skin:"oxide",promotion:!1}),setupForTarget=async function(target){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const instance=getInstanceForElement(target);if(instance)return Promise.resolve(instance);const pendingPromise=new _pending.default("editor_tiny/editor:setupForTarget"),plugins=getPlugins(options),[tinyMCE,pluginValues]=await Promise.all([(0,_loader.getTinyMCE)(),importPluginList(Object.keys(plugins))]),{pluginNames:pluginNames,pluginConfig:pluginConfig}=pluginValues,instanceConfig=getStandardConfig(target,0,options,pluginNames);pluginConfig.forEach((pluginConfig=>{"function"==typeof pluginConfig.configure&&Object.assign(instanceConfig,pluginConfig.configure(instanceConfig))}));const[editor]=await tinyMCE.init(instanceConfig);return instanceMap.set(target,editor),editor.on("remove",(_ref2=>{let{target:target}=_ref2;instanceMap.delete(target.targetElm)})),editor.moodleOptions=options,pendingPromise.resolve(),editor};_exports.setupForTarget=setupForTarget;_exports.configureDefaultEditor=function(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};defaultOptions=options}}));
|
||||
define("editor_tiny/editor",["exports","./loader","core/pending"],(function(_exports,_loader,_pending){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupForTarget=_exports.setupForElementId=_exports.getInstanceForElementId=_exports.getInstanceForElement=_exports.getAllInstances=_exports.configureDefaultEditor=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};const instanceMap=new Map;let defaultOptions={};const importPluginList=async pluginList=>{const pluginHandlers=await Promise.all(pluginList.map((pluginPath=>-1===pluginPath.indexOf("/")?Promise.resolve(pluginPath):"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginPath],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginPath)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginPath])))),pluginNames=pluginHandlers.map((pluginConfig=>"string"==typeof pluginConfig?pluginConfig:Array.isArray(pluginConfig)?pluginConfig[0]:null)).filter((value=>value));return{pluginNames:pluginNames,pluginConfig:pluginHandlers.map((pluginConfig=>Array.isArray(pluginConfig)?pluginConfig[1]:null)).filter((value=>value))}};_exports.getAllInstances=()=>new Map(instanceMap.entries());_exports.getInstanceForElementId=elementId=>getInstanceForElement(document.getElementById(elementId));const getInstanceForElement=element=>{const instance=instanceMap.get(element);if(!instance||!instance.removed)return instance;instanceMap.remove(element)};_exports.getInstanceForElement=getInstanceForElement;_exports.setupForElementId=_ref=>{let{elementId:elementId,options:options}=_ref;const target=document.getElementById(elementId);return setupForTarget(target,options)};(async()=>{const lang=document.querySelector("html").lang,[tinyMCE,langData]=await Promise.all([(0,_loader.getTinyMCE)(),(language=lang,fetch("".concat(M.cfg.wwwroot,"/lib/editor/tiny/lang.php/").concat(M.cfg.langrev,"/").concat(language)).then((response=>response.json())))]);var language;tinyMCE.addI18n(lang,langData)})();const getPlugins=options=>options.plugins?options.plugins:defaultOptions.plugins?defaultOptions.plugins:{},getStandardConfig=(target,tinyMCE,options,plugins)=>{const lang=document.querySelector("html").lang;return{base_url:_loader.baseUrl,target:target,language:lang,content_css:[options.css],convert_urls:!1,a11y_advanced_options:!0,toolbar_mode:"sliding",toolbar:[{name:"history",items:["undo","redo"]},{name:"styles",items:["styles"]},{name:"formatting",items:["bold","italic"]},{name:"alignment",items:["alignleft","aligncenter","alignright","alignjustify"]},{name:"indentation",items:["outdent","indent"]},{name:"comments",items:["addcomment"]}],menu:{},plugins:[...plugins],skin:"oxide",promotion:!1}},setupForTarget=async function(target){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const instance=getInstanceForElement(target);if(instance)return Promise.resolve(instance);const pendingPromise=new _pending.default("editor_tiny/editor:setupForTarget"),plugins=getPlugins(options),[tinyMCE,pluginValues]=await Promise.all([(0,_loader.getTinyMCE)(),importPluginList(Object.keys(plugins))]),{pluginNames:pluginNames,pluginConfig:pluginConfig}=pluginValues,instanceConfig=getStandardConfig(target,0,options,pluginNames);pluginConfig.forEach((pluginConfig=>{"function"==typeof pluginConfig.configure&&Object.assign(instanceConfig,pluginConfig.configure(instanceConfig))}));const[editor]=await tinyMCE.init(instanceConfig);return instanceMap.set(target,editor),editor.on("remove",(_ref2=>{let{target:target}=_ref2;instanceMap.delete(target.targetElm)})),editor.moodleOptions=options,pendingPromise.resolve(),editor};_exports.setupForTarget=setupForTarget;_exports.configureDefaultEditor=function(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};defaultOptions=options}}));
|
||||
|
||||
//# sourceMappingURL=editor.min.js.map
|
File diff suppressed because one or more lines are too long
4
lib/editor/tiny/amd/build/loader.min.js
vendored
4
lib/editor/tiny/amd/build/loader.min.js
vendored
@ -1,4 +1,4 @@
|
||||
define("editor_tiny/loader",["exports"],(function(_exports){
|
||||
define("editor_tiny/loader",["exports","core/config"],(function(_exports,Config){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}
|
||||
/**
|
||||
* Tiny Loader for Moodle
|
||||
*
|
||||
@ -6,6 +6,6 @@ define("editor_tiny/loader",["exports"],(function(_exports){
|
||||
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
let tinyMCEPromise;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getTinyMCE=void 0;_exports.getTinyMCE=()=>tinyMCEPromise||(tinyMCEPromise=new Promise(((resolve,reject)=>{const head=document.querySelector("head");let script=head.querySelector('script[data-tinymce="tinymce"]');script&&resolve(window.tinyMCE),script=document.createElement("script"),script.dataset.tinymce="tinymce",script.src="".concat(M.cfg.wwwroot,"/lib/editor/tiny/js/tinymce/tinymce.js"),script.async=!0,script.addEventListener("load",(()=>{resolve(window.tinyMCE)}),!1),script.addEventListener("error",(err=>{reject(err)}),!1),head.append(script)})),tinyMCEPromise)}));
|
||||
let tinyMCEPromise;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getTinyMCE=_exports.baseUrl=void 0,Config=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Config);const baseUrl="".concat(Config.wwwroot,"/lib/editor/tiny/loader.php/").concat(M.cfg.jsrev);_exports.baseUrl=baseUrl;_exports.getTinyMCE=()=>tinyMCEPromise||(tinyMCEPromise=new Promise(((resolve,reject)=>{const head=document.querySelector("head");let script=head.querySelector('script[data-tinymce="tinymce"]');script&&resolve(window.tinyMCE),script=document.createElement("script"),script.dataset.tinymce="tinymce",script.src="".concat(baseUrl,"/tinymce.js"),script.async=!0,script.addEventListener("load",(()=>{resolve(window.tinyMCE)}),!1),script.addEventListener("error",(err=>{reject(err)}),!1),head.append(script)})),tinyMCEPromise)}));
|
||||
|
||||
//# sourceMappingURL=loader.min.js.map
|
@ -1 +1 @@
|
||||
{"version":3,"file":"loader.min.js","sources":["../src/loader.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 Loader for Moodle\n *\n * @module editor_tiny/loader\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nlet tinyMCEPromise;\n\n/**\n * Get the TinyMCE API Object.\n *\n * @returns {Promise<TinyMCE>} The TinyMCE API Object\n */\nexport const getTinyMCE = () => {\n if (tinyMCEPromise) {\n return tinyMCEPromise;\n }\n\n tinyMCEPromise = new Promise((resolve, reject) => {\n const head = document.querySelector('head');\n let script = head.querySelector('script[data-tinymce=\"tinymce\"]');\n if (script) {\n resolve(window.tinyMCE);\n }\n\n script = document.createElement('script');\n script.dataset.tinymce = 'tinymce';\n script.src = `${M.cfg.wwwroot}/lib/editor/tiny/js/tinymce/tinymce.js`;\n script.async = true;\n\n script.addEventListener('load', () => {\n resolve(window.tinyMCE);\n }, false);\n\n script.addEventListener('error', (err) => {\n reject(err);\n }, false);\n\n head.append(script);\n });\n\n return tinyMCEPromise;\n};\n"],"names":["tinyMCEPromise","Promise","resolve","reject","head","document","querySelector","script","window","tinyMCE","createElement","dataset","tinymce","src","M","cfg","wwwroot","async","addEventListener","err","append"],"mappings":";;;;;;;;IAuBIA,sHAOsB,IAClBA,iBAIJA,eAAiB,IAAIC,SAAQ,CAACC,QAASC,gBAC7BC,KAAOC,SAASC,cAAc,YAChCC,OAASH,KAAKE,cAAc,kCAC5BC,QACAL,QAAQM,OAAOC,SAGnBF,OAASF,SAASK,cAAc,UAChCH,OAAOI,QAAQC,QAAU,UACzBL,OAAOM,cAASC,EAAEC,IAAIC,kDACtBT,OAAOU,OAAQ,EAEfV,OAAOW,iBAAiB,QAAQ,KAC5BhB,QAAQM,OAAOC,YAChB,GAEHF,OAAOW,iBAAiB,SAAUC,MAC9BhB,OAAOgB,QACR,GAEHf,KAAKgB,OAAOb,WAGTP"}
|
||||
{"version":3,"file":"loader.min.js","sources":["../src/loader.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 Loader for Moodle\n *\n * @module editor_tiny/loader\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nlet tinyMCEPromise;\n\nimport * as Config from 'core/config';\n\nexport const baseUrl = `${Config.wwwroot}/lib/editor/tiny/loader.php/${M.cfg.jsrev}`;\n\n/**\n * Get the TinyMCE API Object.\n *\n * @returns {Promise<TinyMCE>} The TinyMCE API Object\n */\nexport const getTinyMCE = () => {\n if (tinyMCEPromise) {\n return tinyMCEPromise;\n }\n\n tinyMCEPromise = new Promise((resolve, reject) => {\n const head = document.querySelector('head');\n let script = head.querySelector('script[data-tinymce=\"tinymce\"]');\n if (script) {\n resolve(window.tinyMCE);\n }\n\n script = document.createElement('script');\n script.dataset.tinymce = 'tinymce';\n script.src = `${baseUrl}/tinymce.js`;\n script.async = true;\n\n script.addEventListener('load', () => {\n resolve(window.tinyMCE);\n }, false);\n\n script.addEventListener('error', (err) => {\n reject(err);\n }, false);\n\n head.append(script);\n });\n\n return tinyMCEPromise;\n};\n"],"names":["tinyMCEPromise","baseUrl","Config","wwwroot","M","cfg","jsrev","Promise","resolve","reject","head","document","querySelector","script","window","tinyMCE","createElement","dataset","tinymce","src","async","addEventListener","err","append"],"mappings":";;;;;;;;IAuBIA,qxBAISC,kBAAaC,OAAOC,+CAAsCC,EAAEC,IAAIC,oDAOnD,IAClBN,iBAIJA,eAAiB,IAAIO,SAAQ,CAACC,QAASC,gBAC7BC,KAAOC,SAASC,cAAc,YAChCC,OAASH,KAAKE,cAAc,kCAC5BC,QACAL,QAAQM,OAAOC,SAGnBF,OAASF,SAASK,cAAc,UAChCH,OAAOI,QAAQC,QAAU,UACzBL,OAAOM,cAASlB,uBAChBY,OAAOO,OAAQ,EAEfP,OAAOQ,iBAAiB,QAAQ,KAC5Bb,QAAQM,OAAOC,YAChB,GAEHF,OAAOQ,iBAAiB,SAAUC,MAC9Bb,OAAOa,QACR,GAEHZ,KAAKa,OAAOV,WAGTb"}
|
@ -22,6 +22,7 @@
|
||||
*/
|
||||
import {
|
||||
getTinyMCE,
|
||||
baseUrl,
|
||||
} from './loader';
|
||||
import Pending from 'core/pending';
|
||||
|
||||
@ -141,6 +142,8 @@ const getPlugins = (options) => {
|
||||
const getStandardConfig = (target, tinyMCE, options, plugins) => {
|
||||
const lang = document.querySelector('html').lang;
|
||||
return {
|
||||
base_url: baseUrl,
|
||||
|
||||
// Set the editor target.
|
||||
// https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#target
|
||||
target,
|
||||
|
@ -23,6 +23,10 @@
|
||||
|
||||
let tinyMCEPromise;
|
||||
|
||||
import * as Config from 'core/config';
|
||||
|
||||
export const baseUrl = `${Config.wwwroot}/lib/editor/tiny/loader.php/${M.cfg.jsrev}`;
|
||||
|
||||
/**
|
||||
* Get the TinyMCE API Object.
|
||||
*
|
||||
@ -42,7 +46,7 @@ export const getTinyMCE = () => {
|
||||
|
||||
script = document.createElement('script');
|
||||
script.dataset.tinymce = 'tinymce';
|
||||
script.src = `${M.cfg.wwwroot}/lib/editor/tiny/js/tinymce/tinymce.js`;
|
||||
script.src = `${baseUrl}/tinymce.js`;
|
||||
script.async = true;
|
||||
|
||||
script.addEventListener('load', () => {
|
||||
|
331
lib/editor/tiny/loader.php
Normal file
331
lib/editor/tiny/loader.php
Normal file
@ -0,0 +1,331 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Tiny text editor integration - TinyMCE Loader.
|
||||
*
|
||||
* @package editor_tiny
|
||||
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace editor_tiny;
|
||||
|
||||
// Disable moodle specific debug messages and any errors in output.
|
||||
define('NO_DEBUG_DISPLAY', true);
|
||||
|
||||
// We need just the values from config.php and minlib.php.
|
||||
define('ABORT_AFTER_CONFIG', true);
|
||||
|
||||
// This stops immediately at the beginning of lib/setup.php.
|
||||
require('../../../config.php');
|
||||
|
||||
/**
|
||||
* An anonymous class to handle loading and serving TinyMCE JavaScript.
|
||||
*
|
||||
* @copyright 2021 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class loader {
|
||||
/** @var string The filepath requested */
|
||||
protected $filepath;
|
||||
|
||||
/** @var int The revision requested */
|
||||
protected $rev;
|
||||
|
||||
/** @var string The mimetype to send */
|
||||
protected $mimetype = null;
|
||||
|
||||
/**
|
||||
* Initialise the class, parse the request and serve the content.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->parse_file_information_from_url();
|
||||
$this->serve_file();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the file information from the URL.
|
||||
*/
|
||||
protected function parse_file_information_from_url(): void {
|
||||
global $CFG;
|
||||
|
||||
// The URL format is /[revision]/[filepath].
|
||||
// The revision is an integer with negative values meaning the file is not cached.
|
||||
// The filepath is a child of the TinyMCE js/tinymce directory containing all upstream code.
|
||||
// The filepath is cleaned using the SAFEPATH option, which does not allow directory traversal.
|
||||
if ($slashargument = min_get_slash_argument()) {
|
||||
$slashargument = ltrim($slashargument, '/');
|
||||
if (substr_count($slashargument, '/') < 1) {
|
||||
$this->send_not_found();
|
||||
}
|
||||
|
||||
[$rev, $filepath] = explode('/', $slashargument, 2);
|
||||
$this->rev = min_clean_param($rev, 'RAW');
|
||||
$this->filepath = min_clean_param($filepath, 'SAFEPATH');
|
||||
} else {
|
||||
$this->rev = min_optional_param('rev', 0, 'RAW');
|
||||
$this->filepath = min_optional_param('filepath', 'standard', 'SAFEPATH');
|
||||
}
|
||||
|
||||
$extension = pathinfo($this->filepath, PATHINFO_EXTENSION);
|
||||
if ($extension === 'css') {
|
||||
$this->mimetype = 'text/css';
|
||||
} else if ($extension === 'js') {
|
||||
$this->mimetype = 'application/javascript';
|
||||
} else if ($extension === 'map') {
|
||||
$this->mimetype = 'application/json';
|
||||
} else {
|
||||
$this->send_not_found();
|
||||
}
|
||||
|
||||
$filepathhash = sha1("{$this->filepath}");
|
||||
if (preg_match('/^plugins\/tiny_/', $this->filepath)) {
|
||||
$parts = explode('/', $this->filepath);
|
||||
array_shift($parts);
|
||||
$component = array_shift($parts);
|
||||
$this->component = preg_replace('/^tiny_/', '', $component);
|
||||
$this->filepath = implode('/', $parts);
|
||||
}
|
||||
$this->candidatefile = "{$CFG->localcachedir}/editor_tiny/{$this->rev}/{$filepathhash}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the requested file from the most appropriate location, caching if possible.
|
||||
*/
|
||||
public function serve_file(): void {
|
||||
// Attempt to send the cached filepathpack.
|
||||
if ($this->rev > 0) {
|
||||
if ($this->is_candidate_file_available()) {
|
||||
// The send_cached_file_if_available function will exit if successful.
|
||||
// In theory the file could become unavailable after checking that the file exists.
|
||||
// Whilst this is unlikely, fall back to caching the content below.
|
||||
$this->send_cached_file_if_available();
|
||||
}
|
||||
|
||||
// The file isn't cached yet.
|
||||
// Store it in the cache and serve it.
|
||||
$this->store_filepath_file();
|
||||
$this->send_cached();
|
||||
} else {
|
||||
// If the revision is less than 0, then do not cache anything.
|
||||
// Moodle is configured to not cache javascript or css.
|
||||
$this->send_uncached_from_dirroot();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full filepath to the requested file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_filepath_from_dirroot(): ?string {
|
||||
global $CFG;
|
||||
|
||||
$rootdir = "{$CFG->dirroot}/lib/editor/tiny";
|
||||
if ($this->component) {
|
||||
$rootdir .= "/plugins/{$this->component}/js";
|
||||
} else {
|
||||
$rootdir .= "/js/tinymce";
|
||||
}
|
||||
|
||||
$filepath = "{$rootdir}/{$this->filepath}";
|
||||
if (file_exists($filepath)) {
|
||||
return $filepath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the file content from the dirroot.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function load_content_from_dirroot(): ?string {
|
||||
if ($filepath = $this->get_filepath_from_dirroot()) {
|
||||
return file_get_contents($filepath);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the file content from the dirroot.
|
||||
*
|
||||
* If the file is not found, send the 404 response instead.
|
||||
*/
|
||||
protected function send_uncached_from_dirroot(): void {
|
||||
if ($filepath = $this->get_filepath_from_dirroot()) {
|
||||
$this->send_uncached_file($filepath);
|
||||
}
|
||||
|
||||
$this->send_not_found();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the candidate file exists.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_candidate_file_available(): bool {
|
||||
return file_exists($this->candidatefile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the candidate file.
|
||||
*/
|
||||
protected function send_cached_file_if_available(): void {
|
||||
global $_SERVER;
|
||||
|
||||
if (file_exists($this->candidatefile)) {
|
||||
// The candidate file exists so will be sent regardless.
|
||||
|
||||
if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
|
||||
// The browser sent headers to check if the file has changed.
|
||||
// We do not actually need to verify the eTag value or compare modification headers because our files
|
||||
// never change in cache. When changes are made we increment the revision counter.
|
||||
$this->send_unmodified_headers(filemtime($this->candidatefile));
|
||||
}
|
||||
|
||||
// No modification headers were sent so simply serve the file from cache.
|
||||
$this->send_cached($this->candidatefile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the file content in the candidate file.
|
||||
*/
|
||||
protected function store_filepath_file(): void {
|
||||
global $CFG;
|
||||
|
||||
clearstatcache();
|
||||
if (!file_exists(dirname($this->candidatefile))) {
|
||||
@mkdir(dirname($this->candidatefile), $CFG->directorypermissions, true);
|
||||
}
|
||||
|
||||
// Prevent serving of incomplete file from concurrent request,
|
||||
// the rename() should be more atomic than fwrite().
|
||||
ignore_user_abort(true);
|
||||
|
||||
$filename = $this->candidatefile;
|
||||
if ($fp = fopen($filename . '.tmp', 'xb')) {
|
||||
$content = $this->load_content_from_dirroot();
|
||||
fwrite($fp, $content);
|
||||
fclose($fp);
|
||||
rename($filename . '.tmp', $filename);
|
||||
@chmod($filename, $CFG->filepermissions);
|
||||
@unlink($filename . '.tmp'); // Just in case anything fails.
|
||||
}
|
||||
|
||||
ignore_user_abort(false);
|
||||
if (connection_aborted()) {
|
||||
die;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the eTag for the candidate file.
|
||||
*
|
||||
* This is a unique hash based on the file arguments.
|
||||
* It does not need to consider the file content because we use a cache busting URL.
|
||||
*
|
||||
* @return string The eTag content
|
||||
*/
|
||||
protected function get_etag(): string {
|
||||
$etag = [
|
||||
$this->filepath,
|
||||
$this->rev,
|
||||
];
|
||||
|
||||
return sha1(implode('/', $etag));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the candidate file, with aggressive cachign headers.
|
||||
*
|
||||
* This includdes eTags, a last-modified, and expiry approximately 90 days in the future.
|
||||
*/
|
||||
protected function send_cached(): void {
|
||||
$path = $this->candidatefile;
|
||||
|
||||
// 90 days only - based on Moodle point release cadence being every 3 months.
|
||||
$lifetime = 60 * 60 * 24 * 90;
|
||||
|
||||
header('Etag: "' . $this->get_etag() . '"');
|
||||
header('Content-Disposition: inline; filename="filepath.php"');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($path)) . ' GMT');
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $lifetime) . ' GMT');
|
||||
header('Pragma: ');
|
||||
header('Cache-Control: public, max-age=' . $lifetime . ', immutable');
|
||||
header('Accept-Ranges: none');
|
||||
header("Content-Type: {$this->mimetype}; charset=utf-8");
|
||||
if (!min_enable_zlib_compression()) {
|
||||
header('Content-Length: ' . filesize($path));
|
||||
}
|
||||
|
||||
readfile($path);
|
||||
die;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the content directly without caching it.
|
||||
*
|
||||
* No aggressive caching is used, and the expiry is set to the current time.
|
||||
*
|
||||
* @param string $filepath
|
||||
*/
|
||||
protected function send_uncached_file(string $filepath): void {
|
||||
header('Content-Disposition: inline; filename="styles_debug.php"');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
|
||||
header('Pragma: ');
|
||||
header('Accept-Ranges: none');
|
||||
header("Content-Type: {$this->mimetype}; charset=utf-8");
|
||||
|
||||
readfile($filepath);
|
||||
die;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send headers to indicate that the file has not been modified at all
|
||||
*
|
||||
* @param int $lastmodified
|
||||
*/
|
||||
protected function send_unmodified_headers(int $lastmodified): void {
|
||||
// 90 days only - based on Moodle point release cadence being every 3 months.
|
||||
$lifetime = 60 * 60 * 24 * 90;
|
||||
header('HTTP/1.1 304 Not Modified');
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $lifetime) . ' GMT');
|
||||
header('Cache-Control: public, max-age=' . $lifetime);
|
||||
header("Content-Type: {$this->mimetype}; charset=utf-8");
|
||||
header('Etag: "' . $this->get_etag() . '"');
|
||||
if ($lastmodified) {
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastmodified) . ' GMT');
|
||||
}
|
||||
die;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a 404 message to indicate that the content was not found.
|
||||
*/
|
||||
protected function send_not_found(): void {
|
||||
header('HTTP/1.0 404 not found');
|
||||
die('TinyMCE file was not found, sorry.');
|
||||
}
|
||||
}
|
||||
|
||||
new loader();
|
Loading…
x
Reference in New Issue
Block a user