Completed
Pull Request — master (#6257)
by Damian
16:41
created

TinyMCEConfig::setTheme()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 4
rs 10
1
<?php
2
3
namespace SilverStripe\Forms\HTMLEditor;
4
5
use SilverStripe\Core\Convert;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\Director;
8
use SilverStripe\View\Requirements;
9
use SilverStripe\View\SSViewer;
10
use SilverStripe\View\ThemeResourceLoader;
11
use TinyMCE_Compressor;
12
13
/**
14
 * Default configuration for HtmlEditor specific to tinymce
15
 */
16
class TinyMCEConfig extends HTMLEditorConfig {
17
18
	/**
19
	 * Location of module relative to BASE_DIR. This must contain the following dirs
20
	 * - plugins
21
	 * - themes
22
	 * - skins
23
	 *
24
	 * If left blank defaults to ADMIN_THIRDPARTY_DIR . '/tinymce'
25
	 *
26
	 * @config
27
	 * @var string
28
	 */
29
	private static $base_dir = null;
30
31
	/**
32
	 * TinyMCE JS settings
33
	 *
34
	 * @link https://www.tinymce.com/docs/configure/
35
	 *
36
	 * @var array
37
	 */
38
    protected $settings = array(
39
		'fix_list_elements' => true, // https://www.tinymce.com/docs/configure/content-filtering/#fix_list_elements
40
		'friendly_name' => '(Please set a friendly name for this config)',
41
		'priority' => 0, // used for Per-member config override
42
		'browser_spellcheck' => true,
43
		'body_class' => 'typography',
44
        'elementpath' => false, // https://www.tinymce.com/docs/configure/editor-appearance/#elementpath
45
		'relative_urls' => true,
46
		'remove_script_host' => true,
47
		'convert_urls' => false, // Prevent site-root images being rewritten to base relative
48
		'menubar' => false,
49
		'language' => 'en',
50
    );
51
52
	/**
53
	 * Holder list of enabled plugins
54
     *
55
     * @var array
56
	 */
57
	protected $plugins = array(
58
		'table' => null,
59
		'emoticons' => null,
60
		'paste' => null,
61
		'code' => null,
62
		'link' => null,
63
		'importcss' => null,
64
	);
65
66
	/**
67
	 * Theme name
68
	 *
69
	 * @var string
70
	 */
71
	protected $theme = 'modern';
72
73
	/**
74
	 * Get the theme
75
	 *
76
	 * @return string
77
	 */
78
	public function getTheme() {
79
		return $this->theme;
80
	}
81
82
	/**
83
	 * Set the theme name
84
	 *
85
	 * @param string $theme
86
	 * @return $this
87
	 */
88
	public function setTheme($theme) {
89
		$this->theme = $theme;
90
		return $this;
91
	}
92
93
	/**
94
	 * Holder list of buttons, organised by line. This array is 1-based indexed array
95
     *
96
     * {@link https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols}
97
     *
98
     * @var array
99
	 */
100
	protected $buttons = array(
101
		1 => array(
102
            'bold', 'italic', 'underline', 'removeformat', '|',
103
			'alignleft', 'aligncenter', 'alignright', 'alignjustify', '|',
104
			'bullist', 'numlist', 'outdent', 'indent',
105
        ),
106
		2 => array(
107
            'formatselect', '|',
108
			'paste', 'pastetext', '|',
109
			'table', 'ssmedia', 'sslink', 'unlink', '|',
110
			'code'
111
        ),
112
		3 => array()
113
	);
114
115
	public function getOption($key) {
116
		if(isset($this->settings[$key])) {
117
            return $this->settings[$key];
118
        }
119
        return null;
120
	}
121
122
	public function setOption($key,$value) {
123
		$this->settings[$key] = $value;
124
		return $this;
125
	}
126
127
	public function setOptions($options) {
128
		foreach ($options as $key => $value) {
129
			$this->settings[$key] = $value;
130
		}
131
		return $this;
132
	}
133
134
    /**
135
     * Get all settings
136
     *
137
     * @return array
138
     */
139
    protected function getSettings() {
140
        return $this->settings;
141
    }
142
143
    public function getAttributes() {
144
        return [
145
            'data-editor' => 'tinyMCE', // Register ss.editorWrappers.tinyMCE
146
            'data-config' => Convert::array2json($this->getConfig())
147
        ];
148
    }
149
150
	/**
151
	 * Enable one or several plugins. Will maintain unique list if already
152
	 * enabled plugin is re-passed. If passed in as a map of plugin-name to path,
153
	 * the plugin will be loaded by tinymce.PluginManager.load() instead of through tinyMCE.init().
154
	 * Keep in mind that these externals plugins require a dash-prefix in their name.
155
	 *
156
	 * @see http://wiki.moxiecode.com/index.php/TinyMCE:API/tinymce.PluginManager/load
157
	 *
158
	 * If passing in a non-associative array, the plugin name should be located in the standard tinymce
159
	 * plugins folder.
160
	 *
161
	 * If passing in an associative array, the key of each item should be the plugin name.
162
	 * The value of each item is one of:
163
	 *  - null - Will be treated as a stardard plugin in the standard location
164
	 *  - relative path - Will be treated as a relative url
165
	 *  - absolute url - Some url to an external plugin
166
	 *
167
	 * @param string $plugin,... a string, or several strings, or a single array of strings - The plugins to enable
0 ignored issues
show
Documentation introduced by
There is no parameter named $plugin,.... Did you maybe mean $plugin?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
168
	 * @return $this
169
	 */
170
	public function enablePlugins($plugin) {
0 ignored issues
show
Unused Code introduced by
The parameter $plugin is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
171
		$plugins = func_get_args();
172
		if (is_array(current($plugins))) {
173
            $plugins = current($plugins);
174
        }
175
		foreach ($plugins as $name => $path) {
176
			// if plugins are passed without a path
177
			if(is_numeric($name)) {
178
				$name = $path;
179
				$path = null;
180
			}
181
			if (!array_key_exists($name, $this->plugins)) {
182
                $this->plugins[$name] = $path;
183
            }
184
		}
185
		return $this;
186
	}
187
188
	/**
189
	 * Enable one or several plugins. Will properly handle being passed a plugin that is already disabled
190
	 * @param string $plugin,... a string, or several strings, or a single array of strings - The plugins to enable
0 ignored issues
show
Documentation introduced by
There is no parameter named $plugin,.... Did you maybe mean $plugin?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
191
	 * @return $this
192
	 */
193
	public function disablePlugins($plugin) {
0 ignored issues
show
Unused Code introduced by
The parameter $plugin is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
194
		$plugins = func_get_args();
195
		if (is_array(current($plugins))) {
196
            $plugins = current($plugins);
197
        }
198
		foreach ($plugins as $name) {
199
			unset($this->plugins[$name]);
200
		}
201
		return $this;
202
	}
203
204
	/**
205
     * Gets the list of all enabled plugins as an associative array.
206
     * Array keys are the plugin names, and values are potentially the plugin location
207
     *
208
	 * @return array
209
	 */
210
	public function getPlugins() {
211
		return $this->plugins;
212
	}
213
214
	/**
215
	 * Get list of plugins without custom locations, which is the set of
216
	 * plugins which can be loaded via the standard plugin path, and could
217
	 * potentially be minified
218
	 *
219
	 * @return array
220
	 */
221
	public function getInternalPlugins() {
222
		// Return only plugins with no custom url
223
		$plugins = [];
224
		foreach($this->getPlugins() as $name => $url) {
225
			if(empty($url)) {
226
				$plugins[] = $name;
227
			}
228
		}
229
		return $plugins;
230
	}
231
232
    /**
233
     * Get all button rows, skipping empty rows
234
     *
235
     * @return array
236
     */
237
    public function getButtons() {
238
        return array_filter($this->buttons);
239
    }
240
241
	/**
242
	 * Totally re-set the buttons on a given line
243
	 *
244
	 * @param int $line The line number to redefine, from 1 to 3
245
	 * @param string $buttons,... A string or several strings, or a single array of strings.
0 ignored issues
show
Documentation introduced by
There is no parameter named $buttons,.... Did you maybe mean $buttons?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
246
     * The button names to assign to this line.
247
	 * @return $this
248
	 */
249
	public function setButtonsForLine($line, $buttons) {
250
		if (func_num_args() > 2) {
251
			$buttons = func_get_args();
252
			array_shift($buttons);
253
		}
254
		$this->buttons[$line] = is_array($buttons) ? $buttons : array($buttons);
255
		return $this;
256
	}
257
258
	/**
259
	 * Add buttons to the end of a line
260
	 * @param int $line The line number to redefine, from 1 to 3
261
	 * @param string $buttons,... A string or several strings, or a single array of strings.
0 ignored issues
show
Documentation introduced by
There is no parameter named $buttons,.... Did you maybe mean $buttons?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
262
     * The button names to add to this line
263
	 * @return $this
264
	 */
265
	public function addButtonsToLine($line, $buttons) {
266
        if(func_num_args() > 2) {
267
            $buttons = func_get_args();
268
            array_shift($buttons);
269
        }
270
        if(!is_array($buttons)) {
271
            $buttons = [$buttons];
272
        }
273
		foreach ($buttons as $button) {
274
			$this->buttons[$line][] = $button;
275
		}
276
		return $this;
277
	}
278
279
	/**
280
	 * Internal function for adding and removing buttons related to another button
281
	 * @param string $name The name of the button to modify
282
	 * @param int $offset The offset relative to that button to perform an array_splice at.
283
     * 0 for before $name, 1 for after.
284
	 * @param int $del The number of buttons to remove at the position given by index(string) + offset
285
	 * @param mixed $add An array or single item to insert at the position given by index(string) + offset,
286
	 * or null for no insertion
287
	 * @return bool True if $name matched a button, false otherwise
288
	 */
289
	protected function modifyButtons($name, $offset, $del=0, $add=null) {
290
		foreach ($this->buttons as &$buttons) {
291
			if (($idx = array_search($name, $buttons)) !== false) {
292
				if ($add) {
293
                    array_splice($buttons, $idx + $offset, $del, $add);
294
                } else {
295
                    array_splice($buttons, $idx + $offset, $del);
296
                }
297
				return true;
298
			}
299
		}
300
		return false;
301
	}
302
303
	/**
304
	 * Insert buttons before the first occurance of another button
305
	 * @param string $before the name of the button to insert other buttons before
306
	 * @param string $buttons,... a string, or several strings, or a single array of strings.
0 ignored issues
show
Documentation introduced by
There is no parameter named $buttons,.... Did you maybe mean $buttons?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
307
     * The button names to insert before that button
308
	 * @return bool True if insertion occured, false if it did not (because the given button name was not found)
309
	 */
310
	public function insertButtonsBefore($before, $buttons) {
311
        if(func_num_args() > 2) {
312
            $buttons = func_get_args();
313
            array_shift($buttons);
314
        }
315
        if(!is_array($buttons)) {
316
            $buttons = [$buttons];
317
        }
318
		return $this->modifyButtons($before, 0, 0, $buttons);
319
	}
320
321
	/**
322
	 * Insert buttons after the first occurance of another button
323
	 * @param string $after the name of the button to insert other buttons before
324
	 * @param string $buttons,... a string, or several strings, or a single array of strings.
0 ignored issues
show
Documentation introduced by
There is no parameter named $buttons,.... Did you maybe mean $buttons?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
325
     * The button names to insert after that button
326
	 * @return bool True if insertion occured, false if it did not (because the given button name was not found)
327
	 */
328
	public function insertButtonsAfter($after, $buttons) {
329
		if(func_num_args() > 2) {
330
            $buttons = func_get_args();
331
            array_shift($buttons);
332
        }
333
        if(!is_array($buttons)) {
334
            $buttons = [$buttons];
335
        }
336
		return $this->modifyButtons($after, 1, 0, $buttons);
337
	}
338
339
	/**
340
	 * Remove the first occurance of buttons
341
	 * @param string $buttons,... one or more strings - the name of the buttons to remove
0 ignored issues
show
Documentation introduced by
There is no parameter named $buttons,.... Did you maybe mean $buttons?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
342
	 */
343
	public function removeButtons($buttons) {
344
        if(func_num_args() > 1) {
345
            $buttons = func_get_args();
346
        }
347
        if(!is_array($buttons)) {
348
            $buttons = [$buttons];
349
        }
350
		foreach ($buttons as $button) {
351
			$this->modifyButtons($button, 0, 1);
352
		}
353
	}
354
355
	/**
356
	 * Generate the JavaScript that will set TinyMCE's configuration:
357
	 * - Parse all configurations into JSON objects to be used in JavaScript
358
	 * - Includes TinyMCE and configurations using the {@link Requirements} system
359
     *
360
     * @return array
361
	 */
362
	protected function getConfig() {
363
        $settings = $this->getSettings();
364
365
		// https://www.tinymce.com/docs/configure/url-handling/#document_base_url
366
        $settings['document_base_url'] = Director::absoluteBaseURL();
367
368
		// https://www.tinymce.com/docs/api/class/tinymce.editormanager/#baseURL
369
		$tinyMCEBaseURL = Controller::join_links(
370
			Director::absoluteBaseURL(),
371
			$this->config()->get('base_dir') ?: ADMIN_THIRDPARTY_DIR . '/tinymce'
372
		);
373
		$settings['baseURL'] = $tinyMCEBaseURL;
374
375
        // map all plugins to absolute urls for loading
376
		$plugins = array();
377
        foreach($this->getPlugins() as $plugin => $path) {
378
			if(!$path) {
379
				// Empty paths: Convert to urls in standard base url
380
				$path = Controller::join_links(
381
					$tinyMCEBaseURL,
382
					"plugins/{$plugin}/plugin.min.js"
383
				);
384
			} elseif(!Director::is_absolute_url($path)) {
385
				// Non-absolute urls are made absolute
386
				$path = Director::absoluteURL($path);
387
			}
388
            $plugins[$plugin] = $path;
389
        }
390
391
        // https://www.tinymce.com/docs/configure/integration-and-setup/#external_plugins
392
        if($plugins) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $plugins of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
393
            $settings['external_plugins'] = $plugins;
394
        }
395
396
        // https://www.tinymce.com/docs/configure/editor-appearance/#groupingtoolbarcontrols
397
        $buttons = $this->getButtons();
398
        $settings['toolbar'] = [];
399
        foreach($buttons as $rowButtons) {
400
            $row = implode(' ', $rowButtons);
401
            if(count($buttons) > 1) {
402
                $settings['toolbar'][] = $row;
403
            } else {
404
                $settings['toolbar'] = $row;
405
            }
406
        }
407
408
        // https://www.tinymce.com/docs/configure/content-appearance/#content_css
409
		$settings['content_css'] = $this->getEditorCSS();
410
411
		// https://www.tinymce.com/docs/configure/editor-appearance/#theme_url
412
		$theme = $this->getTheme();
413
		if(!Director::is_absolute_url($theme)) {
414
			$theme = Controller::join_links($tinyMCEBaseURL, "themes/{$theme}/theme.min.js");
415
		}
416
		$settings['theme_url'] = $theme;
417
418
        // Send back
419
        return $settings;
420
	}
421
422
    /**
423
     * Get location of all editor.css files
424
     *
425
     * @return array
426
     */
427
    protected function getEditorCSS() {
428
        $editor = array();
429
430
		// Add standard editor.css
431
		$editor[] = Director::absoluteURL(FRAMEWORK_ADMIN_DIR . '/client/dist/styles/editor.css');
432
433
		// Themed editor.css
434
		$themedEditor = ThemeResourceLoader::instance()->findThemedCSS('editor', SSViewer::get_themes());
435
		if($themedEditor) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $themedEditor of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
436
			$editor[] = Director::absoluteURL($themedEditor, Director::BASE);
437
		}
438
439
		return $editor;
440
	}
441
442
	/**
443
	 * Generate gzipped TinyMCE configuration including plugins and languages.
444
	 * This ends up "pre-loading" TinyMCE bundled with the required plugins
445
	 * so that multiple HTTP requests on the client don't need to be made.
446
	 *
447
	 * @return string
448
	 */
449
	public function getScriptURL() {
450
		// If gzip is disabled just return core script url
451
		$useGzip = HTMLEditorField::config()->get('use_gzip');
452
		if(!$useGzip) {
453
			return ADMIN_THIRDPARTY_DIR . '/tinymce/tinymce.min.js';
454
		}
455
456
		// tinyMCE JS requirement
457
		require_once ADMIN_THIRDPARTY_PATH . '/tinymce/tiny_mce_gzip.php';
458
		$tag = TinyMCE_Compressor::renderTag(array(
459
			'url' => ADMIN_THIRDPARTY_DIR . '/tinymce/tiny_mce_gzip.php',
460
			'plugins' => implode(',', $this->getInternalPlugins()),
461
			'themes' => $this->getTheme(),
462
			'languages' => $this->getOption('language')
463
		), true);
464
		preg_match('/src="([^"]*)"/', $tag, $matches);
465
		return html_entity_decode($matches[1]);
466
	}
467
468
	public function init() {
469
		// include TinyMCE Javascript
470
		Requirements::javascript($this->getScriptURL());
471
	}
472
}
473