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