[BUGFIX] Fix RTE in Inline Elements
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / Classes / Form / Element / RichTextElement.php
1 <?php
2 namespace TYPO3\CMS\Rtehtmlarea\Form\Element;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
18 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
19 use TYPO3\CMS\Backend\Utility\BackendUtility;
20 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Html\RteHtmlParser;
23 use TYPO3\CMS\Core\Localization\Locales;
24 use TYPO3\CMS\Core\Localization\LocalizationFactory;
25 use TYPO3\CMS\Core\Utility\ArrayUtility;
26 use TYPO3\CMS\Core\Utility\ClientUtility;
27 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
28 use TYPO3\CMS\Core\Utility\GeneralUtility;
29 use TYPO3\CMS\Core\Utility\PathUtility;
30 use TYPO3\CMS\Lang\LanguageService;
31 use TYPO3\CMS\Rtehtmlarea\RteHtmlAreaApi;
32
33 /**
34 * Render rich text editor in FormEngine
35 */
36 class RichTextElement extends AbstractFormElement
37 {
38 /**
39 * Main result array as defined in initializeResultArray() of AbstractNode
40 *
41 * @var array
42 */
43 protected $resultArray;
44
45 /**
46 * pid of page record the TSconfig is located at.
47 * This is pid of record if table is not pages, or uid if table is pages
48 *
49 * @var int
50 */
51 protected $pidOfPageRecord;
52
53 /**
54 * pid of fixed versioned record.
55 * This is the pid of the record in normal cases, but is changed to the pid
56 * of the "mother" record in case the handled record is a versioned overlay
57 * and "mother" is located at a different pid.
58 *
59 * @var int
60 */
61 protected $pidOfVersionedMotherRecord;
62
63 /**
64 * Native, not further processed TsConfig of RTE section for this record on given pid.
65 *
66 * Example:
67 *
68 * RTE = foo
69 * RTE.bar = xy
70 *
71 * array(
72 * 'value' => 'foo',
73 * 'properties' => array(
74 * 'bar' => 'xy',
75 * ),
76 * );
77 *
78 * @var array
79 */
80 protected $vanillaRteTsConfig;
81
82 /**
83 * Based on $vanillaRteTsConfig, this property contains "processed" configuration
84 * where table and type specific RTE setup is merged into 'default.' array.
85 *
86 * @var array
87 */
88 protected $processedRteConfiguration;
89
90 /**
91 * An unique identifier based on field name to have id attributes in HTML referenced in javascript.
92 *
93 * @var string
94 */
95 protected $domIdentifier;
96
97 /**
98 * Parsed "defaultExtras" TCA
99 *
100 * @var array
101 */
102 protected $defaultExtras;
103
104 /**
105 * Some client info containing "user agent", "browser", "version", "system"
106 *
107 * @var array
108 */
109 protected $client;
110
111 /**
112 * Selected language
113 *
114 * @var string
115 */
116 protected $language;
117
118 /**
119 * TYPO3 language code of the content language
120 *
121 * @var string
122 */
123 protected $contentTypo3Language;
124
125 /**
126 * ISO language code of the content language
127 *
128 * @var string
129 */
130 protected $contentISOLanguage;
131
132 /**
133 * Uid of chosen content language
134 *
135 * @var int
136 */
137 protected $contentLanguageUid;
138
139 /**
140 * The order of the toolbar: the name is the TYPO3-button name
141 *
142 * @var string
143 */
144 protected $defaultToolbarOrder;
145
146 /**
147 * Conversion array: TYPO3 button names to htmlArea button names
148 *
149 * @var array
150 */
151 protected $convertToolbarForHtmlAreaArray = [
152 'space' => 'space',
153 'bar' => 'separator',
154 'linebreak' => 'linebreak'
155 ];
156
157 /**
158 * Final toolbar array
159 *
160 * @var array
161 */
162 protected $toolbar = [];
163
164 /**
165 * Save the buttons for the toolbar
166 *
167 * @var array
168 */
169 protected $toolbarOrderArray = [];
170
171 /**
172 * Plugin buttons
173 *
174 * @var array
175 */
176 protected $pluginButton = [];
177
178 /**
179 * Plugin labels
180 *
181 * @var array
182 */
183 protected $pluginLabel = [];
184
185 /**
186 * Array of plugin id's enabled in the current RTE editing area
187 *
188 * @var array
189 */
190 protected $pluginEnabledArray = [];
191
192 /**
193 * Cumulative array of plugin id's enabled so far in any of the RTE editing areas of the form
194 *
195 * @var array
196 */
197 protected $pluginEnabledCumulativeArray = [];
198
199 /**
200 * Array of registered plugins indexed by their plugin Id's
201 *
202 * @var array
203 */
204 protected $registeredPlugins = [];
205
206 /**
207 * This will render a <textarea> OR RTE area form field,
208 * possibly with various control/validation features
209 *
210 * @return array As defined in initializeResultArray() of AbstractNode
211 */
212 public function render()
213 {
214 $table = $this->data['tableName'];
215 $fieldName = $this->data['fieldName'];
216 $row = $this->data['databaseRow'];
217 $parameterArray = $this->data['parameterArray'];
218
219 $backendUser = $this->getBackendUserAuthentication();
220
221 $this->resultArray = $this->initializeResultArray();
222 $this->defaultExtras = BackendUtility::getSpecConfParts($parameterArray['fieldConf']['defaultExtras']);
223 $this->pidOfPageRecord = $this->data['effectivePid'];
224 BackendUtility::fixVersioningPid($table, $row);
225 $this->pidOfVersionedMotherRecord = (int)$row['pid'];
226 $this->vanillaRteTsConfig = $backendUser->getTSConfig('RTE', BackendUtility::getPagesTSconfig($this->pidOfPageRecord));
227 $this->processedRteConfiguration = BackendUtility::RTEsetup(
228 $this->vanillaRteTsConfig['properties'],
229 $table,
230 $fieldName,
231 $this->data['recordTypeValue']
232 );
233 $this->client = $this->clientInfo();
234 $this->domIdentifier = preg_replace('/[^a-zA-Z0-9_:.-]/', '_', $parameterArray['itemFormElName']);
235 $this->domIdentifier = htmlspecialchars(preg_replace('/^[^a-zA-Z]/', 'x', $this->domIdentifier));
236
237 $this->initializeLanguageRelatedProperties();
238
239 // Get skin file name from Page TSConfig if any
240 $skinFilename = trim($this->processedRteConfiguration['skin']) ?: 'EXT:rtehtmlarea/Resources/Public/Css/Skin/htmlarea.css';
241 $skinFilename = PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName($skinFilename));
242 $skinDirectory = dirname($skinFilename);
243
244 // jQuery UI Resizable style sheet and main skin stylesheet
245 $this->resultArray['stylesheetFiles'][] = $skinDirectory . '/jquery-ui-resizable.css';
246 $this->resultArray['stylesheetFiles'][] = $skinFilename;
247
248 $this->enableRegisteredPlugins();
249
250 // Configure toolbar
251 $this->setToolbar();
252
253 // Check if some plugins need to be disabled
254 $this->setPlugins();
255
256 // Merge the list of enabled plugins with the lists from the previous RTE editing areas on the same form
257 $this->pluginEnabledCumulativeArray = $this->pluginEnabledArray;
258
259 $this->addInstanceJavaScriptRegistration();
260
261 $this->addOnSubmitJavaScriptCode();
262
263 // Add RTE JavaScript
264 $this->loadRequireModulesForRTE();
265
266 // Create language labels
267 $this->createJavaScriptLanguageLabelsFromFiles();
268
269 // Get RTE init JS code
270 $this->resultArray['additionalJavaScriptPost'][] = $this->getRteInitJsCode();
271
272 $html = $this->getMainHtml();
273
274 $this->resultArray['html'] = $this->renderWizards(
275 [$html],
276 $parameterArray['fieldConf']['config']['wizards'],
277 $table,
278 $row,
279 $fieldName,
280 $parameterArray,
281 $parameterArray['itemFormElName'],
282 $this->defaultExtras,
283 true
284 );
285
286 return $this->resultArray;
287 }
288
289 /**
290 * Create main HTML elements
291 *
292 * @return string Main RTE html
293 */
294 protected function getMainHtml()
295 {
296 $backendUser = $this->getBackendUserAuthentication();
297
298 if ($this->isInFullScreenMode()) {
299 $width = '100%';
300 $height = '100%';
301 $paddingRight = '0px';
302 $editorWrapWidth = '100%';
303 } else {
304 $options = $backendUser->userTS['options.'];
305 $width = 530 + (isset($options['RTELargeWidthIncrement']) ? (int)$options['RTELargeWidthIncrement'] : 150);
306 /** @var InlineStackProcessor $inlineStackProcessor */
307 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
308 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
309 $inlineStructureDepth = $inlineStackProcessor->getStructureDepth();
310 $width -= $inlineStructureDepth > 0 ? ($inlineStructureDepth + 1) * 12 : 0;
311 $widthOverride = isset($backendUser->uc['rteWidth']) && trim($backendUser->uc['rteWidth']) ? trim($backendUser->uc['rteWidth']) : trim($this->processedRteConfiguration['RTEWidthOverride']);
312 if ($widthOverride) {
313 if (strstr($widthOverride, '%')) {
314 if ($this->client['browser'] !== 'msie') {
315 $width = (int)$widthOverride > 0 ? (int)$widthOverride : '100%';
316 }
317 } else {
318 $width = (int)$widthOverride > 0 ? (int)$widthOverride : $width;
319 }
320 }
321 $width = strstr($width, '%') ? $width : $width . 'px';
322 $height = 380 + (isset($options['RTELargeHeightIncrement']) ? (int)$options['RTELargeHeightIncrement'] : 0);
323 $heightOverride = isset($backendUser->uc['rteHeight']) && (int)$backendUser->uc['rteHeight'] ? (int)$backendUser->uc['rteHeight'] : (int)$this->processedRteConfiguration['RTEHeightOverride'];
324 $height = $heightOverride > 0 ? $heightOverride . 'px' : $height . 'px';
325 $paddingRight = '2';
326 $editorWrapWidth = '99%';
327 }
328 $rteDivStyle = 'position:relative; left:0px; top:0px; height:' . $height . '; width:' . $width . '; border: 1px solid black; padding: 2 ' . $paddingRight . ' 2 2;';
329
330 $itemFormElementName = $this->data['parameterArray']['itemFormElName'];
331
332 // This seems to result in:
333 // _TRANSFORM_bodytext (the handled field name) in case the field is a direct DB field
334 // _TRANSFORM_vDEF (constant string) in case the RTE is within a flex form
335 $triggerFieldName = preg_replace('/\\[([^]]+)\\]$/', '[_TRANSFORM_\\1]', $itemFormElementName);
336
337 $value = $this->transformDatabaseContentToEditor($this->data['parameterArray']['itemFormElValue']);
338
339 $result = [];
340 // The hidden field tells the DataHandler that processing should be done on this value.
341 $result[] = '<input type="hidden" name="' . htmlspecialchars($triggerFieldName) . '" value="RTE" />';
342 $result[] = '<div id="pleasewait' . $this->domIdentifier . '" class="pleasewait" style="display: block;" >';
343 $result[] = $this->getLanguageService()->sL('LLL:EXT:rtehtmlarea/Resources/Private/Language/locallang.xlf:Please wait');
344 $result[] = '</div>';
345 $result[] = '<div id="editorWrap' . $this->domIdentifier . '" class="editorWrap" style="visibility: hidden; width:' . $editorWrapWidth . '; height:100%;">';
346 $result[] = '<textarea ' . $this->getValidationDataAsDataAttribute($this->data['parameterArray']['fieldConf']['config']) . ' id="RTEarea' . $this->domIdentifier . '" name="' . htmlspecialchars($itemFormElementName) . '" rows="0" cols="0" style="' . htmlspecialchars($rteDivStyle) . '">';
347 $result[] = htmlspecialchars($value);
348 $result[] = '</textarea>';
349 $result[] = '</div>';
350
351 return implode(LF, $result);
352 }
353
354 /**
355 * Add registered plugins to the array of enabled plugins
356 *
357 * @return void
358 */
359 protected function enableRegisteredPlugins()
360 {
361 // Traverse registered plugins
362 if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rtehtmlarea']['plugins'])) {
363 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rtehtmlarea']['plugins'] as $pluginId => $pluginObjectConfiguration) {
364 if (is_array($pluginObjectConfiguration) && isset($pluginObjectConfiguration['objectReference'])) {
365 /** @var RteHtmlAreaApi $plugin */
366 $plugin = GeneralUtility::makeInstance($pluginObjectConfiguration['objectReference']);
367 $configuration = [
368 'language' => $this->language,
369 'contentTypo3Language' => $this->contentTypo3Language,
370 'contentISOLanguage' => $this->contentISOLanguage,
371 'contentLanguageUid' => $this->contentLanguageUid,
372 'RTEsetup' => $this->vanillaRteTsConfig,
373 'client' => $this->client,
374 'thisConfig' => $this->processedRteConfiguration,
375 'specConf' => $this->defaultExtras,
376 ];
377 if ($plugin->main($configuration)) {
378 $this->registeredPlugins[$pluginId] = $plugin;
379 // Override buttons from previously registered plugins
380 $pluginButtons = GeneralUtility::trimExplode(',', $plugin->getPluginButtons(), true);
381 foreach ($this->pluginButton as $previousPluginId => $buttonList) {
382 $this->pluginButton[$previousPluginId] = implode(',', array_diff(GeneralUtility::trimExplode(',', $this->pluginButton[$previousPluginId], true), $pluginButtons));
383 }
384 $this->pluginButton[$pluginId] = $plugin->getPluginButtons();
385 $pluginLabels = GeneralUtility::trimExplode(',', $plugin->getPluginLabels(), true);
386 foreach ($this->pluginLabel as $previousPluginId => $labelList) {
387 $this->pluginLabel[$previousPluginId] = implode(',', array_diff(GeneralUtility::trimExplode(',', $this->pluginLabel[$previousPluginId], true), $pluginLabels));
388 }
389 $this->pluginLabel[$pluginId] = $plugin->getPluginLabels();
390 $this->pluginEnabledArray[] = $pluginId;
391 }
392 }
393 }
394 }
395 // Process overrides
396 $hidePlugins = [];
397 foreach ($this->registeredPlugins as $pluginId => $plugin) {
398 /** @var RteHtmlAreaApi $plugin */
399 if ($plugin->addsButtons() && !$this->pluginButton[$pluginId]) {
400 $hidePlugins[] = $pluginId;
401 }
402 }
403 $this->pluginEnabledArray = array_unique(array_diff($this->pluginEnabledArray, $hidePlugins));
404 }
405
406 /**
407 * Set the toolbar config (only in this PHP-Object, not in JS):
408 *
409 * @return void
410 */
411 protected function setToolbar()
412 {
413 $backendUser = $this->getBackendUserAuthentication();
414
415 $this->defaultToolbarOrder = 'bar, blockstylelabel, blockstyle, textstylelabel, textstyle, linebreak,
416 bar, formattext, bold, strong, italic, emphasis, big, small, insertedtext, deletedtext, citation, code,'
417 . 'definition, keyboard, monospaced, quotation, sample, variable, bidioverride, strikethrough, subscript, superscript, underline, span,
418 bar, fontstyle, fontsize, bar, formatblock, insertparagraphbefore, insertparagraphafter, blockquote, line,
419 bar, left, center, right, justifyfull,
420 bar, orderedlist, unorderedlist, definitionlist, definitionitem, outdent, indent,
421 bar, language, showlanguagemarks,lefttoright, righttoleft,
422 bar, textcolor, bgcolor, textindicator,
423 bar, editelement, showmicrodata,
424 bar, image, emoticon, insertcharacter, insertsofthyphen, abbreviation, user,
425 bar, link, unlink,
426 bar, table,'
427 . ($this->processedRteConfiguration['hideTableOperationsInToolbar']
428 && is_array($this->processedRteConfiguration['buttons.'])
429 && is_array($this->processedRteConfiguration['buttons.']['toggleborders.'])
430 && $this->processedRteConfiguration['buttons.']['toggleborders.']['keepInToolbar'] ? ' toggleborders,' : '')
431 . 'bar, findreplace, spellcheck,
432 bar, chMode, inserttag, removeformat, bar, copy, cut, paste, pastetoggle, pastebehaviour, bar, undo, redo, bar, about, linebreak,'
433 . ($this->processedRteConfiguration['hideTableOperationsInToolbar'] ? '' : 'bar, toggleborders,')
434 . ' bar, tableproperties, tablerestyle, bar, rowproperties, rowinsertabove, rowinsertunder, rowdelete, rowsplit, bar,
435 columnproperties, columninsertbefore, columninsertafter, columndelete, columnsplit, bar,
436 cellproperties, cellinsertbefore, cellinsertafter, celldelete, cellsplit, cellmerge';
437
438 // Additional buttons from registered plugins
439 foreach ($this->registeredPlugins as $pluginId => $plugin) {
440 /** @var RteHtmlAreaApi $plugin */
441 if ($this->isPluginEnabled($pluginId)) {
442 $pluginButtons = $plugin->getPluginButtons();
443 //Add only buttons not yet in the default toolbar order
444 $addButtons = implode(
445 ',',
446 array_diff(
447 GeneralUtility::trimExplode(',', $pluginButtons, true),
448 GeneralUtility::trimExplode(',', $this->defaultToolbarOrder, true)
449 )
450 );
451 $this->defaultToolbarOrder = ($addButtons ? 'bar,' . $addButtons . ',linebreak,' : '') . $this->defaultToolbarOrder;
452 }
453 }
454 $toolbarOrder = $this->processedRteConfiguration['toolbarOrder'] ?: $this->defaultToolbarOrder;
455 // Getting rid of undefined buttons
456 $this->toolbarOrderArray = array_intersect(GeneralUtility::trimExplode(',', $toolbarOrder, true), GeneralUtility::trimExplode(',', $this->defaultToolbarOrder, true));
457 $toolbarOrder = array_unique(array_values($this->toolbarOrderArray));
458 // Fetching specConf for field from backend
459 $pList = is_array($this->defaultExtras['richtext']['parameters']) ? implode(',', $this->defaultExtras['richtext']['parameters']) : '';
460 if ($pList !== '*') {
461 // If not all
462 $show = is_array($this->defaultExtras['richtext']['parameters']) ? $this->defaultExtras['richtext']['parameters'] : [];
463 if ($this->processedRteConfiguration['showButtons']) {
464 if (!GeneralUtility::inList($this->processedRteConfiguration['showButtons'], '*')) {
465 $show = array_unique(array_merge($show, GeneralUtility::trimExplode(',', $this->processedRteConfiguration['showButtons'], true)));
466 } else {
467 $show = array_unique(array_merge($show, $toolbarOrder));
468 }
469 }
470 if (is_array($this->processedRteConfiguration['showButtons.'])) {
471 foreach ($this->processedRteConfiguration['showButtons.'] as $buttonId => $value) {
472 if ($value) {
473 $show[] = $buttonId;
474 }
475 }
476 $show = array_unique($show);
477 }
478 } else {
479 $show = $toolbarOrder;
480 }
481 $RTEkeyList = isset($backendUser->userTS['options.']['RTEkeyList']) ? $backendUser->userTS['options.']['RTEkeyList'] : '*';
482 if ($RTEkeyList !== '*') {
483 // If not all
484 $show = array_intersect($show, GeneralUtility::trimExplode(',', $RTEkeyList, true));
485 }
486 // Hiding buttons of disabled plugins
487 $hideButtons = ['space', 'bar', 'linebreak'];
488 foreach ($this->pluginButton as $pluginId => $buttonList) {
489 if (!$this->isPluginEnabled($pluginId)) {
490 $buttonArray = GeneralUtility::trimExplode(',', $buttonList, true);
491 foreach ($buttonArray as $button) {
492 $hideButtons[] = $button;
493 }
494 }
495 }
496 // Hiding labels of disabled plugins
497 foreach ($this->pluginLabel as $pluginId => $label) {
498 if (!$this->isPluginEnabled($pluginId)) {
499 $hideButtons[] = $label;
500 }
501 }
502 // Hiding buttons
503 $show = array_diff($show, GeneralUtility::trimExplode(',', $this->processedRteConfiguration['hideButtons'], true));
504 // Apply toolbar constraints from registered plugins
505 foreach ($this->registeredPlugins as $pluginId => $plugin) {
506 if ($this->isPluginEnabled($pluginId) && method_exists($plugin, 'applyToolbarConstraints')) {
507 $show = $plugin->applyToolbarConstraints($show);
508 }
509 }
510 // Getting rid of the buttons for which we have no position
511 $show = array_intersect($show, $toolbarOrder);
512 foreach ($this->registeredPlugins as $pluginId => $plugin) {
513 /** @var RteHtmlAreaApi $plugin */
514 $plugin->setToolbar($show);
515 }
516 $this->toolbar = $show;
517 }
518
519 /**
520 * Disable some plugins
521 *
522 * @return void
523 */
524 protected function setPlugins()
525 {
526 // Disabling a plugin that adds buttons if none of its buttons is in the toolbar
527 $hidePlugins = [];
528 foreach ($this->pluginButton as $pluginId => $buttonList) {
529 /** @var RteHtmlAreaApi $plugin */
530 $plugin = $this->registeredPlugins[$pluginId];
531 if ($plugin->addsButtons()) {
532 $showPlugin = false;
533 $buttonArray = GeneralUtility::trimExplode(',', $buttonList, true);
534 foreach ($buttonArray as $button) {
535 if (in_array($button, $this->toolbar)) {
536 $showPlugin = true;
537 }
538 }
539 if (!$showPlugin) {
540 $hidePlugins[] = $pluginId;
541 }
542 }
543 }
544 $this->pluginEnabledArray = array_diff($this->pluginEnabledArray, $hidePlugins);
545 // Hiding labels of disabled plugins
546 $hideLabels = [];
547 foreach ($this->pluginLabel as $pluginId => $label) {
548 if (!$this->isPluginEnabled($pluginId)) {
549 $hideLabels[] = $label;
550 }
551 }
552 $this->toolbar = array_diff($this->toolbar, $hideLabels);
553 // Adding plugins declared as prerequisites by enabled plugins
554 $requiredPlugins = [];
555 foreach ($this->registeredPlugins as $pluginId => $plugin) {
556 /** @var RteHtmlAreaApi $plugin */
557 if ($this->isPluginEnabled($pluginId)) {
558 $requiredPlugins = array_merge($requiredPlugins, GeneralUtility::trimExplode(',', $plugin->getRequiredPlugins(), true));
559 }
560 }
561 $requiredPlugins = array_unique($requiredPlugins);
562 foreach ($requiredPlugins as $pluginId) {
563 if (is_object($this->registeredPlugins[$pluginId]) && !$this->isPluginEnabled($pluginId)) {
564 $this->pluginEnabledArray[] = $pluginId;
565 }
566 }
567 $this->pluginEnabledArray = array_unique($this->pluginEnabledArray);
568 // Completing the toolbar conversion array for htmlArea
569 foreach ($this->registeredPlugins as $pluginId => $plugin) {
570 /** @var RteHtmlAreaApi $plugin */
571 if ($this->isPluginEnabled($pluginId)) {
572 $this->convertToolbarForHtmlAreaArray = array_unique(array_merge($this->convertToolbarForHtmlAreaArray, $plugin->getConvertToolbarForHtmlAreaArray()));
573 }
574 }
575 }
576
577 /**
578 * Add RTE main scripts and plugin scripts
579 *
580 * @return void
581 */
582 protected function loadRequireModulesForRTE()
583 {
584 $this->resultArray['requireJsModules'] = [];
585 $this->resultArray['requireJsModules'][] = 'TYPO3/CMS/Rtehtmlarea/HTMLArea/HTMLArea';
586 foreach ($this->pluginEnabledCumulativeArray as $pluginId) {
587 /** @var RteHtmlAreaApi $plugin */
588 $plugin = $this->registeredPlugins[$pluginId];
589 $extensionKey = is_object($plugin) ? $plugin->getExtensionKey() : 'rtehtmlarea';
590 $requirePath = 'TYPO3/CMS/' . GeneralUtility::underscoredToUpperCamelCase($extensionKey);
591 $this->resultArray['requireJsModules'][] = $requirePath . '/Plugins/' . $pluginId;
592 }
593 }
594
595 /**
596 * Return RTE initialization inline JavaScript code
597 *
598 * @return string RTE initialization inline JavaScript code
599 */
600 protected function getRteInitJsCode()
601 {
602 $skinFilename = trim($this->processedRteConfiguration['skin']) ?: 'EXT:rtehtmlarea/Resources/Public/Css/Skin/htmlarea.css';
603 $skinFilename = GeneralUtility::getFileAbsFileName($skinFilename);
604 $skinDirectory = dirname($skinFilename);
605 // Editing area style sheet
606 $editedContentCSS = GeneralUtility::createVersionNumberedFilename($skinDirectory . '/htmlarea-edited-content.css');
607 $editorUrl = GeneralUtility::getFileAbsFileName('EXT:rtehtmlarea/Resources/');
608 $editorUrl = dirname($editorUrl) . '/';
609 return 'require(["TYPO3/CMS/Rtehtmlarea/HTMLArea/HTMLArea"], function (HTMLArea) {
610 if (typeof RTEarea === "undefined") {
611 RTEarea = new Object();
612 RTEarea[0] = new Object();
613 RTEarea[0].version = "' . TYPO3_version . '";
614 RTEarea[0].editorUrl = "' . PathUtility::getAbsoluteWebPath($editorUrl) . '";
615 RTEarea[0].editorSkin = "' . PathUtility::getAbsoluteWebPath($skinDirectory) . '/";
616 RTEarea[0].editedContentCSS = "' . PathUtility::getAbsoluteWebPath($editedContentCSS) . '";
617 RTEarea.init = function() {
618 HTMLArea.init();
619 };
620 RTEarea.initEditor = function(editorNumber) {
621 if (typeof HTMLArea === "undefined" || !HTMLArea.isReady) {
622 window.setTimeout(function () {
623 RTEarea.initEditor(editorNumber);
624 }, 40);
625 } else {
626 HTMLArea.initEditor(editorNumber);
627 }
628 };
629 }
630 RTEarea.init();
631 });';
632 }
633
634 /**
635 * Return the Javascript code for configuring the RTE
636 *
637 * @return void
638 */
639 protected function addInstanceJavaScriptRegistration()
640 {
641 $backendUser = $this->getBackendUserAuthentication();
642
643 $jsArray = [];
644 $jsArray[] = 'window.HTMLArea = window.HTMLArea || {};';
645 $jsArray[] = 'if (typeof configureEditorInstance === "undefined") {';
646 $jsArray[] = ' configureEditorInstance = new Object();';
647 $jsArray[] = '}';
648 $jsArray[] = 'configureEditorInstance[' . GeneralUtility::quoteJSvalue($this->domIdentifier) . '] = function() {';
649 $jsArray[] = 'if (typeof RTEarea === "undefined") {';
650 $jsArray[] = ' window.setTimeout("configureEditorInstance[' . GeneralUtility::quoteJSvalue($this->domIdentifier) . ']();", 40);';
651 $jsArray[] = '} else {';
652 $jsArray[] = 'editornumber = ' . GeneralUtility::quoteJSvalue($this->domIdentifier) . ';';
653 $jsArray[] = 'RTEarea[editornumber] = new Object();';
654 $jsArray[] = 'RTEarea[editornumber].RTEtsConfigParams = "&RTEtsConfigParams=' . rawurlencode($this->RTEtsConfigParams()) . '";';
655 $jsArray[] = 'RTEarea[editornumber].number = editornumber;';
656 $jsArray[] = 'RTEarea[editornumber].deleted = false;';
657 $jsArray[] = 'RTEarea[editornumber].textAreaId = ' . GeneralUtility::quoteJSvalue($this->domIdentifier) . ';';
658 $jsArray[] = 'RTEarea[editornumber].id = "RTEarea" + editornumber;';
659 $jsArray[] = 'RTEarea[editornumber].RTEWidthOverride = "'
660 . (isset($backendUser->uc['rteWidth']) && trim($backendUser->uc['rteWidth'])
661 ? trim($backendUser->uc['rteWidth'])
662 : trim($this->processedRteConfiguration['RTEWidthOverride'])) . '";';
663 $jsArray[] = 'RTEarea[editornumber].RTEHeightOverride = "'
664 . (isset($backendUser->uc['rteHeight']) && (int)$backendUser->uc['rteHeight']
665 ? (int)$backendUser->uc['rteHeight']
666 : (int)$this->processedRteConfiguration['RTEHeightOverride']) . '";';
667 $jsArray[] = 'RTEarea[editornumber].resizable = '
668 . (isset($backendUser->uc['rteResize']) && $backendUser->uc['rteResize']
669 ? 'true;'
670 : (trim($this->processedRteConfiguration['rteResize']) ? 'true;' : 'false;'));
671 $jsArray[] = 'RTEarea[editornumber].maxHeight = "'
672 . (isset($backendUser->uc['rteMaxHeight']) && (int)$backendUser->uc['rteMaxHeight']
673 ? trim($backendUser->uc['rteMaxHeight'])
674 : ((int)$this->processedRteConfiguration['rteMaxHeight'] ?: '2000')) . '";';
675 $jsArray[] = 'RTEarea[editornumber].fullScreen = ' . ($this->isInFullScreenMode() ? 'true;' : 'false;');
676 $jsArray[] = 'RTEarea[editornumber].showStatusBar = ' . (trim($this->processedRteConfiguration['showStatusBar']) ? 'true;' : 'false;');
677 $jsArray[] = 'RTEarea[editornumber].enableWordClean = ' . (trim($this->processedRteConfiguration['enableWordClean']) ? 'true;' : 'false;');
678 $jsArray[] = 'RTEarea[editornumber].htmlRemoveComments = ' . (trim($this->processedRteConfiguration['removeComments']) ? 'true;' : 'false;');
679 $jsArray[] = 'RTEarea[editornumber].disableEnterParagraphs = ' . (trim($this->processedRteConfiguration['disableEnterParagraphs']) ? 'true;' : 'false;');
680 $jsArray[] = 'RTEarea[editornumber].disableObjectResizing = ' . (trim($this->processedRteConfiguration['disableObjectResizing']) ? 'true;' : 'false;');
681 $jsArray[] = 'RTEarea[editornumber].removeTrailingBR = ' . (trim($this->processedRteConfiguration['removeTrailingBR']) ? 'true;' : 'false;');
682 $jsArray[] = 'RTEarea[editornumber].useCSS = ' . (trim($this->processedRteConfiguration['useCSS']) ? 'true' : 'false') . ';';
683 $jsArray[] = 'RTEarea[editornumber].disablePCexamples = ' . (trim($this->processedRteConfiguration['disablePCexamples']) ? 'true;' : 'false;');
684 $jsArray[] = 'RTEarea[editornumber].showTagFreeClasses = ' . (trim($this->processedRteConfiguration['showTagFreeClasses']) ? 'true;' : 'false;');
685 $jsArray[] = 'RTEarea[editornumber].tceformsNested = ' . (!empty($this->data) ? json_encode($this->data['tabAndInlineStack']) : '[]') . ';';
686 $jsArray[] = 'RTEarea[editornumber].dialogueWindows = new Object();';
687 if (isset($this->processedRteConfiguration['dialogueWindows.']['defaultPositionFromTop'])) {
688 $jsArray[] = 'RTEarea[editornumber].dialogueWindows.positionFromTop = ' . (int)$this->processedRteConfiguration['dialogueWindows.']['defaultPositionFromTop'] . ';';
689 }
690 if (isset($this->processedRteConfiguration['dialogueWindows.']['defaultPositionFromLeft'])) {
691 $jsArray[] = 'RTEarea[editornumber].dialogueWindows.positionFromLeft = ' . (int)$this->processedRteConfiguration['dialogueWindows.']['defaultPositionFromLeft'] . ';';
692 }
693 $jsArray[] = 'RTEarea[editornumber].sys_language_content = "' . $this->contentLanguageUid . '";';
694 $jsArray[] = 'RTEarea[editornumber].typo3ContentLanguage = "' . $this->contentTypo3Language . '";';
695 $jsArray[] = 'RTEarea[editornumber].userUid = "' . 'BE_' . $backendUser->user['uid'] . '";';
696
697 // Setting the plugin flags
698 $jsArray[] = 'RTEarea[editornumber].plugin = new Object();';
699 foreach ($this->pluginEnabledArray as $pluginId) {
700 $jsArray[] = 'RTEarea[editornumber].plugin.' . $pluginId . ' = true;';
701 }
702
703 // Setting the buttons configuration
704 $jsArray[] = 'RTEarea[editornumber].buttons = new Object();';
705 if (is_array($this->processedRteConfiguration['buttons.'])) {
706 foreach ($this->processedRteConfiguration['buttons.'] as $buttonIndex => $conf) {
707 $button = substr($buttonIndex, 0, -1);
708 if (is_array($conf)) {
709 $jsArray[] = 'RTEarea[editornumber].buttons.' . $button . ' = ' . $this->buildNestedJSArray($conf) . ';';
710 }
711 }
712 }
713
714 // Setting the list of tags to be removed if specified in the RTE config
715 if (trim($this->processedRteConfiguration['removeTags'])) {
716 $jsArray[] = 'RTEarea[editornumber].htmlRemoveTags = /^(' . implode('|', GeneralUtility::trimExplode(',', $this->processedRteConfiguration['removeTags'], true)) . ')$/i;';
717 }
718
719 // Setting the list of tags to be removed with their contents if specified in the RTE config
720 if (trim($this->processedRteConfiguration['removeTagsAndContents'])) {
721 $jsArray[] = 'RTEarea[editornumber].htmlRemoveTagsAndContents = /^(' . implode('|', GeneralUtility::trimExplode(',', $this->processedRteConfiguration['removeTagsAndContents'], true)) . ')$/i;';
722 }
723
724 // Setting array of custom tags if specified in the RTE config
725 if (!empty($this->processedRteConfiguration['customTags'])) {
726 $customTags = GeneralUtility::trimExplode(',', $this->processedRteConfiguration['customTags'], true);
727 if (!empty($customTags)) {
728 $jsArray[] = 'RTEarea[editornumber].customTags= ' . json_encode($customTags) . ';';
729 }
730 }
731
732 // Setting array of content css files if specified in the RTE config
733 $versionNumberedFileNames = [];
734 $contentCssFileNames = $this->getContentCssFileNames();
735 foreach ($contentCssFileNames as $contentCssFileName) {
736 $versionNumberedFileNames[] = GeneralUtility::createVersionNumberedFilename($contentCssFileName);
737 }
738 $jsArray[] = 'RTEarea[editornumber].pageStyle = ["' . implode('","', $versionNumberedFileNames) . '"];';
739
740 $jsArray[] = 'RTEarea[editornumber].classesUrl = "' . $this->writeTemporaryFile(('classes_' . $this->language), 'js', $this->buildJSClassesArray()) . '";';
741
742 // Add Javascript configuration for registered plugins
743 foreach ($this->registeredPlugins as $pluginId => $plugin) {
744 /** @var RteHtmlAreaApi $plugin */
745 if ($this->isPluginEnabled($pluginId)) {
746 $jsPluginString = $plugin->buildJavascriptConfiguration();
747 if ($jsPluginString) {
748 $jsArray[] = $plugin->buildJavascriptConfiguration();
749 }
750 }
751 }
752
753 // Avoid premature reference to HTMLArea when being initially loaded by IRRE Ajax call
754 $jsArray[] = 'RTEarea[editornumber].toolbar = ' . $this->getJSToolbarArray() . ';';
755 $jsArray[] = 'RTEarea[editornumber].convertButtonId = ' . json_encode(array_flip($this->convertToolbarForHtmlAreaArray)) . ';';
756 $jsArray[] = 'RTEarea.initEditor(editornumber);';
757 $jsArray[] = '}';
758 $jsArray[] = '}';
759 $jsArray[] = 'configureEditorInstance[' . GeneralUtility::quoteJSvalue($this->domIdentifier) . ']();';
760
761 $this->resultArray['additionalJavaScriptPost'][] = implode(LF, $jsArray);
762 }
763
764 /**
765 * Get the name of the contentCSS files to use
766 *
767 * @return array An array of full file name of the content css files to use
768 */
769 protected function getContentCssFileNames()
770 {
771 $contentCss = is_array($this->processedRteConfiguration['contentCSS.']) ? $this->processedRteConfiguration['contentCSS.'] : [];
772 if (isset($this->processedRteConfiguration['contentCSS'])) {
773 $contentCss[] = trim($this->processedRteConfiguration['contentCSS']);
774 }
775 $contentCssFiles = [];
776 if (!empty($contentCss)) {
777 foreach ($contentCss as $contentCssKey => $contentCssfile) {
778 $fileName = trim($contentCssfile);
779 $absolutePath = GeneralUtility::getFileAbsFileName($fileName);
780 if (file_exists($absolutePath) && filesize($absolutePath)) {
781 $contentCssFiles[$contentCssKey] = PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName($fileName));
782 }
783 }
784 } else {
785 // Fallback to default content css file if none of the configured files exists and is not empty
786 $contentCssFiles['default'] = PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName('EXT:rtehtmlarea/Resources/Public/Css/ContentCss/Default.css'));
787 }
788 return array_unique($contentCssFiles);
789 }
790
791 /**
792 * Return TRUE, if the plugin can be loaded
793 *
794 * @param string $pluginId: The identification string of the plugin
795 * @return bool TRUE if the plugin can be loaded
796 */
797 protected function isPluginEnabled($pluginId)
798 {
799 return in_array($pluginId, $this->pluginEnabledArray);
800 }
801
802 /**
803 * Return JS arrays of classes configuration
804 *
805 * @return string JS classes arrays
806 */
807 protected function buildJSClassesArray()
808 {
809 $RTEProperties = $this->vanillaRteTsConfig['properties'];
810 // Declare sub-arrays
811 $classesArray = [
812 'labels' => [],
813 'values' => [],
814 'noShow' => [],
815 'alternating' => [],
816 'counting' => [],
817 'selectable' => [],
818 'requires' => [],
819 'requiredBy' => [],
820 'XOR' => []
821 ];
822 $JSClassesArray = '';
823 // Scanning the list of classes if specified in the RTE config
824 if (is_array($RTEProperties['classes.'])) {
825 foreach ($RTEProperties['classes.'] as $className => $conf) {
826 $className = rtrim($className, '.');
827
828 $label = '';
829 if (!empty($conf['name'])) {
830 $label = $this->getLanguageService()->sL(trim($conf['name']));
831 $label = str_replace('"', '\\"', str_replace('\\\'', '\'', $label));
832 }
833 $classesArray['labels'][$className] = $label;
834 $classesArray['values'][$className] = str_replace('\\\'', '\'', $conf['value']);
835 if (isset($conf['noShow'])) {
836 $classesArray['noShow'][$className] = $conf['noShow'];
837 }
838 if (is_array($conf['alternating.'])) {
839 $classesArray['alternating'][$className] = $conf['alternating.'];
840 }
841 if (is_array($conf['counting.'])) {
842 $classesArray['counting'][$className] = $conf['counting.'];
843 }
844 if (isset($conf['selectable'])) {
845 $classesArray['selectable'][$className] = $conf['selectable'];
846 }
847 if (isset($conf['requires'])) {
848 $classesArray['requires'][$className] = explode(',', GeneralUtility::rmFromList($className, $this->cleanList($conf['requires'])));
849 }
850 }
851 // Remove circularities from classes dependencies
852 $requiringClasses = array_keys($classesArray['requires']);
853 foreach ($requiringClasses as $requiringClass) {
854 if ($this->hasCircularDependency($classesArray, $requiringClass, $requiringClass)) {
855 unset($classesArray['requires'][$requiringClass]);
856 }
857 }
858 // Reverse relationship for the dependency checks when removing styles
859 $requiringClasses = array_keys($classesArray['requires']);
860 foreach ($requiringClasses as $className) {
861 foreach ($classesArray['requires'][$className] as $requiredClass) {
862 if (!is_array($classesArray['requiredBy'][$requiredClass])) {
863 $classesArray['requiredBy'][$requiredClass] = [];
864 }
865 if (!in_array($className, $classesArray['requiredBy'][$requiredClass])) {
866 $classesArray['requiredBy'][$requiredClass][] = $className;
867 }
868 }
869 }
870 }
871 // Scanning the list of sets of mutually exclusives classes if specified in the RTE config
872 if (is_array($RTEProperties['mutuallyExclusiveClasses.'])) {
873 foreach ($RTEProperties['mutuallyExclusiveClasses.'] as $listName => $conf) {
874 $classSet = GeneralUtility::trimExplode(',', $conf, true);
875 $classList = implode(',', $classSet);
876 foreach ($classSet as $className) {
877 $classesArray['XOR'][$className] = '/^(' . implode('|', GeneralUtility::trimExplode(',', GeneralUtility::rmFromList($className, $classList), true)) . ')$/';
878 }
879 }
880 }
881 foreach ($classesArray as $key => $subArray) {
882 $JSClassesArray .= 'HTMLArea.classes' . ucfirst($key) . ' = ' . $this->buildNestedJSArray($subArray) . ';' . LF;
883 }
884 return $JSClassesArray;
885 }
886
887 /**
888 * Check for possible circularity in classes dependencies
889 *
890 * @param array $classesArray: reference to the array of classes dependencies
891 * @param string $requiringClass: class requiring at some iteration level from the initial requiring class
892 * @param string $initialClass: initial class from which a circular relationship is being searched
893 * @param int $recursionLevel: depth of recursive call
894 * @return bool TRUE, if a circular relationship is found
895 */
896 protected function hasCircularDependency(&$classesArray, $requiringClass, $initialClass, $recursionLevel = 0)
897 {
898 if (is_array($classesArray['requires'][$requiringClass])) {
899 if (in_array($initialClass, $classesArray['requires'][$requiringClass])) {
900 return true;
901 } else {
902 if ($recursionLevel++ < 20) {
903 foreach ($classesArray['requires'][$requiringClass] as $requiringClass2) {
904 if ($this->hasCircularDependency($classesArray, $requiringClass2, $initialClass, $recursionLevel)) {
905 return true;
906 }
907 }
908 }
909 return false;
910 }
911 } else {
912 return false;
913 }
914 }
915
916 /**
917 * Translate Page TS Config array in JS nested array definition
918 * Replace 0 values with false
919 * Unquote regular expression values
920 * Replace empty arrays with empty objects
921 *
922 * @param array $conf: Page TSConfig configuration array
923 * @return string nested JS array definition
924 */
925 protected function buildNestedJSArray($conf)
926 {
927 $convertedConf = GeneralUtility::removeDotsFromTS($conf);
928 return str_replace(
929 [':"0"', ':"\\/^(', ')$\\/i"', ':"\\/^(', ')$\\/"', '[]'],
930 [':false', ':/^(', ')$/i', ':/^(', ')$/', '{}'], json_encode($convertedConf)
931 );
932 }
933
934 /**
935 * Writes contents in a file in typo3temp and returns the file name
936 *
937 * @param string $label: A label to insert at the beginning of the name of the file
938 * @param string $fileExtension: The file extension of the file, defaulting to 'js'
939 * @param string $contents: The contents to write into the file
940 * @return string The name of the file written to typo3temp
941 * @throws \RuntimeException If writing to file failed
942 */
943 protected function writeTemporaryFile($label, $fileExtension = 'js', $contents = '')
944 {
945 $relativeFilename = 'typo3temp/assets/js/rte_' . str_replace('-', '_', $label) . '_' . GeneralUtility::shortMD5($contents, 20) . '.' . $fileExtension;
946 $destination = PATH_site . $relativeFilename;
947 if (!file_exists($destination)) {
948 $minifiedJavaScript = '';
949 if ($fileExtension === 'js' && $contents !== '') {
950 $minifiedJavaScript = GeneralUtility::minifyJavaScript($contents);
951 }
952 $failure = GeneralUtility::writeFileToTypo3tempDir($destination, $minifiedJavaScript ? $minifiedJavaScript : $contents);
953 if ($failure) {
954 throw new \RuntimeException($failure, 1460975840);
955 }
956 }
957 if (isset($GLOBALS['TSFE'])) {
958 $fileName = $relativeFilename;
959 } else {
960 $fileName = '../' . $relativeFilename;
961 }
962 return GeneralUtility::resolveBackPath($fileName);
963 }
964
965 /**
966 * Both rte framework and rte plugins can have label files that are
967 * used in JS. The methods gathers those and creates a JS object from
968 * file labels.
969 *
970 * @return string
971 */
972 protected function createJavaScriptLanguageLabelsFromFiles()
973 {
974 $labelArray = [];
975 // Load labels of 3 base files into JS
976 foreach (['tooltips', 'msg', 'dialogs'] as $identifier) {
977 $fileName = 'EXT:rtehtmlarea/Resources/Private/Language/locallang_' . $identifier . '.xlf';
978 $newLabels = $this->getMergedLabelsFromFile($fileName);
979 if (!empty($newLabels)) {
980 $labelArray[$identifier] = $newLabels;
981 }
982 }
983 // Load labels of plugins into JS
984 foreach ($this->pluginEnabledCumulativeArray as $pluginId) {
985 /** @var RteHtmlAreaApi $plugin */
986 $plugin = $this->registeredPlugins[$pluginId];
987 $extensionKey = is_object($plugin) ? $plugin->getExtensionKey() : 'rtehtmlarea';
988 $fileName = 'EXT:' . $extensionKey . '/Resources/Private/Language/Plugins/' . $pluginId . '/locallang_js.xlf';
989 $newLabels = $this->getMergedLabelsFromFile($fileName);
990 if (!empty($newLabels)) {
991 $labelArray[$pluginId] = $newLabels;
992 }
993 }
994 $this->resultArray['additionalJavaScriptPost'][] = 'HTMLArea.I18N = ' . json_encode($labelArray);
995 }
996
997 /**
998 * Get all labels from a specific label file, merge default
999 * labels and target language labels.
1000 *
1001 * @param string $fileName The file to merge labels from
1002 * @return array Label keys and values
1003 */
1004 protected function getMergedLabelsFromFile($fileName)
1005 {
1006 /** @var $languageFactory LocalizationFactory */
1007 $languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
1008 $localizationArray = $languageFactory->getParsedData($fileName, $this->language, 'utf-8', 1);
1009 if (is_array($localizationArray) && !empty($localizationArray)) {
1010 if (!empty($localizationArray[$this->language])) {
1011 $finalLocalLang = $localizationArray['default'];
1012 ArrayUtility::mergeRecursiveWithOverrule($finalLocalLang, $localizationArray[$this->language], true, false);
1013 $localizationArray[$this->language] = $finalLocalLang;
1014 } else {
1015 $localizationArray[$this->language] = $localizationArray['default'];
1016 }
1017 } else {
1018 $localizationArray = [];
1019 }
1020 return $localizationArray[$this->language];
1021 }
1022
1023 /**
1024 * Return the JS code of the toolbar configuration for the HTMLArea editor
1025 *
1026 * @return string The JS code as nested JS arrays
1027 */
1028 protected function getJSToolbarArray()
1029 {
1030 // The toolbar array
1031 $toolbar = [];
1032 // The current row; a "linebreak" ends the current row
1033 $row = [];
1034 // The current group; each group is between "bar"s; a "linebreak" ends the current group
1035 $group = [];
1036 // Process each toolbar item in the toolbar order list
1037 foreach ($this->toolbarOrderArray as $item) {
1038 switch ($item) {
1039 case 'linebreak':
1040 // Add row to toolbar if not empty
1041 if (!empty($group)) {
1042 $row[] = $group;
1043 $group = [];
1044 }
1045 if (!empty($row)) {
1046 $toolbar[] = $row;
1047 $row = [];
1048 }
1049 break;
1050 case 'bar':
1051 // Add group to row if not empty
1052 if (!empty($group)) {
1053 $row[] = $group;
1054 $group = [];
1055 }
1056 break;
1057 case 'space':
1058 if (end($group) != $this->convertToolbarForHtmlAreaArray[$item]) {
1059 $group[] = $this->convertToolbarForHtmlAreaArray[$item];
1060 }
1061 break;
1062 default:
1063 if (in_array($item, $this->toolbar)) {
1064 // Add the item to the group
1065 $convertedItem = $this->convertToolbarForHtmlAreaArray[$item];
1066 if ($convertedItem) {
1067 $group[] = $convertedItem;
1068 }
1069 }
1070 }
1071 }
1072 // Add the last group and last line, if not empty
1073 if (!empty($group)) {
1074 $row[] = $group;
1075 }
1076 if (!empty($row)) {
1077 $toolbar[] = $row;
1078 }
1079 return json_encode($toolbar);
1080 }
1081
1082 /**
1083 * Return the Javascript code for copying the HTML code from the editor into the hidden input field.
1084 *
1085 * @return void
1086 */
1087 protected function addOnSubmitJavaScriptCode()
1088 {
1089 $onSubmitCode = [];
1090 $onSubmitCode[] = 'if (RTEarea[' . GeneralUtility::quoteJSvalue($this->domIdentifier) . ']) {';
1091 $onSubmitCode[] = 'document.editform[' . GeneralUtility::quoteJSvalue($this->data['parameterArray']['itemFormElName']) . '].value = RTEarea[' . GeneralUtility::quoteJSvalue($this->domIdentifier) . '].editor.getHTML();';
1092 $onSubmitCode[] = '} else {';
1093 $onSubmitCode[] = 'OK = 0;';
1094 $onSubmitCode[] = '};';
1095 $this->resultArray['additionalJavaScriptSubmit'][] = implode(LF, $onSubmitCode);
1096 }
1097
1098 /**
1099 * Client Browser Information
1100 *
1101 * @return array Contains keys "user agent", "browser", "version", "system"
1102 */
1103 protected function clientInfo()
1104 {
1105 $userAgent = GeneralUtility::getIndpEnv('HTTP_USER_AGENT');
1106 $browserInfo = ClientUtility::getBrowserInfo($userAgent);
1107 // Known engines: order is not irrelevant!
1108 $knownEngines = ['opera', 'msie', 'gecko', 'webkit'];
1109 if (is_array($browserInfo['all'])) {
1110 foreach ($knownEngines as $engine) {
1111 if ($browserInfo['all'][$engine]) {
1112 $browserInfo['browser'] = $engine;
1113 $browserInfo['version'] = ClientUtility::getVersion($browserInfo['all'][$engine]);
1114 break;
1115 }
1116 }
1117 }
1118 return $browserInfo;
1119 }
1120
1121 /**
1122 * Initialize a couple of language related local properties
1123 *
1124 * @return void
1125 */
1126 public function initializeLanguageRelatedProperties()
1127 {
1128 $this->language = $GLOBALS['LANG']->lang;
1129 if ($this->language === 'default' || !$this->language) {
1130 $this->language = 'en';
1131 }
1132 $currentLanguageUid = $this->data['databaseRow']['sys_language_uid'];
1133 if (is_array($currentLanguageUid)) {
1134 $currentLanguageUid = $currentLanguageUid[0];
1135 }
1136 $this->contentLanguageUid = (int)max($currentLanguageUid, 0);
1137 if ($this->contentLanguageUid) {
1138 $this->contentISOLanguage = $this->language;
1139 if (ExtensionManagementUtility::isLoaded('static_info_tables')) {
1140 $tableA = 'sys_language';
1141 $tableB = 'static_languages';
1142
1143 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1144 ->getQueryBuilderForTable($tableA);
1145
1146 $result = $queryBuilder
1147 ->select('a.uid', 'b.lg_iso_2', 'b.lg_country_iso_2')
1148 ->from($tableA, 'a')
1149 ->where('a.uid', (int)$this->contentLanguageUid)
1150 ->leftJoin(
1151 'a',
1152 $tableB,
1153 'b',
1154 $queryBuilder->expr()->eq('a.static_lang_isocode', $queryBuilder->quoteIdentifier('b.uid'))
1155 )
1156 ->execute();
1157
1158 while ($languageRow = $result->fetch()) {
1159 $this->contentISOLanguage = strtolower(trim($languageRow['lg_iso_2']) . (trim($languageRow['lg_country_iso_2']) ? '_' . trim($languageRow['lg_country_iso_2']) : ''));
1160 }
1161 }
1162 } else {
1163 $this->contentISOLanguage = trim($this->processedRteConfiguration['defaultContentLanguage']) ?: 'en';
1164 $languageCodeParts = explode('_', $this->contentISOLanguage);
1165 $this->contentISOLanguage = strtolower($languageCodeParts[0]) . ($languageCodeParts[1] ? '_' . strtoupper($languageCodeParts[1]) : '');
1166 // Find the configured language in the list of localization locales
1167 /** @var $locales Locales */
1168 $locales = GeneralUtility::makeInstance(Locales::class);
1169 // If not found, default to 'en'
1170 if (!in_array($this->contentISOLanguage, $locales->getLocales())) {
1171 $this->contentISOLanguage = 'en';
1172 }
1173 }
1174 $this->contentTypo3Language = $this->contentISOLanguage === 'en' ? 'default' : $this->contentISOLanguage;
1175 }
1176
1177 /**
1178 * Log usage of deprecated Page TS Config Property
1179 *
1180 * @param string $deprecatedProperty: Name of deprecated property
1181 * @param string $useProperty: Name of property to use instead
1182 * @param string $version: Version of TYPO3 in which the property will be removed
1183 * @return void
1184 */
1185 protected function logDeprecatedProperty($deprecatedProperty, $useProperty, $version)
1186 {
1187 $backendUser = $this->getBackendUserAuthentication();
1188 if (!$this->processedRteConfiguration['logDeprecatedProperties.']['disabled']) {
1189 $message = sprintf(
1190 'RTE Page TSConfig property "%1$s" used on page id #%4$s is DEPRECATED and will be removed in TYPO3 %3$s. Use "%2$s" instead.',
1191 $deprecatedProperty,
1192 $useProperty,
1193 $version,
1194 $this->data['databaseRow']['pid']
1195 );
1196 GeneralUtility::deprecationLog($message);
1197 if ($this->processedRteConfiguration['logDeprecatedProperties.']['logAlsoToBELog']) {
1198 $message = sprintf(
1199 $this->getLanguageService()->sL('LLL:EXT:rtehtmlarea/Resources/Private/Language/locallang.xlf:deprecatedPropertyMessage'),
1200 $deprecatedProperty,
1201 $useProperty,
1202 $version,
1203 $this->data['databaseRow']['pid']
1204 );
1205 $backendUser->simplelog($message, 'rtehtmlarea');
1206 }
1207 }
1208 }
1209
1210 /**
1211 * A list of parameters that is mostly given as GET/POST to other RTE controllers.
1212 *
1213 * @return string
1214 */
1215 protected function RTEtsConfigParams()
1216 {
1217 $parameters = BackendUtility::getSpecConfParametersFromArray($this->defaultExtras['rte_transform']['parameters']);
1218 $result = [
1219 $this->data['tableName'],
1220 $this->data['databaseRow']['uid'],
1221 $this->data['fieldName'],
1222 $this->pidOfVersionedMotherRecord,
1223 $this->data['recordTypeValue'],
1224 $this->pidOfPageRecord,
1225 $parameters['imgpath'],
1226 ];
1227 return implode(':', $result);
1228 }
1229
1230 /**
1231 * Clean list
1232 *
1233 * @param string $str String to clean
1234 * @return string Cleaned string
1235 */
1236 protected function cleanList($str)
1237 {
1238 if (strstr($str, '*')) {
1239 $str = '*';
1240 } else {
1241 $str = implode(',', array_unique(GeneralUtility::trimExplode(',', $str, true)));
1242 }
1243 return $str;
1244 }
1245
1246 /**
1247 * Performs transformation of content from database to richtext editor
1248 *
1249 * @param string $value Value to transform.
1250 * @return string Transformed content
1251 */
1252 protected function transformDatabaseContentToEditor($value)
1253 {
1254 // change <strong> to <b>
1255 $value = preg_replace('/<(\\/?)strong/i', '<$1b', $value);
1256 // change <em> to <i>
1257 $value = preg_replace('/<(\\/?)em([^b>]*>)/i', '<$1i$2', $value);
1258
1259 if ($this->defaultExtras['rte_transform']) {
1260 /** @var RteHtmlParser $parseHTML */
1261 $parseHTML = GeneralUtility::makeInstance(RteHtmlParser::class);
1262 $parseHTML->init($this->data['table'] . ':' . $this->data['fieldName'], $this->pidOfVersionedMotherRecord);
1263 $value = $parseHTML->RTE_transform($value, $this->defaultExtras, 'rte', $this->processedRteConfiguration);
1264 }
1265 return $value;
1266 }
1267
1268 /**
1269 * True if RTE is in full screen mode / called via wizard controller
1270 *
1271 * @return bool
1272 */
1273 protected function isInFullScreenMode()
1274 {
1275 return GeneralUtility::_GP('M') === 'wizard_rte';
1276 }
1277
1278 /**
1279 * @return LanguageService
1280 */
1281 protected function getLanguageService()
1282 {
1283 return $GLOBALS['LANG'];
1284 }
1285
1286 /**
1287 * @return BackendUserAuthentication
1288 */
1289 protected function getBackendUserAuthentication()
1290 {
1291 return $GLOBALS['BE_USER'];
1292 }
1293 }