From 5ec6aa532c50a9d43afd2cd2b8d724e5a2389b0d Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 6 Jan 2012 12:01:33 +0100 Subject: [PATCH] ENHANCEMENT GZipped/combined file loading for TinyMCE, which reduces number of HTTP requests in the standard CMS by a third (and decreases file size due to loading pre-compressed files by default) --- forms/HtmlEditorField.php | 28 ++- thirdparty/tinymce/.htaccess | 3 + thirdparty/tinymce/tiny_mce_gzip.js | 137 +++++++++++ thirdparty/tinymce/tiny_mce_gzip.php | 340 +++++++++++++++++++++++++++ 4 files changed, 506 insertions(+), 2 deletions(-) create mode 100644 thirdparty/tinymce/.htaccess create mode 100644 thirdparty/tinymce/tiny_mce_gzip.js create mode 100755 thirdparty/tinymce/tiny_mce_gzip.php diff --git a/forms/HtmlEditorField.php b/forms/HtmlEditorField.php index da4d67c72..f64aa77ef 100644 --- a/forms/HtmlEditorField.php +++ b/forms/HtmlEditorField.php @@ -7,13 +7,37 @@ * @subpackage fields-formattedinput */ class HtmlEditorField extends TextareaField { + + /** + * @var Boolean Use TinyMCE's GZIP compressor + */ + static $use_gzip = true; /** * Includes the JavaScript neccesary for this field to work using the {@link Requirements} system. */ public static function include_js() { - Requirements::javascript(MCE_ROOT . 'tiny_mce_src.js'); - Requirements::customScript(HtmlEditorConfig::get_active()->generateJS(), 'htmlEditorConfig'); + require_once 'tinymce/tiny_mce_gzip.php'; + + $configObj = HtmlEditorConfig::get_active(); + + if(self::$use_gzip) { + $internalPlugins = array(); + foreach($configObj->getPlugins() as $plugin => $path) if(!$path) $internalPlugins[] = $plugin; + $tag = TinyMCE_Compressor::renderTag(array( + 'url' => THIRDPARTY_DIR . '/tinymce/tiny_mce_gzip.php', + 'plugins' => implode(',', $internalPlugins), + 'themes' => 'advanced', + 'languages' => $configObj->getOption('language') + ), true); + preg_match('/src="([^"]*)"/', $tag, $matches); + Requirements::javascript($matches[1]); + + } else { + Requirements::javascript(MCE_ROOT . 'tiny_mce_src.js'); + } + + Requirements::customScript($configObj->generateJS(), 'htmlEditorConfig'); } /** diff --git a/thirdparty/tinymce/.htaccess b/thirdparty/tinymce/.htaccess new file mode 100644 index 000000000..1455c5c32 --- /dev/null +++ b/thirdparty/tinymce/.htaccess @@ -0,0 +1,3 @@ + + Allow from all + \ No newline at end of file diff --git a/thirdparty/tinymce/tiny_mce_gzip.js b/thirdparty/tinymce/tiny_mce_gzip.js new file mode 100644 index 000000000..78ae50dbf --- /dev/null +++ b/thirdparty/tinymce/tiny_mce_gzip.js @@ -0,0 +1,137 @@ +var tinyMCE_GZ = { + settings : { + themes : '', + plugins : '', + languages : '', + disk_cache : true, + page_name : 'tiny_mce_gzip.php', + debug : false, + suffix : '' + }, + + init : function(s, cb, sc) { + var t = this, n, i, nl = document.getElementsByTagName('script'); + + for (n in s) + t.settings[n] = s[n]; + + s = t.settings; + + if (window.tinyMCEPreInit) { + t.baseURL = tinyMCEPreInit.base; + } else { + for (i=0; i 10000) { + w.clearInterval(ti); + + if (c < 10000 && x.status == 200) { + t.loaded = 1; + t.eval(x.responseText); + tinymce.dom.Event.domLoaded = true; + cb.call(sc || t, x); + } + + ti = x = null; + } + }, 10); + } else + t.eval(x.responseText); + }, + + start : function() { + var t = this, each = tinymce.each, s = t.settings, ln = s.languages.split(','); + + tinymce.suffix = s.suffix; + + function load(u) { + tinymce.ScriptLoader.markDone(tinyMCE.baseURI.toAbsolute(u)); + }; + + // Add core languages + each(ln, function(c) { + if (c) + load('langs/' + c + '.js'); + }); + + // Add themes with languages + each(s.themes.split(','), function(n) { + if (n) { + load('themes/' + n + '/editor_template' + s.suffix + '.js'); + + each (ln, function(c) { + if (c) + load('themes/' + n + '/langs/' + c + '.js'); + }); + } + }); + + // Add plugins with languages + each(s.plugins.split(','), function(n) { + if (n) { + load('plugins/' + n + '/editor_plugin' + s.suffix + '.js'); + + each(ln, function(c) { + if (c) + load('plugins/' + n + '/langs/' + c + '.js'); + }); + } + }); + }, + + end : function() { + }, + + eval : function(co) { + var se = document.createElement('script'); + + // Create script + se.type = 'text/javascript'; + se.text = co; + + // Add it to evaluate it and remove it + (document.getElementsByTagName('head')[0] || document.documentElement).appendChild(se); + se.parentNode.removeChild(se); + } +}; diff --git a/thirdparty/tinymce/tiny_mce_gzip.php b/thirdparty/tinymce/tiny_mce_gzip.php new file mode 100755 index 000000000..88b810084 --- /dev/null +++ b/thirdparty/tinymce/tiny_mce_gzip.php @@ -0,0 +1,340 @@ + getSysTempDir() + // CUSTOM END + )); + + // Handle request, compress and stream to client + $tinyMCECompressor->handleRequest(); +} + +/** + * This class combines and compresses the TinyMCE core, plugins, themes and + * language packs into one disk cached gzipped request. It improves the loading speed of TinyMCE dramatically but + * still provides dynamic initialization. + * + * Example of direct usage: + * require_once("../jscripts/tiny_mce/tiny_mce_gzip.php"); + * + * // Renders script tag with compressed scripts + * TinyMCE_Compressor::renderTag(array( + * "url" => "../jscripts/tiny_mce/tiny_mce_gzip.php", + * "plugins" => "pagebreak,style", + * "themes" => "advanced", + * "languages" => "en" + * )); + */ +class TinyMCE_Compressor { + private $files, $settings; + private static $defaultSettings = array( + "plugins" => "", + "themes" => "", + "languages" => "", + "disk_cache" => true, + "expires" => "30d", + "cache_dir" => "", + "compress" => true, + "suffix" => "", + "files" => "", + "source" => false, + ); + + /** + * Constructs a new compressor instance. + * + * @param Array $settings Name/value array with non-default settings for the compressor instance. + */ + public function __construct($settings = array()) { + $this->settings = array_merge(self::$defaultSettings, $settings); + + if (empty($this->settings["cache_dir"])) + $this->settings["cache_dir"] = dirname(__FILE__); + } + + /** + * Adds a file to the concatenation/compression process. + * + * @param String $path Path to the file to include in the compressed package/output. + */ + public function &addFile($file) { + $this->files .= ($this->files ? "," : "") . $file; + + return $this; + } + + /** + * Handles the incoming HTTP request and sends back a compressed script depending on settings and client support. + */ + public function handleRequest() { + $files = array(); + $supportsGzip = false; + $expiresOffset = $this->parseTime($this->settings["expires"]); + $tinymceDir = dirname(__FILE__); + + // Override settings with querystring params + $plugins = self::getParam("plugins"); + if ($plugins) + $this->settings["plugins"] = $plugins; + $plugins = explode(',', $this->settings["plugins"]); + + $themes = self::getParam("themes"); + if ($themes) + $this->settings["themes"] = $themes; + $themes = explode(',', $this->settings["themes"]); + + $languages = self::getParam("languages"); + if ($languages) + $this->settings["languages"] = $languages; + $languages = explode(',', $this->settings["languages"]); + + $tagFiles = self::getParam("files"); + if ($tagFiles) + $this->settings["files"] = $tagFiles; + + $diskCache = self::getParam("diskcache"); + if ($diskCache) + $this->settings["disk_cache"] = ($diskCache === "true"); + + $src = self::getParam("src"); + if ($src) + $this->settings["source"] = ($src === "true"); + + // Add core + $files[] = "tiny_mce"; + foreach ($languages as $language) + $files[] = "langs/$language"; + + // Add plugins + foreach ($plugins as $plugin) { + $files[] = "plugins/$plugin/editor_plugin"; + + foreach ($languages as $language) + $files[] = "plugins/$plugin/langs/$language"; + } + + // Add themes + foreach ($themes as $theme) { + $files[] = "themes/$theme/editor_template"; + + foreach ($languages as $language) + $files[] = "themes/$theme/langs/$language"; + } + + // Add any specified files. + $allFiles = array_merge($files, explode(',', $this->settings['files'])); + + // Process source files + for ($i = 0; $i < count($allFiles); $i++) { + $file = $allFiles[$i]; + + if ($this->settings["source"] && file_exists($file . "_src.js")) { + $file .= "_src.js"; + } else if (file_exists($file . ".js")) { + $file .= ".js"; + } else { + $file = ""; + } + + $allFiles[$i] = $file; + } + + // Generate hash for all files + $hash = md5(implode('', $allFiles)); + + // Check if it supports gzip + $zlibOn = ini_get('zlib.output_compression') || (ini_set('zlib.output_compression', 0) === false); + $encodings = (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) ? strtolower($_SERVER['HTTP_ACCEPT_ENCODING']) : ""; + $encoding = preg_match( '/\b(x-gzip|gzip)\b/', $encodings, $match) ? $match[1] : ""; + + // Is northon antivirus header + if (isset($_SERVER['---------------'])) + $encoding = "x-gzip"; + + $supportsGzip = $this->settings['compress'] && !empty($encoding) && !$zlibOn && function_exists('gzencode'); + + // Set cache file name + $cacheFile = $this->settings["cache_dir"] . "/" . $hash . ($supportsGzip ? ".gz" : ".js"); + + // Set headers + header("Content-type: text/javascript"); + header("Vary: Accept-Encoding"); // Handle proxies + header("Expires: " . gmdate("D, d M Y H:i:s", time() + $expiresOffset) . " GMT"); + header("Cache-Control: public, max-age=" . $expiresOffset); + + if ($supportsGzip) + header("Content-Encoding: " . $encoding); + + // Use cached file + if ($this->settings['disk_cache'] && file_exists($cacheFile)) { + readfile($cacheFile); + return; + } + + // Set base URL for where tinymce is loaded from + $buffer = "var tinyMCEPreInit={base:'" . dirname($_SERVER["SCRIPT_NAME"]) . "',suffix:''};"; + + // Load all tinymce script files into buffer + foreach ($allFiles as $file) { + if ($file) { + $fileContents = $this->getFileContents($tinymceDir . "/" . $file); +// $buffer .= "\n//-FILE-$tinymceDir/$file (". strlen($fileContents) . " bytes)\n"; + $buffer .= $fileContents; + } + } + + // Mark all themes, plugins and languages as done + $buffer .= 'tinymce.each("' . implode(',', $files) . '".split(","),function(f){tinymce.ScriptLoader.markDone(tinyMCE.baseURL+"/"+f+".js");});'; + + // Compress data + if ($supportsGzip) + $buffer = gzencode($buffer, 9, FORCE_GZIP); + + // Write cached file + if ($this->settings["disk_cache"]) + @file_put_contents($cacheFile, $buffer); + + // Stream contents to client + echo $buffer; + } + + /** + * Renders a script tag that loads the TinyMCE script. + * + * @param Array $settings Name/value array with settings for the script tag. + * @param Bool $return The script tag is return instead of being output if true + * @return String the tag is returned if $return is true + */ + public static function renderTag($tagSettings, $return = false) { + $settings = array_merge(self::$defaultSettings, $tagSettings); + + if (empty($settings["cache_dir"])) + $settings["cache_dir"] = dirname(__FILE__); + + $scriptSrc = $settings["url"] . "?js=1"; + + // Add plugins + if (isset($settings["plugins"])) + $scriptSrc .= "&plugins=" . (is_array($settings["plugins"]) ? implode(',', $settings["plugins"]) : $settings["plugins"]); + + // Add themes + if (isset($settings["themes"])) + $scriptSrc .= "&themes=" . (is_array($settings["themes"]) ? implode(',', $settings["themes"]) : $settings["themes"]); + + // Add languages + if (isset($settings["languages"])) + $scriptSrc .= "&languages=" . (is_array($settings["languages"]) ? implode(',', $settings["languages"]) : $settings["languages"]); + + // Add disk_cache + if (isset($settings["disk_cache"])) + $scriptSrc .= "&diskcache=" . ($settings["disk_cache"] === true ? "true" : "false"); + + // Add any explicitly specified files if the default settings have been overriden by the tag ones + /* + * Specifying tag files will override (rather than merge with) any site-specific ones set in the + * TinyMCE_Compressor object creation. Note that since the parameter parser limits content to alphanumeric + * only base filenames can be specified. The file extension is assumed to be ".js" and the directory is + * the TinyMCE root directory. A typical use of this is to include a script which initiates the TinyMCE object. + */ + if (isset($tagSettings["files"])) + $scriptSrc .= "&files=" .(is_array($settings["files"]) ? implode(',', $settings["files"]) : $settings["files"]); + + // Add src flag + if (isset($settings["source"])) + $scriptSrc .= "&src=" . ($settings["source"] === true ? "true" : "false"); + + $scriptTag = ''; + + if ($return) { + return $scriptTag; + } else { + echo $scriptTag; + } + } + + /** + * Returns a sanitized query string parameter. + * + * @param String $name Name of the query string param to get. + * @param String $default Default value if the query string item shouldn't exist. + * @return String Sanitized query string parameter value. + */ + public static function getParam($name, $default = "") { + if (!isset($_GET[$name])) + return $default; + + return preg_replace("/[^0-9a-z\-_,]+/i", "", $_GET[$name]); // Sanatize for security, remove anything but 0-9,a-z,-_, + } + + /** + * Parses the specified time format into seconds. Supports formats like 10h, 10d, 10m. + * + * @param String $time Time format to convert into seconds. + * @return Int Number of seconds for the specified format. + */ + private function parseTime($time) { + $multipel = 1; + + // Hours + if (strpos($time, "h") > 0) + $multipel = 3600; + + // Days + if (strpos($time, "d") > 0) + $multipel = 86400; + + // Months + if (strpos($time, "m") > 0) + $multipel = 2592000; + + // Trim string + return intval($time) * $multipel; + } + + /** + * Returns the contents of the script file if it exists and removes the UTF-8 BOM header if it exists. + * + * @param String $file File to load. + * @return String File contents or empty string if it doesn't exist. + */ + private function getFileContents($file) { + $content = file_get_contents($file); + + // Remove UTF-8 BOM + if (substr($content, 0, 3) === pack("CCC", 0xef, 0xbb, 0xbf)) + $content = substr($content, 3); + + return $content; + } +} +?> \ No newline at end of file