Issues (1686)

sources/subs/Editor.subs.php (1 issue)

1
<?php
2
3
/**
4
 * This file contains functions specific to the editing box and is
5
 * generally used for WYSIWYG type functionality.
6
 *
7
 * @package   ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
10
 *
11
 * This file contains code covered by:
12
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
13
 *
14
 * @version 2.0 dev
15
 *
16
 */
17
18
use ElkArte\Cache\Cache;
19
use ElkArte\Languages\Txt;
20
21
/**
22
 * Creates a box that can be used for richedit stuff like BBC, Smileys etc.
23
 *
24
 * @param array $editorOptions associative array of options => value
25
 *  Must contain:
26
 *   - id => unique id for the css
27
 *   - value => text for the editor or blank
28
 *   - smiley_container => ID for where the smileys will be placed
29
 *   - bbc_container => ID for where the toolbar will be placed
30
 * Optionally:
31
 *   - height => height of the initial box
32
 *   - width => width of the box (100%)
33
 *   - force_rich => force wysiwyg to be enabled
34
 *   - disable_smiley_box => boolean to turn off the smiley box
35
 *   - labels => array(
36
 *       - 'post_button' => $txt['for post button'],
37
 *     ),
38
 *   - preview_type => 2 how to act on preview click, see template_control_richedit_buttons
39
 *
40
 * @event integrate_editor_plugins
41
 * @uses GenericControls template
42
 * @uses Post language
43
 */
44
function create_control_richedit($editorOptions)
45
{
46
	global $txt, $options, $context, $settings, $scripturl;
47
48
	// Load the Post language file... for the moment at least.
49
	Txt::load('Post');
50
51
	// Every control must have a ID!
52
	assert(isset($editorOptions['id']));
53
	assert(isset($editorOptions['value']));
54
55
	// Is this the first richedit - if so we need to ensure things are initialised and that we load all needed files
56
	if (empty($context['controls']['richedit']))
57
	{
58
		// Store the name / ID we are creating for template compatibility.
59
		$context['post_box_name'] = $editorOptions['id'];
60
		$context['smiley_box_name'] = $editorOptions['smiley_container'] ?? null;
61
		$context['bbc_box_name'] = $editorOptions['bbc_container'] ?? null;
62
63
		// Don't show the smileys if they are off or not wanted.
64
		$editorOptions['disable_smiley_box'] = !empty($editorOptions['disable_smiley_box'])
65
			|| $GLOBALS['context']['smiley_set'] === 'none'
66
			|| !empty($GLOBALS['options']['show_no_smileys'])
67
			|| empty($editorOptions['smiley_container']);
68
69
		// This really has some WYSIWYG stuff.
70
		theme()->getTemplates()->load('GenericControls');
71
		loadCSSFile('jquery.sceditor.css');
72
		if (!empty($context['theme_variant']) && file_exists($settings['theme_dir'] . '/css/' . $context['theme_variant'] . '/jquery.sceditor.elk' . $context['theme_variant'] . '.css'))
73
		{
74
			loadCSSFile($context['theme_variant'] . '/jquery.sceditor.elk' . $context['theme_variant'] . '.css');
75
		}
76
77
		// JS makes the editor go round
78
		loadJavascriptFile([
79
			'editor/jquery.sceditor.bbcode.min.js',
80
			'editor/jquery.sceditor.elkarte.js',
81
			'post.js',
82
			'editor/dropAttachments.js',
83
			'editor/ilaAttachments.js',
84
			'editor/chunkUpload.js'
85
		]);
86
87
		theme()->addJavascriptVar([
88
			'post_box_name' => $editorOptions['id'],
89
			'elk_smileys_url' => $context['smiley_path'],
90
			'elk_emoji_url' => $context['emoji_path'],
91
			'bbc_quote_from' => $txt['quote_from'],
92
			'bbc_quote' => $txt['quote'],
93
			'bbc_search_on' => $txt['search_on'],
94
			'ila_filename' => $txt['file'] . ' ' . $txt['name']], true
95
		);
96
97
		// Editor language file
98
		if (!empty($txt['lang_locale']))
99
		{
100
			loadJavascriptFile($scripturl . '?action=jslocale;sa=sceditor', array('defer' => true), 'sceditor_language');
101
		}
102
103
		// Our not so concise shortcut line
104
		$context['shortcuts_text'] = $context['shortcuts_text'] ?? $txt['shortcuts'];
105
	}
106
107
	// Start off the editor...
108
	$context['controls']['richedit'][$editorOptions['id']] = [
109
		'id' => $editorOptions['id'],
110
		'value' => $editorOptions['value'],
111
		'rich_active' => !empty($options['wysiwyg_default']) || !empty($editorOptions['force_rich']) || !empty($_REQUEST[$editorOptions['id'] . '_mode']),
112
		'disable_smiley_box' => $editorOptions['disable_smiley_box'],
113
		'width' => $editorOptions['width'] ?? '100%',
114
		'height' => $editorOptions['height'] ?? '250px',
115
		'form' => $editorOptions['form'] ?? 'postmodify',
116
		'preview_type' => isset($editorOptions['preview_type']) ? (int) $editorOptions['preview_type'] : 1,
117
		'labels' => !empty($editorOptions['labels']) ? $editorOptions['labels'] : [],
118
		'locale' => !empty($txt['lang_locale']) ? $txt['lang_locale'] : 'en_US',
119
		'plugin_addons' => !empty($editorOptions['plugin_addons']) ? $editorOptions['plugin_addons'] : [],
120 2
		'plugin_options' => !empty($editorOptions['plugin_options']) ? $editorOptions['plugin_options'] : [],
121 2
		'buttons' => !empty($editorOptions['buttons']) ? $editorOptions['buttons'] : [],
122
		'hidden_fields' => !empty($editorOptions['hidden_fields']) ? $editorOptions['hidden_fields'] : [],
123 2
	];
124
125
	// Allow addons an easy way to add plugins, initialization objects, etc to the editor control
126 2
	call_integration_hook('integrate_editor_plugins', array($editorOptions['id']));
127
128
	// Switch between default images and back... mostly in case you don't have an PersonalMessage template, but do have a Post template.
129 2
	$use_defaults = isset($settings['use_default_images'], $settings['default_template']) && $settings['use_default_images'] === 'defaults';
130 2
	if ($use_defaults)
131
	{
132
		$temp = [];
133 2
		$temp[] = $settings['theme_url'];
134
		$temp[] = $settings['images_url'];
135
		$temp[] = $settings['theme_dir'];
136 2
137
		$settings['theme_url'] = $settings['default_theme_url'];
138
		$settings['images_url'] = $settings['default_images_url'];
139 2
		$settings['theme_dir'] = $settings['default_theme_dir'];
140
	}
141
142 2
	// Setup the toolbar, smileys, plugins
143 2
	$context['bbc_toolbar'] = loadEditorToolbar();
144 2
	$context['editor_bbc_toolbar'] = buildBBCToolbar($context['bbc_box_name']);
145
	$context['smileys'] = empty($editorOptions['disable_smiley_box']) ? loadEditorSmileys($context['controls']['richedit'][$editorOptions['id']]) : '';
146
	$context['editor_smileys_toolbar'] = buildSmileyToolbar(empty($editorOptions['disable_smiley_box']));
147
	$context['plugins'] = loadEditorPlugins($context['controls']['richedit'][$editorOptions['id']]);
148
	$context['plugin_options'] = getPluginOptions($context['controls']['richedit'][$editorOptions['id']], $editorOptions['id']);
149
150 2
	// Switch the URLs back... now we're back to whatever the main sub template is (like folder in PersonalMessage.)
151 2
	if ($use_defaults)
152 2
	{
153 2
		list($settings['theme_url'], $settings['images_url'], $settings['theme_dir']) = $temp;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $temp does not seem to be defined for all execution paths leading up to this point.
Loading history...
154 2
	}
155 2
156 2
	// Provide some dynamic error checking (no subject, no body, no service!)
157 2
	if (!empty($editorOptions['live_errors']))
158
	{
159
		Txt::load('Errors');
160 2
		theme()->addInlineJavascript('
161
		
162 2
	error_txts[\'no_subject\'] = ' . JavaScriptEscape($txt['error_no_subject']) . ';
163
	error_txts[\'no_message\'] = ' . JavaScriptEscape($txt['error_no_message']) . ';
164
		', true);
165
	}
166 2
}
167
168
/**
169
 *  Defines what editor plugins to load
170
 *
171
 *  What it does:
172 2
 *  - loads the JS needed by core plugins.
173
 *  - loads the CSS needed by core plugins
174 2
 *  - Will merge in plugins added by addons.
175
 *  - The editor will load and init the plugins
176
 *
177
 * @param array $editor_context The editor context containing the plugin addons.
178 2
 * @return array The list of loaded plugins.
179 2
 */
180
function loadEditorPlugins($editor_context)
181
{
182
	global $modSettings;
183
184
	$plugins = [];
185
	$neededJS = [];
186
	$neededCSS = [];
187
188
	$plugins[] = 'initialLoad';
189
	$neededJS[] = 'editor/initialLoad.plugin.js';
190
191
	if (!empty($modSettings['enableSplitTag']))
192
	{
193
		$plugins[] = 'splittag';
194
		$neededJS[] = 'editor/splittag.plugin.js';
195 2
	}
196 2
197 2
	if (!empty($modSettings['enableUndoRedo']))
198 2
	{
199 2
		$plugins[] = 'undo';
200 2
		$neededJS[] = 'editor/undo.plugin.min.js';
201 2
	}
202 2
203 2
	if (!empty($modSettings['mentions_enabled']))
204 2
	{
205 2
		$plugins[] = 'mention';
206 2
		$neededJS = array_merge($neededJS, ['editor/jquery.atwho.min.js', 'editor/jquery.caret.min.js', 'editor/mentioning.plugin.js']);
207 2
	}
208 2
209 2
	if (!empty($modSettings['enableGiphy']))
210 2
	{
211 2
		$plugins[] = 'giphy';
212 2
		$neededJS[] = 'editor/giphy.plugin.js';
213
		$neededCSS[] = 'sceditor.giphy.css';
214
	}
215
216 2
	if (!empty($neededJS))
217
	{
218
		loadJavascriptFile($neededJS, ['defer' => true]);
219 2
	}
220
221
	if (!empty($neededCSS))
222
	{
223
		loadCSSFile($neededCSS);
224
	}
225
226
	// Merge with other plugins added by core features or addons which have loaded JS/CSS as needed
227
	if (!empty($editor_context['plugin_addons']))
228
	{
229
		if (!is_array($editor_context['plugin_addons']))
230
		{
231 2
			$editor_context['plugin_addons'] = [$editor_context['plugin_addons']];
232
		}
233
234
		$plugins = array_filter(array_merge($plugins, $editor_context['plugin_addons']));
235
	}
236
237
	return $plugins;
238 2
}
239
240
/**
241
 * Loads any built in plugin options and merges in any addon defined ones.
242
 *
243 2
 * @param array $editor_context
244
 * @param string $editor_id
245
 * @return array
246
 */
247
function getPluginOptions($editor_context, $editor_id)
248
{
249
	global $modSettings;
250
251
	$plugin_options = [];
252 2
253
	if (!empty($modSettings['mentions_enabled']))
254
	{
255 2
		$plugin_options[] = '
256
			mentionOptions: {
257
				editor_id: \'' . $editor_id . '\',
258 2
				cache: {
259 2
					mentions: [],
260
					queries: [],
261
					names: []
262
				}
263
			}';
264
	}
265 2
266
	// Allow addons to insert additional editor objects
267
	if (!empty($editor_context['plugin_options']) && is_array($editor_context['plugin_options']))
268 2
	{
269
		$plugin_options = array_merge($plugin_options, $editor_context['plugin_options']);
270
	}
271
272
	return $plugin_options;
273
}
274
275
/**
276
 * Loads the editor toolbar with just the enabled commands
277
 *
278
 * Each row will be a comma seperated list of commands that the editor knows (or should)
279
 * bold,italic,quote,.....
280
 *
281
 * @return array
282
 */
283
function loadEditorToolbar()
284
{
285
	$bbc_tags = loadToolbarDefaults();
286 2
	$disabledToolbar = getDisabledBBC();
287 2
288
	// Build our toolbar, taking into account any bbc codes from integration
289 2
	$bbcToolbar = [];
290
	foreach ($bbc_tags as $row => $tagRow)
291 2
	{
292
		$bbcToolbar[$row] = $bbcToolbar[$row] ?? [];
293
		$tagsRow = [];
294 2
295
		// For each row of buttons defined, lets build our tags
296
		foreach ($tagRow as $tags)
297 2
		{
298
			foreach ($tags as $tag)
299 2
			{
300
				// Just add this code in the existing grouping
301
				if (!isset($disabledToolbar[$tag]))
302 2
				{
303
					$tagsRow[] = $tag;
304 2
				}
305
			}
306
307
			// If the row is not empty, and the last added tag is not a space, add a space.
308
			if (!empty($tagsRow) && $tagsRow[count($tagsRow) - 1] !== 'space')
309 2
			{
310
				$tagsRow[] = 'space';
311 2
			}
312
		}
313
314
		// Build that beautiful button row
315
		if (!empty($tagsRow))
316 2
		{
317
			$bbcToolbar[$row][] = implode(',', $tagsRow);
318 2
		}
319
	}
320
321
	return $bbcToolbar;
322
}
323
324 2
/**
325
 * Loads disabled BBC tags from the DB as defined in the ACP.  It
326 2
 * will then translate BBC to editor commands (b => bold) such that
327
 * disabled BBC will also disable the associated editor command button.
328
 *
329
 * @return array
330
 */
331
function getDisabledBBC()
332 2
{
333
	global $modSettings;
334
335
	// Generate a list of buttons that shouldn't be shown
336
	$disabled_tags = [];
337
	$disabledToolbar = [];
338
339
	if (!empty($modSettings['disabledBBC']))
340
	{
341
		$disabled_tags = explode(',', $modSettings['disabledBBC']);
342
	}
343
344
	// Map bbc codes to editor toolbar tags
345
	$translate_tags_to_code = ['b' => 'bold', 'i' => 'italic', 'u' => 'underline', 's' => 'strike',
346
							   'img' => 'image', 'url' => 'link', 'sup' => 'superscript', 'sub' => 'subscript', 'hr' => 'horizontalrule'];
347
348
	// Remove the toolbar buttons for any bbc tags that have been turned off in the ACP
349
	foreach ($disabled_tags as $tag)
350
	{
351
		// list is special, its prevents two tags
352
		if ($tag === 'list')
353
		{
354
			$disabledToolbar['bulletlist'] = true;
355
			$disabledToolbar['orderedlist'] = true;
356
		}
357
		elseif (isset($translate_tags_to_code[$tag]))
358
		{
359
			$disabledToolbar[$translate_tags_to_code[$tag]] = true;
360
		}
361
362
		// Tag is the same as the code, like font, color, size etc
363
		$disabledToolbar[trim($tag)] = true;
364
	}
365
366
	return $disabledToolbar;
367
}
368
369
/**
370
 * Loads the toolbar default buttons which defines what editor buttons might show
371
 *
372
 * @event integrate_bbc_buttons
373
 * @return array|mixed
374
 */
375
function loadToolbarDefaults()
376
{
377
	$bbc_tags = [];
378
379
	// The below array is used to show a command button in the editor, the execution
380
	// and display details of any added buttons must be defined in the javascript files
381
	// see jquery.sceditor.elkarte.js under the sceditor.formats.bbcode area
382
	// for examples of how to use the .set command to add codes.  Include your new
383
	// JS with addInlineJavascript() or loadJavascriptFile() in your addon
384
	$bbc_tags['row1'] = [
385
		['bold', 'italic', 'underline', 'strike'],
386
		['left', 'center', 'right', 'pre'],
387
		['image', 'link', 'giphy'],
388
		['bulletlist', 'orderedlist'],
389
		['source', 'expand'],
390
	];
391
392
	$bbc_tags['row2'] = [
393
		['tt', 'superscript', 'subscript'],
394
		['spoiler', 'footnote', 'removeformat'],
395
		['quote', 'code', 'table', 'horizontalrule', 'email'],
396
		['font', 'size', 'color'],
397
		['splittag', 'undo', 'redo'],
398
	];
399
400
	// Allow mods to add BBC buttons to the toolbar, actions are defined in the JS
401
	call_integration_hook('integrate_bbc_buttons', array(&$bbc_tags));
402
403
	return $bbc_tags;
404
}
405
406
/**
407
 * Loads the smileys for use in the required locations (postform or popup)
408
 *
409
 * What it does:
410
 * - Will load the default smileys or custom smileys as defined in ACP
411
 * - Caches the DB call for 10 mins
412
 * - Sorts smileys to proper array positions removing hidden ones
413
 *
414
 * @return array|array[]|mixed
415
 */
416
function loadEditorSmileys($editorOptions)
417
{
418
	global $context, $modSettings;
419
420
	$smileys = [
421
		'postform' => [],
422
		'popup' => [],
423
	];
424
425
	// Initialize smiley array... if not loaded before.
426
	if (empty($context['smileys']) && empty($editorOptions['disable_smiley_box']))
427
	{
428
		$temp = [];
429
		if (!Cache::instance()->getVar($temp, 'posting_smileys', 600))
430
		{
431 2
			require_once(SUBSDIR . '/Smileys.subs.php');
432
			$smileys = getEditorSmileys();
433
			foreach ($smileys as $section => $smileyRows)
434
			{
435
				$last_row = null;
436
				foreach ($smileyRows as $rowIndex => $smileRow)
437
				{
438
					$smileys[$section][$rowIndex]['smileys'][count($smileRow['smileys']) - 1]['isLast'] = true;
439
					$last_row = $rowIndex;
440
				}
441
442
				if ($last_row !== null)
443
				{
444
					$smileys[$section][$last_row]['isLast'] = true;
445
				}
446
			}
447
448
			Cache::instance()->put('posting_smileys', $smileys, 600);
449
		}
450
		else
451
		{
452
			$smileys = $temp;
453
		}
454
455
		// The smiley popup may take advantage of Jquery UI ....
456
		if (!empty($smileys['popup']))
457
		{
458
			$modSettings['jquery_include_ui'] = true;
459
		}
460
461
		return $smileys;
462
	}
463
464
	return empty($context['smileys']) ? $smileys : $context['smileys'];
465
}
466
467
function buildSmileyToolbar($useSmileys)
468
{
469
	global $context;
470
471
	if (!$useSmileys)
472
	{
473
		return ', emoticons: {}';
474
	}
475
476
	$emoticons = ',
477
		emoticons: {';
478
479
	$countLocations = count($context['smileys']);
480
	foreach ($context['smileys'] as $location => $smileyRows)
481
	{
482
		$countLocations--;
483
		if ($location === 'postform')
484
		{
485 2
			$emoticons .= '
486
				dropdown: {';
487
		}
488
489
		if ($location === 'popup')
490
		{
491
			$emoticons .= '
492 2
				popup: {';
493
		}
494
495
		$numRows = count($smileyRows);
496
497
		// This is needed because otherwise the editor will remove all the duplicate (empty)
498
		// keys and leave only 1 additional line
499
		$emptyPlaceholder = 0;
500
		foreach ($smileyRows as $smileyRow)
501
		{
502
			foreach ($smileyRow['smileys'] as $smiley)
503
			{
504
				$emoticons .= '
505
					' . JavaScriptEscape($smiley['code']) . ': {url: ' . (JavaScriptEscape((isset($smiley['emoji']) ? $context['emoji_path'] : $context['smiley_path']) . $smiley['filename'])) . ', tooltip: ' . JavaScriptEscape($smiley['description']) . '}' . (empty($smiley['isLast']) ? ',' : '');
506
			}
507
508
			if (empty($smileyRow['isLast']) && $numRows !== 1)
509
			{
510
				$emoticons .= ",'-" . $emptyPlaceholder++ . "': '',";
511
			}
512
		}
513
514
		$emoticons .= '
515
			}' . ($countLocations !== 0 ? ',' : '');
516
	}
517
518
	return $emoticons . '
519
	}';
520
}
521
522
/**
523
 *  Builds the BBC toolbar configuration for the editor.
524
 *
525
 *  What it does:
526
 *  - If the $bbcContainer parameter is null, it returns the configuration for the source mode toolbar.
527
 *  - Otherwise, it builds the toolbar configuration based on the buttons in the $context['bbc_toolbar'] array.
528
 *
529
 * @param mixed|null $bbcContainer The container of the editor. Null for source mode, otherwise the container element.
530
 * @return string The configuration for the BBC toolbar.
531
 */
532
function buildBBCToolbar($bbcContainer)
533
{
534 2
	global $context;
535
536
	if ($bbcContainer === null)
537
	{
538
		return ', 
539
			toolbar: "source"';
540
	}
541
542
	// Show all the editor command buttons
543
	$toolbar = ',
544
		toolbar: "';
545
546
	// Create the tooltag rows to display the buttons in the editor
547
	foreach ($context['bbc_toolbar'] as $buttonRow)
548
	{
549
		$toolbar .= $buttonRow[0] . '|';
550
	}
551
552
	$toolbar .= '"';
553
554
	return $toolbar;
555
}
556