MDL-40621 fixed small bug in function editor_tinymce_plugin::fix_row()
[moodle.git] / lib / editor / tinymce / classes / plugin.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 defined('MOODLE_INTERNAL') || die();
19 /**
20  * TinyMCE text editor plugin base class.
21  *
22  * This is a base class for TinyMCE plugins implemented within Moodle. These
23  * plugins can optionally provide new buttons/plugins within TinyMCE itself,
24  * or configure the TinyMCE options.
25  *
26  * As well as overridable functions, other utility functions in this class
27  * can be used when writing the plugins.
28  *
29  * Finally, a static function in this class is used to call into all the
30  * plugins when required.
31  *
32  * @package editor_tinymce
33  * @copyright 2012 The Open University
34  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35  */
36 abstract class editor_tinymce_plugin {
37     /** @var string Plugin folder */
38     protected $plugin;
40     /** @var array Plugin settings */
41     protected $config = null;
43     /** @var array list of buttons defined by this plugin */
44     protected $buttons = array();
46     /**
47      * @param string $plugin Name of folder
48      */
49     public function __construct($plugin) {
50         $this->plugin = $plugin;
51     }
53     /**
54      * Returns list of buttons defined by this plugin.
55      * useful mostly as information when setting custom toolbar.
56      *
57      * @return array
58      */
59     public function get_buttons() {
60         return $this->buttons;
61     }
62     /**
63      * Makes sure config is loaded and cached.
64      * @return void
65      */
66     protected function load_config() {
67         if (!isset($this->config)) {
68             $name = $this->get_name();
69             $this->config = get_config("tinymce_$name");
70         }
71     }
73     /**
74      * Returns plugin config value.
75      * @param  string $name
76      * @param  string $default value if config does not exist yet
77      * @return string value or default
78      */
79     public function get_config($name, $default = null) {
80         $this->load_config();
81         return isset($this->config->$name) ? $this->config->$name : $default;
82     }
84     /**
85      * Sets plugin config value.
86      * @param  string $name name of config
87      * @param  string $value string config value, null means delete
88      * @return string value
89      */
90     public function set_config($name, $value) {
91         $pluginname = $this->get_name();
92         $this->load_config();
93         if ($value === null) {
94             unset($this->config->$name);
95         } else {
96             $this->config->$name = $value;
97         }
98         set_config($name, $value, "tinymce_$pluginname");
99     }
101     /**
102      * Returns name of this tinymce plugin.
103      * @return string
104      */
105     public function get_name() {
106         // All class names start with "tinymce_".
107         $words = explode('_', get_class($this), 2);
108         return $words[1];
109     }
111     /**
112      * Adjusts TinyMCE init parameters for this plugin.
113      *
114      * Subclasses must implement this function in order to carry out changes
115      * to the TinyMCE settings.
116      *
117      * @param array $params TinyMCE init parameters array
118      * @param context $context Context where editor is being shown
119      * @param array $options Options for this editor
120      */
121     protected abstract function update_init_params(array &$params, context $context,
122             array $options = null);
124     /**
125      * Gets the order in which to run this plugin. Order usually only matters if
126      * (a) the place you add your button might depend on another plugin, or
127      * (b) you want to make some changes to layout etc. that should happen last.
128      * The default order is 100; within that, plugins are sorted alphabetically.
129      * Return a lower number if you want this plugin to run earlier, or a higher
130      * number if you want it to run later.
131      */
132     protected function get_sort_order() {
133         return 100;
134     }
136     /**
137      * Adds a button to the editor, after another button (or at the end).
138      *
139      * Specify the location of this button using the $after variable. If you
140      * leave this blank, the button will be added at the end.
141      *
142      * If you want to try different possible locations depending on existing
143      * plugins you can set $alwaysadd to false and check the return value
144      * to see if it succeeded.
145      *
146      * @param array $params TinyMCE init parameters array
147      * @param int $row Row to add button to (1 to 3)
148      * @param string $button Identifier of button/plugin
149      * @param string $after Adds button directly after the named plugin
150      * @param bool $alwaysadd If specified $after string not found, add at end
151      * @return bool True if added
152      */
153     protected function add_button_after(array &$params, $row, $button,
154             $after = '', $alwaysadd = true) {
156         if ($this->is_button_present($params, $button)) {
157             return true;
158         }
160         $row = $this->fix_row($params, $row);
162         $field = 'theme_advanced_buttons' . $row;
163         $old = $params[$field];
165         // Empty = add at end.
166         if ($after === '') {
167             $params[$field] = $old . ',' . $button;
168             return true;
169         }
171         // Try to add after given plugin.
172         $params[$field] = preg_replace('~(,|^)(' . preg_quote($after) . ')(,|$)~',
173                 '$1$2,' . $button . '$3', $old);
174         if ($params[$field] !== $old) {
175             return true;
176         }
178         // If always adding, recurse to add it empty.
179         if ($alwaysadd) {
180             return $this->add_button_after($params, $row, $button);
181         }
183         // Otherwise return false (failed to add).
184         return false;
185     }
187     /**
188      * Adds a button to the editor.
189      *
190      * Specify the location of this button using the $before variable. If you
191      * leave this blank, the button will be added at the start.
192      *
193      * If you want to try different possible locations depending on existing
194      * plugins you can set $alwaysadd to false and check the return value
195      * to see if it succeeded.
196      *
197      * @param array $params TinyMCE init parameters array
198      * @param int $row Row to add button to (1 to 10)
199      * @param string $button Identifier of button/plugin
200      * @param string $before Adds button directly before the named plugin
201      * @param bool $alwaysadd If specified $before string not found, add at start
202      * @return bool True if added
203      */
204     protected function add_button_before(array &$params, $row, $button,
205             $before = '', $alwaysadd = true) {
207         if ($this->is_button_present($params, $button)) {
208             return true;
209         }
210         $row = $this->fix_row($params, $row);
212         $field = 'theme_advanced_buttons' . $row;
213         $old = $params[$field];
215         // Empty = add at start.
216         if ($before === '') {
217             $params[$field] = $button . ',' . $old;
218             return true;
219         }
221         // Try to add before given plugin.
222         $params[$field] = preg_replace('~(,|^)(' . preg_quote($before) . ')(,|$)~',
223                 '$1' . $button . ',$2$3', $old);
224         if ($params[$field] !== $old) {
225             return true;
226         }
228         // If always adding, recurse to add it empty.
229         if ($alwaysadd) {
230             return $this->add_button_before($params, $row, $button);
231         }
233         // Otherwise return false (failed to add).
234         return false;
235     }
237     /**
238      * Tests if button already present.
239      * @param array $params
240      * @param string $button
241      * @return bool
242      */
243     private function is_button_present(array $params, $button) {
244         for($i=1; $i<=10; $i++) {
245             $field = 'theme_advanced_buttons' . $i;
246             if (!isset($params[$field])) {
247                 continue;
248             }
249             $buttons = explode(',', $params[$field]);
250             if (in_array($button, $buttons)) {
251                 return true;
252             }
253         }
254         return false;
255     }
257     /**
258      * Checks the row value is valid, fix if necessary.
259      *
260      * @param array $params TinyMCE init parameters array
261      * @param int $row Row to add button if exists
262      * @return int requested row if exists, lower number if does not exist.
263      */
264     private function fix_row(array &$params, $row) {
265         $row = ($row < 1) ? 1 : (int)$row;
266         $row = ($row > 10) ? 10 : $row;
268         $field = 'theme_advanced_buttons' . $row;
269         if (isset($params[$field])) {
270             return $row;
271         }
272         for($i=$row; $i>=1; $i--) {
273             $field = 'theme_advanced_buttons' . $i;
274             if (isset($params[$field])) {
275                 return $i;
276             }
277         }
278         // This should not happen.
279         return 1;
280     }
282     /**
283      * Adds a JavaScript plugin into TinyMCE. Note that adding a plugin does
284      * not by itself add a button; you must do both.
285      *
286      * If you leave $pluginname blank (default) it uses the folder name.
287      *
288      * @param array $params TinyMCE init parameters array
289      * @param string $pluginname Identifier for plugin within TinyMCE
290      * @param string $jsfile Name of JS file (within plugin 'tinymce' directory)
291      */
292     protected function add_js_plugin(&$params, $pluginname='', $jsfile='editor_plugin.js') {
293         global $CFG;
295         // Set default plugin name.
296         if ($pluginname === '') {
297             $pluginname = $this->plugin;
298         }
300         // Add plugin to list in params, so it doesn't try to load it again.
301         $params['plugins'] .= ',-' . $pluginname;
303         // Add special param that causes Moodle TinyMCE init to load the plugin.
304         if (!isset($params['moodle_init_plugins'])) {
305             $params['moodle_init_plugins'] = '';
306         } else {
307             $params['moodle_init_plugins'] .= ',';
308         }
310         // Get URL of main JS file and store in params.
311         $jsurl = $this->get_tinymce_file_url($jsfile, false);
312         $params['moodle_init_plugins'] .= $pluginname . ':' . $jsurl;
313     }
315     /**
316      * Returns URL to files in the TinyMCE folder within this plugin, suitable
317      * for client-side use such as loading JavaScript files. (This URL normally
318      * goes through loader.php and contains the plugin version to ensure
319      * correct and long-term cacheing.)
320      *
321      * @param string $file Filename or path within the folder
322      * @param bool $absolute Set false to get relative URL from plugins folder
323      */
324     public function get_tinymce_file_url($file='', $absolute=true) {
325         global $CFG;
327         // Version number comes from plugin version.php, except in developer
328         // mode where the special string 'dev' is used (prevents cacheing and
329         // serves unminified JS).
330         if (debugging('', DEBUG_DEVELOPER)) {
331             $version = '-1';
332         } else {
333             $version = $this->get_version();
334         }
336         // Calculate the JS url (relative to the TinyMCE plugins folder - using
337         // relative URL saves a few bytes in each HTML page).
338         if ($CFG->slasharguments) {
339             // URL is usually from loader.php...
340             $jsurl = 'loader.php/' . $this->plugin . '/' . $version . '/' . $file;
341         } else {
342             // ...except when slash arguments are turned off it serves direct.
343             // In this situation there is no version details and it is up to
344             // the browser and server to negotiate cacheing, which will mean
345             // requesting the JS files frequently (reduced performance).
346             $jsurl = $this->plugin . '/tinymce/' . $file;
347         }
349         if ($absolute) {
350             $jsurl = $CFG->wwwroot . '/lib/editor/tinymce/plugins/' . $jsurl;
351         }
353         return $jsurl;
354     }
356     /**
357      * Obtains version number from version.php for this plugin.
358      *
359      * @return string Version number
360      */
361     protected function get_version() {
362         global $CFG;
364         $plugin = new stdClass;
365         require($CFG->dirroot . '/lib/editor/tinymce/plugins/' . $this->plugin . '/version.php');
366         return $plugin->version;
367     }
369     /**
370      * Calls all available plugins to adjust the TinyMCE init parameters.
371      *
372      * @param array $params TinyMCE init parameters array
373      * @param context $context Context where editor is being shown
374      * @param array $options Options for this editor
375      */
376     public static function all_update_init_params(array &$params,
377             context $context, array $options = null) {
378         global $CFG;
380         // Get list of plugin directories.
381         $plugins = get_plugin_list('tinymce');
383         // Get list of disabled subplugins.
384         $disabled = array();
385         if ($params['moodle_config']->disabledsubplugins) {
386             foreach (explode(',', $params['moodle_config']->disabledsubplugins) as $sp) {
387                 $sp = trim($sp);
388                 if ($sp !== '') {
389                     $disabled[$sp] = $sp;
390                 }
391             }
392         }
394         // Construct all the plugins.
395         $pluginobjects = array();
396         foreach ($plugins as $plugin => $dir) {
397             if (isset($disabled[$plugin])) {
398                 continue;
399             }
400             require_once($dir . '/lib.php');
401             $classname = 'tinymce_' . $plugin;
402             $pluginobjects[] = new $classname($plugin);
403         }
405         // Sort plugins by sort order and name.
406         usort($pluginobjects, array('editor_tinymce_plugin', 'compare_plugins'));
408         // Run the function for each plugin.
409         foreach ($pluginobjects as $obj) {
410             $obj->update_init_params($params, $context, $options);
411         }
412     }
414     /**
415      * Gets a named plugin object. Will cause fatal error if plugin doesn't exist.
416      *
417      * @param string $plugin Name of plugin e.g. 'moodleemoticon'
418      * @return editor_tinymce_plugin Plugin object
419      */
420     public static function get($plugin) {
421         $dir = get_component_directory('tinymce_' . $plugin);
422         require_once($dir . '/lib.php');
423         $classname = 'tinymce_' . $plugin;
424         return new $classname($plugin);
425     }
427     /**
428      * Compares two plugins.
429      * @param editor_tinymce_plugin $a
430      * @param editor_tinymce_plugin $b
431      * @return Negative number if $a is before $b
432      */
433     public static function compare_plugins(editor_tinymce_plugin $a, editor_tinymce_plugin $b) {
434         // Use sort order first.
435         $order = $a->get_sort_order() - $b->get_sort_order();
436         if ($order != 0) {
437             return $order;
438         }
440         // Then sort alphabetically.
441         return strcmp($a->plugin, $b->plugin);
442     }