From fae911708e696bf468ff0447bd47523004a87ccc Mon Sep 17 00:00:00 2001 From: sam marshall Date: Wed, 16 May 2012 17:21:08 +0100 Subject: [PATCH] MDL-33041 (3) TinyMCE: Support custom plugins --- lib/editor/tinymce/classes/plugin.php | 323 ++++++++++++++++++++++++++ lib/editor/tinymce/db/subplugins.php | 27 +++ lib/editor/tinymce/extra/strings.php | 17 ++ lib/editor/tinymce/lib.php | 17 ++ lib/editor/tinymce/module.js | 10 + lib/editor/tinymce/plugins/loader.php | 96 ++++++++ 6 files changed, 490 insertions(+) create mode 100644 lib/editor/tinymce/classes/plugin.php create mode 100644 lib/editor/tinymce/db/subplugins.php create mode 100644 lib/editor/tinymce/plugins/loader.php diff --git a/lib/editor/tinymce/classes/plugin.php b/lib/editor/tinymce/classes/plugin.php new file mode 100644 index 00000000000..9179031e46d --- /dev/null +++ b/lib/editor/tinymce/classes/plugin.php @@ -0,0 +1,323 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +/** + * TinyMCE text editor plugin base class. + * + * This is a base class for TinyMCE plugins implemented within Moodle. These + * plugins can optionally provide new buttons/plugins within TinyMCE itself, + * or configure the TinyMCE options. + * + * As well as overridable functions, other utility functions in this class + * can be used when writing the plugins. + * + * Finally, a static function in this class is used to call into all the + * plugins when required. + * + * @package editor_tinymce + * @copyright 2012 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class editor_tinymce_plugin { + /** @var string Plugin folder */ + protected $plugin; + + /** + * @param string $plugin Name of folder + */ + public function __construct($plugin) { + $this->plugin = $plugin; + } + + /** + * Adjusts TinyMCE init parameters for this plugin. + * + * Subclasses must implement this function in order to carry out changes + * to the TinyMCE settings. + * + * @param array $params TinyMCE init parameters array + * @param context $context Context where editor is being shown + * @param array $options Options for this editor + */ + protected abstract function update_init_params(array &$params, context $context, + array $options = null); + + /** + * Gets the order in which to run this plugin. Order usually only matters if + * (a) the place you add your button might depend on another plugin, or + * (b) you want to make some changes to layout etc. that should happen last. + * The default order is 100; within that, plugins are sorted alphabetically. + * Return a lower number if you want this plugin to run earlier, or a higher + * number if you want it to run later. + */ + protected function get_sort_order() { + return 100; + } + + /** + * Adds a button to the editor, after another button (or at the end). + * + * Specify the location of this button using the $after variable. If you + * leave this blank, the button will be added at the end. + * + * If you want to try different possible locations depending on existing + * plugins you can set $alwaysadd to false and check the return value + * to see if it succeeded. + * + * @param array $params TinyMCE init parameters array + * @param int $row Row to add button to (1 to 3) + * @param string $button Identifier of button/plugin + * @param string $after Adds button directly after the named plugin + * @param bool $alwaysadd If specified $after string not found, add at end + * @return bool True if added + */ + protected function add_button_after(array &$params, $row, $button, + $after = '', $alwaysadd = true) { + $this->check_row($row); + + $field = 'theme_advanced_buttons' . $row; + $old = $params[$field]; + + // Empty = add at end. + if ($after === '') { + $params[$field] = $old . ',' . $button; + return true; + } + + // Try to add after given plugin. + $params[$field] = preg_replace('~(,|^)(' . preg_quote($after) . ')(,|$)~', + '$1$2,' . $button . '$3', $old); + if ($params[$field] !== $old) { + return true; + } + + // If always adding, recurse to add it empty. + if ($alwaysadd) { + return $this->add_button_after($params, $row, $button); + } + + // Otherwise return false (failed to add). + return false; + } + + /** + * Adds a button to the editor. + * + * Specify the location of this button using the $before variable. If you + * leave this blank, the button will be added at the start. + * + * If you want to try different possible locations depending on existing + * plugins you can set $alwaysadd to false and check the return value + * to see if it succeeded. + * + * @param array $params TinyMCE init parameters array + * @param int $row Row to add button to (1 to 3) + * @param string $button Identifier of button/plugin + * @param string $before Adds button directly before the named plugin + * @param bool $alwaysadd If specified $after string not found, add at start + * @return bool True if added + */ + protected function add_button_before(array &$params, $row, $button, + $before = '', $alwaysadd = true) { + $this->check_row($row); + + $field = 'theme_advanced_buttons' . $row; + $old = $params[$field]; + + // Empty = add at start. + if ($before === '') { + $params[$field] = $button . ',' . $old; + return true; + } + + // Try to add after given plugin. + $params[$field] = preg_replace('~(,|^)(' . preg_quote($before) . ')(,|$)~', + '$1' . $button . ',$2$3', $old); + if ($params[$field] !== $old) { + return true; + } + + // If always adding, recurse to add it empty. + if ($alwaysadd) { + return $this->add_button_before($params, $row, $button); + } + + // Otherwise return false (failed to add). + return false; + } + + /** + * Checks the row value is valid. + * + * @param int $row Row to add button to (1 to 3) + * @throws coding_exception If row value is outside the range 1-3 + */ + private function check_row($row) { + if ($row < 1 || $row > 3) { + throw new coding_exception("Invalid row option: $row"); + } + } + + /** + * Adds a JavaScript plugin into TinyMCE. Note that adding a plugin does + * not by itself add a button; you must do both. + * + * If you leave $pluginname blank (default) it uses the folder name. + * + * @param array $params TinyMCE init parameters array + * @param string $pluginname Identifier for plugin within TinyMCE + * @param string $jsfile Name of JS file (within plugin 'tinymce' directory) + */ + protected function add_js_plugin(&$params, $pluginname='', $jsfile='editor_plugin.js') { + global $CFG; + + // Set default plugin name. + if ($pluginname === '') { + $pluginname = $this->plugin; + } + + // Add plugin to list in params, so it doesn't try to load it again. + $params['plugins'] .= ',-' . $pluginname; + + // Add special param that causes Moodle TinyMCE init to load the plugin. + if (!isset($params['moodle_init_plugins'])) { + $params['moodle_init_plugins'] = ''; + } else { + $params['moodle_init_plugins'] .= ','; + } + + // Get URL of main JS file and store in params. + $jsurl = $this->get_tinymce_file_url($jsfile, false); + $params['moodle_init_plugins'] .= $pluginname . ':' . $jsurl; + } + + /** + * Returns URL to files in the TinyMCE folder within this plugin, suitable + * for client-side use such as loading JavaScript files. (This URL normally + * goes through loader.php and contains the plugin version to ensure + * correct and long-term cacheing.) + * + * @param string $file Filename or path within the folder + * @param bool $absolute Set false to get relative URL from plugins folder + */ + public function get_tinymce_file_url($file='', $absolute=true) { + global $CFG; + + // Version number comes from plugin version.php, except in developer + // mode where the special string 'dev' is used (prevents cacheing and + // serves unminified JS). + if (debugging('', DEBUG_DEVELOPER)) { + $version = '-1'; + } else { + $version = $this->get_version(); + } + + // Calculate the JS url (relative to the TinyMCE plugins folder - using + // relative URL saves a few bytes in each HTML page). + if ($CFG->slasharguments) { + // URL is usually from loader.php... + $jsurl = 'loader.php/' . $this->plugin . '/' . $version . '/' . $file; + } else { + // ...except when slash arguments are turned off it serves direct. + // In this situation there is no version details and it is up to + // the browser and server to negotiate cacheing, which will mean + // requesting the JS files frequently (reduced performance). + $jsurl = $this->plugin . '/tinymce/' . $file; + } + + if ($absolute) { + $jsurl = $CFG->wwwroot . '/lib/editor/tinymce/plugins/' . $jsurl; + } + + return $jsurl; + } + + /** + * Obtains version number from version.php for this plugin. + * + * @return string Version number + */ + protected function get_version() { + global $CFG; + + $plugin = new stdClass; + require($CFG->dirroot . '/lib/editor/tinymce/plugins/' . $this->plugin . '/version.php'); + return $plugin->version; + } + + /** + * Calls all available plugins to adjust the TinyMCE init parameters. + * + * @param array $params TinyMCE init parameters array + * @param context $context Context where editor is being shown + * @param array $options Options for this editor + */ + public static function all_update_init_params(array &$params, + context $context, array $options = null) { + global $CFG; + + // Get list of plugin directories. + $plugins = get_plugin_list('tinymce'); + + // Construct all the plugins. + $pluginobjects = array(); + foreach ($plugins as $plugin => $dir) { + require_once($dir . '/lib.php'); + $classname = 'tinymce_' . $plugin; + $pluginobjects[] = new $classname($plugin); + } + + // Sort plugins by sort order and name. + usort($pluginobjects, array('editor_tinymce_plugin', 'compare_plugins')); + + // Run the function for each plugin. + foreach ($pluginobjects as $obj) { + $obj->update_init_params($params, $context, $options); + } + } + + /** + * Gets a named plugin object. Will cause fatal error if plugin doesn't exist. + * + * @param string $plugin Name of plugin e.g. 'moodleemoticon' + * @return editor_tinymce_plugin Plugin object + */ + public static function get($plugin) { + $dir = get_component_directory('tinymce_' . $plugin); + require_once($dir . '/lib.php'); + $classname = 'tinymce_' . $plugin; + return new $classname($plugin); + } + + /** + * Compares two plugins. + * @param editor_tinymce_plugin $a + * @param editor_tinymce_plugin $b + * @return Negative number if $a is before $b + */ + public static function compare_plugins(editor_tinymce_plugin $a, editor_tinymce_plugin $b) { + // Use sort order first. + $order = $a->get_sort_order() - $b->get_sort_order(); + if ($order != 0) { + return $order; + } + + // Then sort alphabetically. + return strcmp($a->plugin, $b->plugin); + } +} diff --git a/lib/editor/tinymce/db/subplugins.php b/lib/editor/tinymce/db/subplugins.php new file mode 100644 index 00000000000..68ef1912b6d --- /dev/null +++ b/lib/editor/tinymce/db/subplugins.php @@ -0,0 +1,27 @@ +. + +/** + * TinyMCE subplugin type declaration. + * + * @package editor_tinymce + * @copyright 2012 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$subplugins = array('tinymce' => 'lib/editor/tinymce/plugins'); diff --git a/lib/editor/tinymce/extra/strings.php b/lib/editor/tinymce/extra/strings.php index 74c09f3c0b3..a01b89dbcef 100644 --- a/lib/editor/tinymce/extra/strings.php +++ b/lib/editor/tinymce/extra/strings.php @@ -51,6 +51,23 @@ foreach ($string as $key=>$value) { $result[$parts[0]][$parts[1]] = $value; } +// Add subplugin strings. These automatically are added under the plugin name +// unless they include a colon, in which case they are treated same as the +// main lang file strings. (Just in case you have a single Moodle plugin +// creating multiple tinymce plugins.) +foreach (get_plugin_list('tinymce') as $component => $ignored) { + $componentstrings = get_string_manager()->load_component_strings( + 'tinymce_' . $component, $lang); + foreach ($componentstrings as $key => $value) { + $parts = explode(':', $key); + if (count($parts) == 2) { + $result[$parts[0]][$parts[1]] = $value; + } else { + $result[$component][$key] = $value; + } + } +} + $output = 'tinyMCE.addI18n({'.$lang.':'.json_encode($result).'});'; $lifetime = '10'; // TODO: increase later diff --git a/lib/editor/tinymce/lib.php b/lib/editor/tinymce/lib.php index daad1a53b4a..69183e25c78 100644 --- a/lib/editor/tinymce/lib.php +++ b/lib/editor/tinymce/lib.php @@ -83,6 +83,7 @@ class tinymce_texteditor extends texteditor { protected function get_init_params($elementid, array $options=null) { global $CFG, $PAGE, $OUTPUT; + require_once($CFG->dirroot . '/lib/editor/tinymce/classes/plugin.php'); //TODO: we need to implement user preferences that affect the editor setup too @@ -186,6 +187,22 @@ class tinymce_texteditor extends texteditor { if (!empty($options['required'])) { $params['init_instance_callback'] = 'M.editor_tinymce.onblur_event'; } + + // Allow plugins to adjust parameters, then return. + editor_tinymce_plugin::all_update_init_params($params, $context, $options); return $params; } + + /** + * Gets a named plugin object. Will cause fatal error if plugin doesn't + * exist. This is intended for use by plugin files themselves. + * + * @param string $plugin Name of plugin e.g. 'moodleemoticon' + * @return editor_tinymce_plugin Plugin object + */ + public function get_plugin($plugin) { + global $CFG; + require_once($CFG->dirroot . '/lib/editor/tinymce/classes/plugin.php'); + return editor_tinymce_plugin::get($plugin); + } } diff --git a/lib/editor/tinymce/module.js b/lib/editor/tinymce/module.js index 2754961545d..a6718919aa6 100644 --- a/lib/editor/tinymce/module.js +++ b/lib/editor/tinymce/module.js @@ -28,6 +28,16 @@ M.editor_tinymce.filepicker_options = M.editor_tinymce.filepicker_options || {}; M.editor_tinymce.init_editor = function(Y, editorid, options) { M.editor_tinymce.editor_options[editorid] = options; + + // Load necessary Moodle plugins into editor + if (options.moodle_init_plugins) { + var extraplugins = options.moodle_init_plugins.split(','); + for (var i=0; i. + +/** + * Loader for resource files within TinyMCE plugins. + * + * This loader handles requests which have the plugin version number in. These + * requests are set to never expire from cache, to improve performance. Only + * files within the 'tinymce' folder of the plugin will be served. + * + * Note there are no access checks in this script - you do not have to be + * logged in to retrieve the plugin resource files. + * + * @package editor_tinymce + * @copyright 2012 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('NO_MOODLE_COOKIES', true); +require_once('../../../../config.php'); +require_once($CFG->dirroot . '/lib/filelib.php'); +require_once($CFG->dirroot . '/lib/jslib.php'); + +// Safely get slash params (cleaned). +$path = get_file_argument(); + +// Param must be of the form [plugin]/[version]/[path] where path is a relative +// path inside the plugin tinymce folder. +$matches = array(); +if (!preg_match('~^/([a-z0-9_]+)/((?:[0-9.]+)|-1)(/.*)$~', $path, $matches)) { + print_error('filenotfound'); +} +list($junk, $tinymceplugin, $version, $innerpath) = $matches; + +// Note that version number is totally ignored, user can specify anything, +// except for the difference between 'dev' and anything else. + +// Check the file exists. +$pluginfolder = $CFG->dirroot . '/lib/editor/tinymce/plugins/' . $tinymceplugin; +$file = $pluginfolder . '/tinymce' .$innerpath; +if (!file_exists($file)) { + print_error('filenotfound'); +} + +// We don't actually care what the version number is but there is a special +// case for '-1' which means, set the files to not be cached. +$allowcache = $version !== '-1'; +if ($allowcache) { + // Set it to expire a year later. Note that this means we should never get + // If-Modified-Since requests so there is no need to handle them specially. + header('Expires: ' . date('r', time() + 365 * 24 * 3600)); + header('Cache-Control: max-age=' . 365 * 24 * 3600); + // Pragma is set to no-cache by default so must be overridden. + header('Pragma:'); +} + +// Get the right MIME type. +$mimetype = mimeinfo('type', $file); + +// For JS files, these can be minified and stored in cache. +if ($mimetype === 'application/x-javascript' && $allowcache) { + // The cached file is stored without version number etc. This is okay + // because $CFG->cachedir is cleared each time there is a plugin update, + // such as a new version of a tinymce plugin. + + // Flatten filename and include cache location. + $cache = $CFG->cachedir . '/editor_tinymce/pluginjs'; + $cachefile = $cache . '/' . $tinymceplugin . + str_replace('/', '_', $innerpath); + + // If it doesn't exist, minify it and save to that location. + if (!file_exists($cachefile)) { + $content = js_minify(array($file)); + js_write_cache_file_content($cachefile, $content); + } + + $file = $cachefile; +} + +// Serve file. +header('Content-Length: ' . filesize($file)); +header('Content-Type: ' . $mimetype); +readfile($file);