1<?php 2/** 3 * Joomla! Content Management System 4 * 5 * @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved. 6 * @license GNU General Public License version 2 or later; see LICENSE.txt 7 */ 8 9namespace Joomla\CMS\Editor; 10 11defined('JPATH_PLATFORM') or die; 12 13use Joomla\Registry\Registry; 14 15/** 16 * Editor class to handle WYSIWYG editors 17 * 18 * @since 1.5 19 */ 20class Editor extends \JObject 21{ 22 /** 23 * An array of Observer objects to notify 24 * 25 * @var array 26 * @since 1.5 27 */ 28 protected $_observers = array(); 29 30 /** 31 * The state of the observable object 32 * 33 * @var mixed 34 * @since 1.5 35 */ 36 protected $_state = null; 37 38 /** 39 * A multi dimensional array of [function][] = key for observers 40 * 41 * @var array 42 * @since 1.5 43 */ 44 protected $_methods = array(); 45 46 /** 47 * Editor Plugin object 48 * 49 * @var object 50 * @since 1.5 51 */ 52 protected $_editor = null; 53 54 /** 55 * Editor Plugin name 56 * 57 * @var string 58 * @since 1.5 59 */ 60 protected $_name = null; 61 62 /** 63 * Object asset 64 * 65 * @var string 66 * @since 1.6 67 */ 68 protected $asset = null; 69 70 /** 71 * Object author 72 * 73 * @var string 74 * @since 1.6 75 */ 76 protected $author = null; 77 78 /** 79 * Editor instances container. 80 * 81 * @var Editor[] 82 * @since 2.5 83 */ 84 protected static $instances = array(); 85 86 /** 87 * Constructor 88 * 89 * @param string $editor The editor name 90 */ 91 public function __construct($editor = 'none') 92 { 93 $this->_name = $editor; 94 } 95 96 /** 97 * Returns the global Editor object, only creating it 98 * if it doesn't already exist. 99 * 100 * @param string $editor The editor to use. 101 * 102 * @return Editor The Editor object. 103 * 104 * @since 1.5 105 */ 106 public static function getInstance($editor = 'none') 107 { 108 $signature = serialize($editor); 109 110 if (empty(self::$instances[$signature])) 111 { 112 self::$instances[$signature] = new Editor($editor); 113 } 114 115 return self::$instances[$signature]; 116 } 117 118 /** 119 * Get the state of the Editor object 120 * 121 * @return mixed The state of the object. 122 * 123 * @since 1.5 124 */ 125 public function getState() 126 { 127 return $this->_state; 128 } 129 130 /** 131 * Attach an observer object 132 * 133 * @param array|object $observer An observer object to attach or an array with handler and event keys 134 * 135 * @return void 136 * 137 * @since 1.5 138 */ 139 public function attach($observer) 140 { 141 if (is_array($observer)) 142 { 143 if (!isset($observer['handler']) || !isset($observer['event']) || !is_callable($observer['handler'])) 144 { 145 return; 146 } 147 148 // Make sure we haven't already attached this array as an observer 149 foreach ($this->_observers as $check) 150 { 151 if (is_array($check) && $check['event'] == $observer['event'] && $check['handler'] == $observer['handler']) 152 { 153 return; 154 } 155 } 156 157 $this->_observers[] = $observer; 158 end($this->_observers); 159 $methods = array($observer['event']); 160 } 161 else 162 { 163 if (!($observer instanceof Editor)) 164 { 165 return; 166 } 167 168 // Make sure we haven't already attached this object as an observer 169 $class = get_class($observer); 170 171 foreach ($this->_observers as $check) 172 { 173 if ($check instanceof $class) 174 { 175 return; 176 } 177 } 178 179 $this->_observers[] = $observer; 180 181 // @todo We require an Editor object above but get the methods from \JPlugin - something isn't right here! 182 $methods = array_diff(get_class_methods($observer), get_class_methods('\JPlugin')); 183 } 184 185 $key = key($this->_observers); 186 187 foreach ($methods as $method) 188 { 189 $method = strtolower($method); 190 191 if (!isset($this->_methods[$method])) 192 { 193 $this->_methods[$method] = array(); 194 } 195 196 $this->_methods[$method][] = $key; 197 } 198 } 199 200 /** 201 * Detach an observer object 202 * 203 * @param object $observer An observer object to detach. 204 * 205 * @return boolean True if the observer object was detached. 206 * 207 * @since 1.5 208 */ 209 public function detach($observer) 210 { 211 $retval = false; 212 213 $key = array_search($observer, $this->_observers); 214 215 if ($key !== false) 216 { 217 unset($this->_observers[$key]); 218 $retval = true; 219 220 foreach ($this->_methods as &$method) 221 { 222 $k = array_search($key, $method); 223 224 if ($k !== false) 225 { 226 unset($method[$k]); 227 } 228 } 229 } 230 231 return $retval; 232 } 233 234 /** 235 * Initialise the editor 236 * 237 * @return void 238 * 239 * @since 1.5 240 * 241 * @deprecated 4.0 This function will not load any custom tag from 4.0 forward, use JHtml::script 242 */ 243 public function initialise() 244 { 245 // Check if editor is already loaded 246 if ($this->_editor === null) 247 { 248 return; 249 } 250 251 $args['event'] = 'onInit'; 252 253 $return = ''; 254 $results[] = $this->_editor->update($args); 255 256 foreach ($results as $result) 257 { 258 if (trim($result)) 259 { 260 // @todo remove code: $return .= $result; 261 $return = $result; 262 } 263 } 264 265 $document = \JFactory::getDocument(); 266 267 if (!empty($return) && method_exists($document, 'addCustomTag')) 268 { 269 $document->addCustomTag($return); 270 } 271 } 272 273 /** 274 * Display the editor area. 275 * 276 * @param string $name The control name. 277 * @param string $html The contents of the text area. 278 * @param string $width The width of the text area (px or %). 279 * @param string $height The height of the text area (px or %). 280 * @param integer $col The number of columns for the textarea. 281 * @param integer $row The number of rows for the textarea. 282 * @param boolean $buttons True and the editor buttons will be displayed. 283 * @param string $id An optional ID for the textarea (note: since 1.6). If not supplied the name is used. 284 * @param string $asset The object asset 285 * @param object $author The author. 286 * @param array $params Associative array of editor parameters. 287 * 288 * @return string 289 * 290 * @since 1.5 291 */ 292 public function display($name, $html, $width, $height, $col, $row, $buttons = true, $id = null, $asset = null, $author = null, $params = array()) 293 { 294 $this->asset = $asset; 295 $this->author = $author; 296 $this->_loadEditor($params); 297 298 // Check whether editor is already loaded 299 if ($this->_editor === null) 300 { 301 \JFactory::getApplication()->enqueueMessage(\JText::_('JLIB_NO_EDITOR_PLUGIN_PUBLISHED'), 'error'); 302 303 return; 304 } 305 306 // Backwards compatibility. Width and height should be passed without a semicolon from now on. 307 // If editor plugins need a unit like "px" for CSS styling, they need to take care of that 308 $width = str_replace(';', '', $width); 309 $height = str_replace(';', '', $height); 310 311 $return = null; 312 313 $args['name'] = $name; 314 $args['content'] = $html; 315 $args['width'] = $width; 316 $args['height'] = $height; 317 $args['col'] = $col; 318 $args['row'] = $row; 319 $args['buttons'] = $buttons; 320 $args['id'] = $id ?: $name; 321 $args['asset'] = $asset; 322 $args['author'] = $author; 323 $args['params'] = $params; 324 $args['event'] = 'onDisplay'; 325 326 $results[] = $this->_editor->update($args); 327 328 foreach ($results as $result) 329 { 330 if (trim($result)) 331 { 332 $return .= $result; 333 } 334 } 335 336 return $return; 337 } 338 339 /** 340 * Save the editor content 341 * 342 * @param string $editor The name of the editor control 343 * 344 * @return string 345 * 346 * @since 1.5 347 * 348 * @deprecated 4.0 Bind functionality to form submit through javascript 349 */ 350 public function save($editor) 351 { 352 $this->_loadEditor(); 353 354 // Check whether editor is already loaded 355 if ($this->_editor === null) 356 { 357 return; 358 } 359 360 $args[] = $editor; 361 $args['event'] = 'onSave'; 362 363 $return = ''; 364 $results[] = $this->_editor->update($args); 365 366 foreach ($results as $result) 367 { 368 if (trim($result)) 369 { 370 $return .= $result; 371 } 372 } 373 374 return $return; 375 } 376 377 /** 378 * Get the editor contents 379 * 380 * @param string $editor The name of the editor control 381 * 382 * @return string 383 * 384 * @since 1.5 385 * 386 * @deprecated 4.0 Use Joomla.editors API, see core.js 387 */ 388 public function getContent($editor) 389 { 390 $this->_loadEditor(); 391 392 $args['name'] = $editor; 393 $args['event'] = 'onGetContent'; 394 395 $return = ''; 396 $results[] = $this->_editor->update($args); 397 398 foreach ($results as $result) 399 { 400 if (trim($result)) 401 { 402 $return .= $result; 403 } 404 } 405 406 return $return; 407 } 408 409 /** 410 * Set the editor contents 411 * 412 * @param string $editor The name of the editor control 413 * @param string $html The contents of the text area 414 * 415 * @return string 416 * 417 * @since 1.5 418 * 419 * @deprecated 4.0 Use Joomla.editors API, see core.js 420 */ 421 public function setContent($editor, $html) 422 { 423 $this->_loadEditor(); 424 425 $args['name'] = $editor; 426 $args['html'] = $html; 427 $args['event'] = 'onSetContent'; 428 429 $return = ''; 430 $results[] = $this->_editor->update($args); 431 432 foreach ($results as $result) 433 { 434 if (trim($result)) 435 { 436 $return .= $result; 437 } 438 } 439 440 return $return; 441 } 442 443 /** 444 * Get the editor extended buttons (usually from plugins) 445 * 446 * @param string $editor The name of the editor. 447 * @param mixed $buttons Can be boolean or array, if boolean defines if the buttons are 448 * displayed, if array defines a list of buttons not to show. 449 * 450 * @return array 451 * 452 * @since 1.5 453 */ 454 public function getButtons($editor, $buttons = true) 455 { 456 $result = array(); 457 458 if (is_bool($buttons) && !$buttons) 459 { 460 return $result; 461 } 462 463 // Get plugins 464 $plugins = \JPluginHelper::getPlugin('editors-xtd'); 465 466 foreach ($plugins as $plugin) 467 { 468 if (is_array($buttons) && in_array($plugin->name, $buttons)) 469 { 470 continue; 471 } 472 473 \JPluginHelper::importPlugin('editors-xtd', $plugin->name, false); 474 $className = 'PlgEditorsXtd' . $plugin->name; 475 476 if (!class_exists($className)) 477 { 478 $className = 'PlgButton' . $plugin->name; 479 } 480 481 if (class_exists($className)) 482 { 483 $plugin = new $className($this, (array) $plugin); 484 } 485 486 // Try to authenticate 487 if (!method_exists($plugin, 'onDisplay')) 488 { 489 continue; 490 } 491 492 $button = $plugin->onDisplay($editor, $this->asset, $this->author); 493 494 if (empty($button)) 495 { 496 continue; 497 } 498 499 if (is_array($button)) 500 { 501 $result = array_merge($result, $button); 502 continue; 503 } 504 505 $result[] = $button; 506 } 507 508 return $result; 509 } 510 511 /** 512 * Load the editor 513 * 514 * @param array $config Associative array of editor config parameters 515 * 516 * @return mixed 517 * 518 * @since 1.5 519 */ 520 protected function _loadEditor($config = array()) 521 { 522 // Check whether editor is already loaded 523 if ($this->_editor !== null) 524 { 525 return; 526 } 527 528 // Build the path to the needed editor plugin 529 $name = \JFilterInput::getInstance()->clean($this->_name, 'cmd'); 530 $path = JPATH_PLUGINS . '/editors/' . $name . '/' . $name . '.php'; 531 532 if (!is_file($path)) 533 { 534 \JLog::add(\JText::_('JLIB_HTML_EDITOR_CANNOT_LOAD'), \JLog::WARNING, 'jerror'); 535 536 return false; 537 } 538 539 // Require plugin file 540 require_once $path; 541 542 // Get the plugin 543 $plugin = \JPluginHelper::getPlugin('editors', $this->_name); 544 545 // If no plugin is published we get an empty array and there not so much to do with it 546 if (empty($plugin)) 547 { 548 return false; 549 } 550 551 $params = new Registry($plugin->params); 552 $params->loadArray($config); 553 $plugin->params = $params; 554 555 // Build editor plugin classname 556 $name = 'PlgEditor' . $this->_name; 557 558 if ($this->_editor = new $name($this, (array) $plugin)) 559 { 560 // Load plugin parameters 561 $this->initialise(); 562 \JPluginHelper::importPlugin('editors-xtd'); 563 } 564 } 565} 566