Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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/>.
  16  
  17  defined('MOODLE_INTERNAL') || die();
  18  
  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;
  39  
  40      /** @var array Plugin settings */
  41      protected $config = null;
  42  
  43      /** @var array list of buttons defined by this plugin */
  44      protected $buttons = array();
  45  
  46      /**
  47       * @param string $plugin Name of folder
  48       */
  49      public function __construct($plugin) {
  50          $this->plugin = $plugin;
  51      }
  52  
  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      }
  72  
  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      }
  83  
  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      }
 100  
 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      }
 110  
 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);
 123  
 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      }
 135  
 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       * Note: button will not be added if it is already present in any row
 147       * (separator is an exception).
 148       *
 149       * The following example will add the button 'newbutton' after the
 150       * 'existingbutton' if it exists or in the end of the last row otherwise:
 151       * <pre>
 152       * if ($row = $this->find_button($params, 'existingbutton')) {
 153       *     $this->add_button_after($params, $row, 'newbutton', 'existingbutton');
 154       * } else {
 155       *     $this->add_button_after($params, $this->count_button_rows($params), 'newbutton');
 156       * }
 157       * </pre>
 158       *
 159       * @param array $params TinyMCE init parameters array
 160       * @param int $row Row to add button to (1 to 3)
 161       * @param string $button Identifier of button/plugin
 162       * @param string $after Adds button directly after the named plugin
 163       * @param bool $alwaysadd If specified $after string not found, add at end
 164       * @return bool True if added or button already exists (in any row)
 165       */
 166      protected function add_button_after(array &$params, $row, $button,
 167              $after = '', $alwaysadd = true) {
 168  
 169          if ($button !== '|' && $this->find_button($params, $button)) {
 170              return true;
 171          }
 172  
 173          $row = $this->fix_row($params, $row);
 174  
 175          $field = 'theme_advanced_buttons' . $row;
 176          $old = $params[$field];
 177  
 178          // Empty = add at end.
 179          if ($after === '') {
 180              $params[$field] = $old . ',' . $button;
 181              return true;
 182          }
 183  
 184          // Try to add after given plugin.
 185          $params[$field] = preg_replace('~(,|^)(' . preg_quote($after) . ')(,|$)~',
 186                  '$1$2,' . $button . '$3', $old);
 187          if ($params[$field] !== $old) {
 188              return true;
 189          }
 190  
 191          // If always adding, recurse to add it empty.
 192          if ($alwaysadd) {
 193              return $this->add_button_after($params, $row, $button);
 194          }
 195  
 196          // Otherwise return false (failed to add).
 197          return false;
 198      }
 199  
 200      /**
 201       * Adds a button to the editor.
 202       *
 203       * Specify the location of this button using the $before variable. If you
 204       * leave this blank, the button will be added at the start.
 205       *
 206       * If you want to try different possible locations depending on existing
 207       * plugins you can set $alwaysadd to false and check the return value
 208       * to see if it succeeded.
 209       *
 210       * Note: button will not be added if it is already present in any row
 211       * (separator is an exception).
 212       *
 213       * The following example will add the button 'newbutton' before the
 214       * 'existingbutton' if it exists or in the end of the last row otherwise:
 215       * <pre>
 216       * if ($row = $this->find_button($params, 'existingbutton')) {
 217       *     $this->add_button_before($params, $row, 'newbutton', 'existingbutton');
 218       * } else {
 219       *     $this->add_button_after($params, $this->count_button_rows($params), 'newbutton');
 220       * }
 221       * </pre>
 222       *
 223       * @param array $params TinyMCE init parameters array
 224       * @param int $row Row to add button to (1 to 10)
 225       * @param string $button Identifier of button/plugin
 226       * @param string $before Adds button directly before the named plugin
 227       * @param bool $alwaysadd If specified $before string not found, add at start
 228       * @return bool True if added or button already exists (in any row)
 229       */
 230      protected function add_button_before(array &$params, $row, $button,
 231              $before = '', $alwaysadd = true) {
 232  
 233          if ($button !== '|' && $this->find_button($params, $button)) {
 234              return true;
 235          }
 236          $row = $this->fix_row($params, $row);
 237  
 238          $field = 'theme_advanced_buttons' . $row;
 239          $old = $params[$field];
 240  
 241          // Empty = add at start.
 242          if ($before === '') {
 243              $params[$field] = $button . ',' . $old;
 244              return true;
 245          }
 246  
 247          // Try to add before given plugin.
 248          $params[$field] = preg_replace('~(,|^)(' . preg_quote($before) . ')(,|$)~',
 249                  '$1' . $button . ',$2$3', $old);
 250          if ($params[$field] !== $old) {
 251              return true;
 252          }
 253  
 254          // If always adding, recurse to add it empty.
 255          if ($alwaysadd) {
 256              return $this->add_button_before($params, $row, $button);
 257          }
 258  
 259          // Otherwise return false (failed to add).
 260          return false;
 261      }
 262  
 263      /**
 264       * Tests if button is already present.
 265       *
 266       * @param array $params TinyMCE init parameters array
 267       * @param string $button button name
 268       * @return false|int false if button is not found, row number otherwise (row numbers start from 1)
 269       */
 270      protected function find_button(array &$params, $button) {
 271          foreach ($params as $key => $value) {
 272              if (preg_match('/^theme_advanced_buttons(\d+)$/', $key, $matches) &&
 273                      strpos(','. $value. ',', ','. $button. ',') !== false) {
 274                  return (int)$matches[1];
 275              }
 276          }
 277          return false;
 278      }
 279  
 280      /**
 281       * Checks the row value is valid, fix if necessary.
 282       *
 283       * @param array $params TinyMCE init parameters array
 284       * @param int $row Row to add button if exists
 285       * @return int requested row if exists, lower number if does not exist.
 286       */
 287      private function fix_row(array &$params, $row) {
 288          if ($row <= 1) {
 289              // Row 1 is always present.
 290              return 1;
 291          } else if (isset($params['theme_advanced_buttons' . $row])) {
 292              return $row;
 293          } else {
 294              return $this->count_button_rows($params);
 295          }
 296      }
 297  
 298      /**
 299       * Counts the number of rows in TinyMCE editor (row numbering starts with 1)
 300       *
 301       * @param array $params TinyMCE init parameters array
 302       * @return int the maximum existing row number
 303       */
 304      protected function count_button_rows(array &$params) {
 305          $maxrow = 1;
 306          foreach ($params as $key => $value) {
 307              if (preg_match('/^theme_advanced_buttons(\d+)$/', $key, $matches) &&
 308                      (int)$matches[1] > $maxrow) {
 309                  $maxrow = (int)$matches[1];
 310              }
 311          }
 312          return $maxrow;
 313      }
 314  
 315      /**
 316       * Adds a JavaScript plugin into TinyMCE. Note that adding a plugin does
 317       * not by itself add a button; you must do both.
 318       *
 319       * If you leave $pluginname blank (default) it uses the folder name.
 320       *
 321       * @param array $params TinyMCE init parameters array
 322       * @param string $pluginname Identifier for plugin within TinyMCE
 323       * @param string $jsfile Name of JS file (within plugin 'tinymce' directory)
 324       */
 325      protected function add_js_plugin(&$params, $pluginname='', $jsfile='editor_plugin.js') {
 326          global $CFG;
 327  
 328          // Set default plugin name.
 329          if ($pluginname === '') {
 330              $pluginname = $this->plugin;
 331          }
 332  
 333          // Add plugin to list in params, so it doesn't try to load it again.
 334          $params['plugins'] .= ',-' . $pluginname;
 335  
 336          // Add special param that causes Moodle TinyMCE init to load the plugin.
 337          if (!isset($params['moodle_init_plugins'])) {
 338              $params['moodle_init_plugins'] = '';
 339          } else {
 340              $params['moodle_init_plugins'] .= ',';
 341          }
 342  
 343          // Get URL of main JS file and store in params.
 344          $jsurl = $this->get_tinymce_file_url($jsfile, false);
 345          $params['moodle_init_plugins'] .= $pluginname . ':' . $jsurl;
 346      }
 347  
 348      /**
 349       * Returns URL to files in the TinyMCE folder within this plugin, suitable
 350       * for client-side use such as loading JavaScript files. (This URL normally
 351       * goes through loader.php and contains the plugin version to ensure
 352       * correct and long-term cacheing.)
 353       *
 354       * @param string $file Filename or path within the folder
 355       * @param bool $absolute Set false to get relative URL from plugins folder
 356       */
 357      public function get_tinymce_file_url($file='', $absolute=true) {
 358          global $CFG;
 359  
 360          // Version number comes from plugin version.php, except in developer
 361          // mode where the special string 'dev' is used (prevents cacheing and
 362          // serves unminified JS).
 363          if ($CFG->debugdeveloper) {
 364              $version = '-1';
 365          } else {
 366              $version = $this->get_version();
 367          }
 368  
 369          // Calculate the JS url (relative to the TinyMCE plugins folder - using
 370          // relative URL saves a few bytes in each HTML page).
 371          if ($CFG->slasharguments) {
 372              // URL is usually from loader.php...
 373              $jsurl = 'loader.php/' . $this->plugin . '/' . $version . '/' . $file;
 374          } else {
 375              // ...except when slash arguments are turned off it serves direct.
 376              // In this situation there is no version details and it is up to
 377              // the browser and server to negotiate cacheing, which will mean
 378              // requesting the JS files frequently (reduced performance).
 379              $jsurl = $this->plugin . '/tinymce/' . $file;
 380          }
 381  
 382          if ($absolute) {
 383              $jsurl = $CFG->wwwroot . '/lib/editor/tinymce/plugins/' . $jsurl;
 384          }
 385  
 386          return $jsurl;
 387      }
 388  
 389      /**
 390       * Obtains version number from version.php for this plugin.
 391       *
 392       * @return string Version number
 393       */
 394      protected function get_version() {
 395          global $CFG;
 396  
 397          $plugin = new stdClass;
 398          require($CFG->dirroot . '/lib/editor/tinymce/plugins/' . $this->plugin . '/version.php');
 399          return $plugin->version;
 400      }
 401  
 402      /**
 403       * Calls all available plugins to adjust the TinyMCE init parameters.
 404       *
 405       * @param array $params TinyMCE init parameters array
 406       * @param context $context Context where editor is being shown
 407       * @param array $options Options for this editor
 408       */
 409      public static function all_update_init_params(array &$params,
 410              context $context, array $options = null) {
 411          global $CFG;
 412  
 413          // Get list of plugin directories.
 414          $plugins = core_component::get_plugin_list('tinymce');
 415  
 416          // Get list of disabled subplugins.
 417          $disabled = array();
 418          if ($params['moodle_config']->disabledsubplugins) {
 419              foreach (explode(',', $params['moodle_config']->disabledsubplugins) as $sp) {
 420                  $sp = trim($sp);
 421                  if ($sp !== '') {
 422                      $disabled[$sp] = $sp;
 423                  }
 424              }
 425          }
 426  
 427          // Construct all the plugins.
 428          $pluginobjects = array();
 429          foreach ($plugins as $plugin => $dir) {
 430              if (isset($disabled[$plugin])) {
 431                  continue;
 432              }
 433              require_once($dir . '/lib.php');
 434              $classname = 'tinymce_' . $plugin;
 435              $pluginobjects[] = new $classname($plugin);
 436          }
 437  
 438          // Sort plugins by sort order and name.
 439          usort($pluginobjects, array('editor_tinymce_plugin', 'compare_plugins'));
 440  
 441          // Run the function for each plugin.
 442          foreach ($pluginobjects as $obj) {
 443              $obj->update_init_params($params, $context, $options);
 444          }
 445      }
 446  
 447      /**
 448       * Gets a named plugin object. Will cause fatal error if plugin doesn't exist.
 449       *
 450       * @param string $plugin Name of plugin e.g. 'moodleemoticon'
 451       * @return editor_tinymce_plugin Plugin object
 452       */
 453      public static function get($plugin) {
 454          $dir = core_component::get_component_directory('tinymce_' . $plugin);
 455          require_once($dir . '/lib.php');
 456          $classname = 'tinymce_' . $plugin;
 457          return new $classname($plugin);
 458      }
 459  
 460      /**
 461       * Compares two plugins.
 462       * @param editor_tinymce_plugin $a
 463       * @param editor_tinymce_plugin $b
 464       * @return Negative number if $a is before $b
 465       */
 466      public static function compare_plugins(editor_tinymce_plugin $a, editor_tinymce_plugin $b) {
 467          // Use sort order first.
 468          $order = $a->get_sort_order() - $b->get_sort_order();
 469          if ($order != 0) {
 470              return $order;
 471          }
 472  
 473          // Then sort alphabetically.
 474          return strcmp($a->plugin, $b->plugin);
 475      }
 476  }