5bf24629e916f452b34adbe8564f59fe75f77b77
[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\FrontendEditing\FrontendEditingController;
23 use TYPO3\CMS\Core\Html\RteHtmlParser;
24 use TYPO3\CMS\Core\Localization\Locales;
25 use TYPO3\CMS\Core\Localization\LocalizationFactory;
26 use TYPO3\CMS\Core\Utility\ArrayUtility;
27 use TYPO3\CMS\Core\Utility\ClientUtility;
28 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
29 use TYPO3\CMS\Core\Utility\GeneralUtility;
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 = array(
152 'space' => 'space',
153 'bar' => 'separator',
154 'linebreak' => 'linebreak'
155 );
156
157 /**
158 * Final toolbar array
159 *
160 * @var array
161 */
162 protected $toolbar = array();
163
164 /**
165 * Save the buttons for the toolbar
166 *
167 * @var array
168 */
169 protected $toolbarOrderArray = array();
170
171 /**
172 * Plugin buttons
173 *
174 * @var array
175 */
176 protected $pluginButton = array();
177
178 /**
179 * Plugin labels
180 *
181 * @var array
182 */
183 protected $pluginLabel = array();
184
185 /**
186 * Array of plugin id's enabled in the current RTE editing area
187 *
188 * @var array
189 */
190 protected $pluginEnabledArray = array();
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 = array();
198
199 /**
200 * Array of registered plugins indexed by their plugin Id's
201 *
202 * @var array
203 */
204 protected $registeredPlugins = array();
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 = $this->getFullFileName($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 array($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 = array();
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 = array(
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 = array();
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 if ($this->client['browser'] === 'msie' || $this->client['browser'] === 'opera') {
416 $this->processedRteConfiguration['keepButtonGroupTogether'] = 0;
417 }
418 $this->defaultToolbarOrder = 'bar, blockstylelabel, blockstyle, textstylelabel, textstyle, linebreak,
419 bar, formattext, bold, strong, italic, emphasis, big, small, insertedtext, deletedtext, citation, code,'
420 . 'definition, keyboard, monospaced, quotation, sample, variable, bidioverride, strikethrough, subscript, superscript, underline, span,
421 bar, fontstyle, fontsize, bar, formatblock, insertparagraphbefore, insertparagraphafter, blockquote, line,
422 bar, left, center, right, justifyfull,
423 bar, orderedlist, unorderedlist, definitionlist, definitionitem, outdent, indent,
424 bar, language, showlanguagemarks,lefttoright, righttoleft,
425 bar, textcolor, bgcolor, textindicator,
426 bar, editelement, showmicrodata,
427 bar, image, emoticon, insertcharacter, insertsofthyphen, abbreviation, user,
428 bar, link, unlink,
429 bar, table,'
430 . ($this->processedRteConfiguration['hideTableOperationsInToolbar']
431 && is_array($this->processedRteConfiguration['buttons.'])
432 && is_array($this->processedRteConfiguration['buttons.']['toggleborders.'])
433 && $this->processedRteConfiguration['buttons.']['toggleborders.']['keepInToolbar'] ? ' toggleborders,' : '')
434 . 'bar, findreplace, spellcheck,
435 bar, chMode, inserttag, removeformat, bar, copy, cut, paste, pastetoggle, pastebehaviour, bar, undo, redo, bar, about, linebreak,'
436 . ($this->processedRteConfiguration['hideTableOperationsInToolbar'] ? '' : 'bar, toggleborders,')
437 . ' bar, tableproperties, tablerestyle, bar, rowproperties, rowinsertabove, rowinsertunder, rowdelete, rowsplit, bar,
438 columnproperties, columninsertbefore, columninsertafter, columndelete, columnsplit, bar,
439 cellproperties, cellinsertbefore, cellinsertafter, celldelete, cellsplit, cellmerge';
440
441 // Additional buttons from registered plugins
442 foreach ($this->registeredPlugins as $pluginId => $plugin) {
443 /** @var RteHtmlAreaApi $plugin */
444 if ($this->isPluginEnabled($pluginId)) {
445 $pluginButtons = $plugin->getPluginButtons();
446 //Add only buttons not yet in the default toolbar order
447 $addButtons = implode(
448 ',',
449 array_diff(
450 GeneralUtility::trimExplode(',', $pluginButtons, true),
451 GeneralUtility::trimExplode(',', $this->defaultToolbarOrder, true)
452 )
453 );
454 $this->defaultToolbarOrder = ($addButtons ? 'bar,' . $addButtons . ',linebreak,' : '') . $this->defaultToolbarOrder;
455 }
456 }
457 $toolbarOrder = $this->processedRteConfiguration['toolbarOrder'] ?: $this->defaultToolbarOrder;
458 // Getting rid of undefined buttons
459 $this->toolbarOrderArray = array_intersect(GeneralUtility::trimExplode(',', $toolbarOrder, true), GeneralUtility::trimExplode(',', $this->defaultToolbarOrder, true));
460 $toolbarOrder = array_unique(array_values($this->toolbarOrderArray));
461 // Fetching specConf for field from backend
462 $pList = is_array($this->defaultExtras['richtext']['parameters']) ? implode(',', $this->defaultExtras['richtext']['parameters']) : '';
463 if ($pList !== '*') {
464 // If not all
465 $show = is_array($this->defaultExtras['richtext']['parameters']) ? $this->defaultExtras['richtext']['parameters'] : array();
466 if ($this->processedRteConfiguration['showButtons']) {
467 if (!GeneralUtility::inList($this->processedRteConfiguration['showButtons'], '*')) {
468 $show = array_unique(array_merge($show, GeneralUtility::trimExplode(',', $this->processedRteConfiguration['showButtons'], true)));
469 } else {
470 $show = array_unique(array_merge($show, $toolbarOrder));
471 }
472 }
473 if (is_array($this->processedRteConfiguration['showButtons.'])) {
474 foreach ($this->processedRteConfiguration['showButtons.'] as $buttonId => $value) {
475 if ($value) {
476 $show[] = $buttonId;
477 }
478 }
479 $show = array_unique($show);
480 }
481 } else {
482 $show = $toolbarOrder;
483 }
484 $RTEkeyList = isset($backendUser->userTS['options.']['RTEkeyList']) ? $backendUser->userTS['options.']['RTEkeyList'] : '*';
485 if ($RTEkeyList !== '*') {
486 // If not all
487 $show = array_intersect($show, GeneralUtility::trimExplode(',', $RTEkeyList, true));
488 }
489 // Hiding buttons of disabled plugins
490 $hideButtons = array('space', 'bar', 'linebreak');
491 foreach ($this->pluginButton as $pluginId => $buttonList) {
492 if (!$this->isPluginEnabled($pluginId)) {
493 $buttonArray = GeneralUtility::trimExplode(',', $buttonList, true);
494 foreach ($buttonArray as $button) {
495 $hideButtons[] = $button;
496 }
497 }
498 }
499 // Hiding labels of disabled plugins
500 foreach ($this->pluginLabel as $pluginId => $label) {
501 if (!$this->isPluginEnabled($pluginId)) {
502 $hideButtons[] = $label;
503 }
504 }
505 // Hiding buttons
506 $show = array_diff($show, GeneralUtility::trimExplode(',', $this->processedRteConfiguration['hideButtons'], true));
507 // Apply toolbar constraints from registered plugins
508 foreach ($this->registeredPlugins as $pluginId => $plugin) {
509 if ($this->isPluginEnabled($pluginId) && method_exists($plugin, 'applyToolbarConstraints')) {
510 $show = $plugin->applyToolbarConstraints($show);
511 }
512 }
513 // Getting rid of the buttons for which we have no position
514 $show = array_intersect($show, $toolbarOrder);
515 foreach ($this->registeredPlugins as $pluginId => $plugin) {
516 /** @var RteHtmlAreaApi $plugin */
517 $plugin->setToolbar($show);
518 }
519 $this->toolbar = $show;
520 }
521
522 /**
523 * Disable some plugins
524 *
525 * @return void
526 */
527 protected function setPlugins()
528 {
529 // Disabling a plugin that adds buttons if none of its buttons is in the toolbar
530 $hidePlugins = array();
531 foreach ($this->pluginButton as $pluginId => $buttonList) {
532 /** @var RteHtmlAreaApi $plugin */
533 $plugin = $this->registeredPlugins[$pluginId];
534 if ($plugin->addsButtons()) {
535 $showPlugin = false;
536 $buttonArray = GeneralUtility::trimExplode(',', $buttonList, true);
537 foreach ($buttonArray as $button) {
538 if (in_array($button, $this->toolbar)) {
539 $showPlugin = true;
540 }
541 }
542 if (!$showPlugin) {
543 $hidePlugins[] = $pluginId;
544 }
545 }
546 }
547 $this->pluginEnabledArray = array_diff($this->pluginEnabledArray, $hidePlugins);
548 // Hiding labels of disabled plugins
549 $hideLabels = array();
550 foreach ($this->pluginLabel as $pluginId => $label) {
551 if (!$this->isPluginEnabled($pluginId)) {
552 $hideLabels[] = $label;
553 }
554 }
555 $this->toolbar = array_diff($this->toolbar, $hideLabels);
556 // Adding plugins declared as prerequisites by enabled plugins
557 $requiredPlugins = array();
558 foreach ($this->registeredPlugins as $pluginId => $plugin) {
559 /** @var RteHtmlAreaApi $plugin */
560 if ($this->isPluginEnabled($pluginId)) {
561 $requiredPlugins = array_merge($requiredPlugins, GeneralUtility::trimExplode(',', $plugin->getRequiredPlugins(), true));
562 }
563 }
564 $requiredPlugins = array_unique($requiredPlugins);
565 foreach ($requiredPlugins as $pluginId) {
566 if (is_object($this->registeredPlugins[$pluginId]) && !$this->isPluginEnabled($pluginId)) {
567 $this->pluginEnabledArray[] = $pluginId;
568 }
569 }
570 $this->pluginEnabledArray = array_unique($this->pluginEnabledArray);
571 // Completing the toolbar conversion array for htmlArea
572 foreach ($this->registeredPlugins as $pluginId => $plugin) {
573 /** @var RteHtmlAreaApi $plugin */
574 if ($this->isPluginEnabled($pluginId)) {
575 $this->convertToolbarForHtmlAreaArray = array_unique(array_merge($this->convertToolbarForHtmlAreaArray, $plugin->getConvertToolbarForHtmlAreaArray()));
576 }
577 }
578 }
579
580 /**
581 * Add RTE main scripts and plugin scripts
582 *
583 * @return void
584 */
585 protected function loadRequireModulesForRTE()
586 {
587 $this->resultArray['requireJsModules'] = array();
588 $this->resultArray['requireJsModules'][] = 'TYPO3/CMS/Rtehtmlarea/HTMLArea/HTMLArea';
589 foreach ($this->pluginEnabledCumulativeArray as $pluginId) {
590 /** @var RteHtmlAreaApi $plugin */
591 $plugin = $this->registeredPlugins[$pluginId];
592 $extensionKey = is_object($plugin) ? $plugin->getExtensionKey() : 'rtehtmlarea';
593 $requirePath = 'TYPO3/CMS/' . GeneralUtility::underscoredToUpperCamelCase($extensionKey);
594 $this->resultArray['requireJsModules'][] = $requirePath . '/Plugins/' . $pluginId;
595 }
596 }
597
598 /**
599 * Return RTE initialization inline JavaScript code
600 *
601 * @return string RTE initialization inline JavaScript code
602 */
603 protected function getRteInitJsCode()
604 {
605 $skinFilename = trim($this->processedRteConfiguration['skin']) ?: 'EXT:rtehtmlarea/Resources/Public/Css/Skin/htmlarea.css';
606 $skinFilename = $this->getFullFileName($skinFilename);
607 $skinDirectory = dirname($skinFilename);
608 // Editing area style sheet
609 $editedContentCSS = GeneralUtility::createVersionNumberedFilename($skinDirectory . '/htmlarea-edited-content.css');
610
611 return 'require(["TYPO3/CMS/Rtehtmlarea/HTMLArea/HTMLArea", "jquery"], function (HTMLArea, $) {
612 if (typeof RTEarea === "undefined") {
613 RTEarea = new Object();
614 RTEarea[0] = new Object();
615 RTEarea[0].version = "' . TYPO3_version . '";
616 RTEarea[0].editorUrl = "' . ExtensionManagementUtility::extRelPath('rtehtmlarea') . '";
617 RTEarea[0].editorSkin = "' . $skinDirectory . '/";
618 RTEarea[0].editedContentCSS = "' . $editedContentCSS . '";
619 RTEarea.init = function() {
620 HTMLArea.init();
621 };
622 RTEarea.initEditor = function(editorNumber) {
623 if (typeof HTMLArea === "undefined" || !HTMLArea.isReady) {
624 window.setTimeout(function () {
625 RTEarea.initEditor(editorNumber);
626 }, 40);
627 } else {
628 HTMLArea.initEditor(editorNumber);
629 }
630 };
631 }
632 RTEarea.init();
633 });';
634 }
635
636 /**
637 * Return the Javascript code for configuring the RTE
638 *
639 * @return void
640 */
641 protected function addInstanceJavaScriptRegistration()
642 {
643 $backendUser = $this->getBackendUserAuthentication();
644
645 $jsArray = array();
646 $jsArray[] = 'if (typeof configureEditorInstance === "undefined") {';
647 $jsArray[] = ' configureEditorInstance = new Object();';
648 $jsArray[] = '}';
649 $jsArray[] = 'configureEditorInstance[' . GeneralUtility::quoteJSvalue($this->domIdentifier) . '] = function() {';
650 $jsArray[] = 'if (typeof RTEarea === "undefined" || typeof HTMLArea === "undefined") {';
651 $jsArray[] = ' window.setTimeout("configureEditorInstance[' . GeneralUtility::quoteJSvalue($this->domIdentifier) . ']();", 40);';
652 $jsArray[] = '} else {';
653 $jsArray[] = 'editornumber = ' . GeneralUtility::quoteJSvalue($this->domIdentifier) . ';';
654 $jsArray[] = 'RTEarea[editornumber] = new Object();';
655 $jsArray[] = 'RTEarea[editornumber].RTEtsConfigParams = "&RTEtsConfigParams=' . rawurlencode($this->RTEtsConfigParams()) . '";';
656 $jsArray[] = 'RTEarea[editornumber].number = editornumber;';
657 $jsArray[] = 'RTEarea[editornumber].deleted = false;';
658 $jsArray[] = 'RTEarea[editornumber].textAreaId = ' . GeneralUtility::quoteJSvalue($this->domIdentifier) . ';';
659 $jsArray[] = 'RTEarea[editornumber].id = "RTEarea" + editornumber;';
660 $jsArray[] = 'RTEarea[editornumber].RTEWidthOverride = "'
661 . (isset($backendUser->uc['rteWidth']) && trim($backendUser->uc['rteWidth'])
662 ? trim($backendUser->uc['rteWidth'])
663 : trim($this->processedRteConfiguration['RTEWidthOverride'])) . '";';
664 $jsArray[] = 'RTEarea[editornumber].RTEHeightOverride = "'
665 . (isset($backendUser->uc['rteHeight']) && (int)$backendUser->uc['rteHeight']
666 ? (int)$backendUser->uc['rteHeight']
667 : (int)$this->processedRteConfiguration['RTEHeightOverride']) . '";';
668 $jsArray[] = 'RTEarea[editornumber].resizable = '
669 . (isset($backendUser->uc['rteResize']) && $backendUser->uc['rteResize']
670 ? 'true;'
671 : (trim($this->processedRteConfiguration['rteResize']) ? 'true;' : 'false;'));
672 $jsArray[] = 'RTEarea[editornumber].maxHeight = "'
673 . (isset($backendUser->uc['rteMaxHeight']) && (int)$backendUser->uc['rteMaxHeight']
674 ? trim($backendUser->uc['rteMaxHeight'])
675 : ((int)$this->processedRteConfiguration['rteMaxHeight'] ?: '2000')) . '";';
676 $jsArray[] = 'RTEarea[editornumber].fullScreen = ' . ($this->isInFullScreenMode() ? 'true;' : 'false;');
677 $jsArray[] = 'RTEarea[editornumber].showStatusBar = ' . (trim($this->processedRteConfiguration['showStatusBar']) ? 'true;' : 'false;');
678 $jsArray[] = 'RTEarea[editornumber].enableWordClean = ' . (trim($this->processedRteConfiguration['enableWordClean']) ? 'true;' : 'false;');
679 $jsArray[] = 'RTEarea[editornumber].htmlRemoveComments = ' . (trim($this->processedRteConfiguration['removeComments']) ? 'true;' : 'false;');
680 $jsArray[] = 'RTEarea[editornumber].disableEnterParagraphs = ' . (trim($this->processedRteConfiguration['disableEnterParagraphs']) ? 'true;' : 'false;');
681 $jsArray[] = 'RTEarea[editornumber].disableObjectResizing = ' . (trim($this->processedRteConfiguration['disableObjectResizing']) ? 'true;' : 'false;');
682 $jsArray[] = 'RTEarea[editornumber].removeTrailingBR = ' . (trim($this->processedRteConfiguration['removeTrailingBR']) ? 'true;' : 'false;');
683 $jsArray[] = 'RTEarea[editornumber].useCSS = ' . (trim($this->processedRteConfiguration['useCSS']) ? 'true' : 'false') . ';';
684 $jsArray[] = 'RTEarea[editornumber].keepButtonGroupTogether = ' . (trim($this->processedRteConfiguration['keepButtonGroupTogether']) ? 'true;' : 'false;');
685 $jsArray[] = 'RTEarea[editornumber].disablePCexamples = ' . (trim($this->processedRteConfiguration['disablePCexamples']) ? 'true;' : 'false;');
686 $jsArray[] = 'RTEarea[editornumber].showTagFreeClasses = ' . (trim($this->processedRteConfiguration['showTagFreeClasses']) ? 'true;' : 'false;');
687 $jsArray[] = 'RTEarea[editornumber].tceformsNested = ' . (!empty($this->data) ? json_encode($this->data['tabAndInlineStack']) : '[]') . ';';
688 $jsArray[] = 'RTEarea[editornumber].dialogueWindows = new Object();';
689 if (isset($this->processedRteConfiguration['dialogueWindows.']['defaultPositionFromTop'])) {
690 $jsArray[] = 'RTEarea[editornumber].dialogueWindows.positionFromTop = ' . (int)$this->processedRteConfiguration['dialogueWindows.']['defaultPositionFromTop'] . ';';
691 }
692 if (isset($this->processedRteConfiguration['dialogueWindows.']['defaultPositionFromLeft'])) {
693 $jsArray[] = 'RTEarea[editornumber].dialogueWindows.positionFromLeft = ' . (int)$this->processedRteConfiguration['dialogueWindows.']['defaultPositionFromLeft'] . ';';
694 }
695 $jsArray[] = 'RTEarea[editornumber].sys_language_content = "' . $this->contentLanguageUid . '";';
696 $jsArray[] = 'RTEarea[editornumber].typo3ContentLanguage = "' . $this->contentTypo3Language . '";';
697 $jsArray[] = 'RTEarea[editornumber].userUid = "' . 'BE_' . $backendUser->user['uid'] . '";';
698
699 // Setting the plugin flags
700 $jsArray[] = 'RTEarea[editornumber].plugin = new Object();';
701 foreach ($this->pluginEnabledArray as $pluginId) {
702 $jsArray[] = 'RTEarea[editornumber].plugin.' . $pluginId . ' = true;';
703 }
704
705 // Setting the buttons configuration
706 $jsArray[] = 'RTEarea[editornumber].buttons = new Object();';
707 if (is_array($this->processedRteConfiguration['buttons.'])) {
708 foreach ($this->processedRteConfiguration['buttons.'] as $buttonIndex => $conf) {
709 $button = substr($buttonIndex, 0, -1);
710 if (is_array($conf)) {
711 $jsArray[] = 'RTEarea[editornumber].buttons.' . $button . ' = ' . $this->buildNestedJSArray($conf) . ';';
712 }
713 }
714 }
715
716 // Setting the list of tags to be removed if specified in the RTE config
717 if (trim($this->processedRteConfiguration['removeTags'])) {
718 $jsArray[] = 'RTEarea[editornumber].htmlRemoveTags = /^(' . implode('|', GeneralUtility::trimExplode(',', $this->processedRteConfiguration['removeTags'], true)) . ')$/i;';
719 }
720
721 // Setting the list of tags to be removed with their contents if specified in the RTE config
722 if (trim($this->processedRteConfiguration['removeTagsAndContents'])) {
723 $jsArray[] = 'RTEarea[editornumber].htmlRemoveTagsAndContents = /^(' . implode('|', GeneralUtility::trimExplode(',', $this->processedRteConfiguration['removeTagsAndContents'], true)) . ')$/i;';
724 }
725
726 // Setting array of custom tags if specified in the RTE config
727 if (!empty($this->processedRteConfiguration['customTags'])) {
728 $customTags = GeneralUtility::trimExplode(',', $this->processedRteConfiguration['customTags'], true);
729 if (!empty($customTags)) {
730 $jsArray[] = 'RTEarea[editornumber].customTags= ' . json_encode($customTags) . ';';
731 }
732 }
733
734 // Setting array of content css files if specified in the RTE config
735 $versionNumberedFileNames = array();
736 $contentCssFileNames = $this->getContentCssFileNames();
737 foreach ($contentCssFileNames as $contentCssFileName) {
738 $versionNumberedFileNames[] = GeneralUtility::createVersionNumberedFilename($contentCssFileName);
739 }
740 $jsArray[] = 'RTEarea[editornumber].pageStyle = ["' . implode('","', $versionNumberedFileNames) . '"];';
741
742 $jsArray[] = 'RTEarea[editornumber].classesUrl = "' . $this->writeTemporaryFile(('classes_' . $this->language), 'js', $this->buildJSClassesArray()) . '";';
743
744 // Add Javascript configuration for registered plugins
745 foreach ($this->registeredPlugins as $pluginId => $plugin) {
746 /** @var RteHtmlAreaApi $plugin */
747 if ($this->isPluginEnabled($pluginId)) {
748 $jsPluginString = $plugin->buildJavascriptConfiguration();
749 if ($jsPluginString) {
750 $jsArray[] = $plugin->buildJavascriptConfiguration();
751 }
752 }
753 }
754
755 // Avoid premature reference to HTMLArea when being initially loaded by IRRE Ajax call
756 $jsArray[] = 'RTEarea[editornumber].toolbar = ' . $this->getJSToolbarArray() . ';';
757 $jsArray[] = 'RTEarea[editornumber].convertButtonId = ' . json_encode(array_flip($this->convertToolbarForHtmlAreaArray)) . ';';
758 $jsArray[] = 'RTEarea.initEditor(editornumber);';
759 $jsArray[] = '}';
760 $jsArray[] = '}';
761 $jsArray[] = 'configureEditorInstance[' . GeneralUtility::quoteJSvalue($this->domIdentifier) . ']();';
762
763 $this->resultArray['additionalJavaScriptPost'][] = implode(LF, $jsArray);
764 }
765
766 /**
767 * Get the name of the contentCSS files to use
768 *
769 * @return array An array of full file name of the content css files to use
770 */
771 protected function getContentCssFileNames()
772 {
773 $contentCss = is_array($this->processedRteConfiguration['contentCSS.']) ? $this->processedRteConfiguration['contentCSS.'] : array();
774 if (isset($this->processedRteConfiguration['contentCSS'])) {
775 $contentCss[] = trim($this->processedRteConfiguration['contentCSS']);
776 }
777 $contentCssFiles = array();
778 if (!empty($contentCss)) {
779 foreach ($contentCss as $contentCssKey => $contentCssfile) {
780 $fileName = trim($contentCssfile);
781 $absolutePath = GeneralUtility::getFileAbsFileName($fileName);
782 if (file_exists($absolutePath) && filesize($absolutePath)) {
783 $contentCssFiles[$contentCssKey] = $this->getFullFileName($fileName);
784 }
785 }
786 } else {
787 // Fallback to default content css file if none of the configured files exists and is not empty
788 $contentCssFiles['default'] = $this->getFullFileName('EXT:rtehtmlarea/Resources/Public/Css/ContentCss/Default.css');
789 }
790 return array_unique($contentCssFiles);
791 }
792
793 /**
794 * Return TRUE, if the plugin can be loaded
795 *
796 * @param string $pluginId: The identification string of the plugin
797 * @return bool TRUE if the plugin can be loaded
798 */
799 protected function isPluginEnabled($pluginId)
800 {
801 return in_array($pluginId, $this->pluginEnabledArray);
802 }
803
804 /**
805 * Return JS arrays of classes configuration
806 *
807 * @return string JS classes arrays
808 */
809 protected function buildJSClassesArray()
810 {
811 $RTEProperties = $this->vanillaRteTsConfig['properties'];
812 // Declare sub-arrays
813 $classesArray = array(
814 'labels' => array(),
815 'values' => array(),
816 'noShow' => array(),
817 'alternating' => array(),
818 'counting' => array(),
819 'selectable' => array(),
820 'requires' => array(),
821 'requiredBy' => array(),
822 'XOR' => array()
823 );
824 $JSClassesArray = '';
825 // Scanning the list of classes if specified in the RTE config
826 if (is_array($RTEProperties['classes.'])) {
827 foreach ($RTEProperties['classes.'] as $className => $conf) {
828 $className = rtrim($className, '.');
829
830 $label = '';
831 if (!empty($conf['name'])) {
832 $label = $this->getLanguageService()->sL(trim($conf['name']));
833 $label = str_replace('"', '\\"', str_replace('\\\'', '\'', $label));
834 }
835 $classesArray['labels'][$className] = $label;
836 $classesArray['values'][$className] = str_replace('\\\'', '\'', $conf['value']);
837 if (isset($conf['noShow'])) {
838 $classesArray['noShow'][$className] = $conf['noShow'];
839 }
840 if (is_array($conf['alternating.'])) {
841 $classesArray['alternating'][$className] = $conf['alternating.'];
842 }
843 if (is_array($conf['counting.'])) {
844 $classesArray['counting'][$className] = $conf['counting.'];
845 }
846 if (isset($conf['selectable'])) {
847 $classesArray['selectable'][$className] = $conf['selectable'];
848 }
849 if (isset($conf['requires'])) {
850 $classesArray['requires'][$className] = explode(',', GeneralUtility::rmFromList($className, $this->cleanList($conf['requires'])));
851 }
852 }
853 // Remove circularities from classes dependencies
854 $requiringClasses = array_keys($classesArray['requires']);
855 foreach ($requiringClasses as $requiringClass) {
856 if ($this->hasCircularDependency($classesArray, $requiringClass, $requiringClass)) {
857 unset($classesArray['requires'][$requiringClass]);
858 }
859 }
860 // Reverse relationship for the dependency checks when removing styles
861 $requiringClasses = array_keys($classesArray['requires']);
862 foreach ($requiringClasses as $className) {
863 foreach ($classesArray['requires'][$className] as $requiredClass) {
864 if (!is_array($classesArray['requiredBy'][$requiredClass])) {
865 $classesArray['requiredBy'][$requiredClass] = array();
866 }
867 if (!in_array($className, $classesArray['requiredBy'][$requiredClass])) {
868 $classesArray['requiredBy'][$requiredClass][] = $className;
869 }
870 }
871 }
872 }
873 // Scanning the list of sets of mutually exclusives classes if specified in the RTE config
874 if (is_array($RTEProperties['mutuallyExclusiveClasses.'])) {
875 foreach ($RTEProperties['mutuallyExclusiveClasses.'] as $listName => $conf) {
876 $classSet = GeneralUtility::trimExplode(',', $conf, true);
877 $classList = implode(',', $classSet);
878 foreach ($classSet as $className) {
879 $classesArray['XOR'][$className] = '/^(' . implode('|', GeneralUtility::trimExplode(',', GeneralUtility::rmFromList($className, $classList), true)) . ')$/';
880 }
881 }
882 }
883 foreach ($classesArray as $key => $subArray) {
884 $JSClassesArray .= 'HTMLArea.classes' . ucfirst($key) . ' = ' . $this->buildNestedJSArray($subArray) . ';' . LF;
885 }
886 return $JSClassesArray;
887 }
888
889 /**
890 * Check for possible circularity in classes dependencies
891 *
892 * @param array $classesArray: reference to the array of classes dependencies
893 * @param string $requiringClass: class requiring at some iteration level from the initial requiring class
894 * @param string $initialClass: initial class from which a circular relationship is being searched
895 * @param int $recursionLevel: depth of recursive call
896 * @return bool TRUE, if a circular relationship is found
897 */
898 protected function hasCircularDependency(&$classesArray, $requiringClass, $initialClass, $recursionLevel = 0)
899 {
900 if (is_array($classesArray['requires'][$requiringClass])) {
901 if (in_array($initialClass, $classesArray['requires'][$requiringClass])) {
902 return true;
903 } else {
904 if ($recursionLevel++ < 20) {
905 foreach ($classesArray['requires'][$requiringClass] as $requiringClass2) {
906 if ($this->hasCircularDependency($classesArray, $requiringClass2, $initialClass, $recursionLevel)) {
907 return true;
908 }
909 }
910 }
911 return false;
912 }
913 } else {
914 return false;
915 }
916 }
917
918 /**
919 * Translate Page TS Config array in JS nested array definition
920 * Replace 0 values with false
921 * Unquote regular expression values
922 * Replace empty arrays with empty objects
923 *
924 * @param array $conf: Page TSConfig configuration array
925 * @return string nested JS array definition
926 */
927 protected function buildNestedJSArray($conf)
928 {
929 $convertedConf = GeneralUtility::removeDotsFromTS($conf);
930 return str_replace(
931 array(':"0"', ':"\\/^(', ')$\\/i"', ':"\\/^(', ')$\\/"', '[]'),
932 array(':false', ':/^(', ')$/i', ':/^(', ')$/', '{}'), json_encode($convertedConf)
933 );
934 }
935
936 /**
937 * Writes contents in a file in typo3temp and returns the file name
938 *
939 * @param string $label: A label to insert at the beginning of the name of the file
940 * @param string $fileExtension: The file extension of the file, defaulting to 'js'
941 * @param string $contents: The contents to write into the file
942 * @return string The name of the file written to typo3temp
943 * @throws \RuntimeException If writing to file failed
944 */
945 protected function writeTemporaryFile($label, $fileExtension = 'js', $contents = '')
946 {
947 $relativeFilename = 'typo3temp/assets/js/rte_' . str_replace('-', '_', $label) . '_' . GeneralUtility::shortMD5($contents, 20) . '.' . $fileExtension;
948 $destination = PATH_site . $relativeFilename;
949 if (!file_exists($destination)) {
950 $minifiedJavaScript = '';
951 if ($fileExtension === 'js' && $contents !== '') {
952 $minifiedJavaScript = GeneralUtility::minifyJavaScript($contents);
953 }
954 $failure = GeneralUtility::writeFileToTypo3tempDir($destination, $minifiedJavaScript ? $minifiedJavaScript : $contents);
955 if ($failure) {
956 throw new \RuntimeException($failure, 1460975840);
957 }
958 }
959 if (isset($GLOBALS['TSFE'])) {
960 $fileName = $relativeFilename;
961 } else {
962 $fileName = '../' . $relativeFilename;
963 }
964 return GeneralUtility::resolveBackPath($fileName);
965 }
966
967 /**
968 * Both rte framework and rte plugins can have label files that are
969 * used in JS. The methods gathers those and creates a JS object from
970 * file labels.
971 *
972 * @return string
973 */
974 protected function createJavaScriptLanguageLabelsFromFiles()
975 {
976 $labelArray = array();
977 // Load labels of 3 base files into JS
978 foreach (array('tooltips', 'msg', 'dialogs') as $identifier) {
979 $fileName = 'EXT:rtehtmlarea/Resources/Private/Language/locallang_' . $identifier . '.xlf';
980 $newLabels = $this->getMergedLabelsFromFile($fileName);
981 if (!empty($newLabels)) {
982 $labelArray[$identifier] = $newLabels;
983 }
984 }
985 // Load labels of plugins into JS
986 foreach ($this->pluginEnabledCumulativeArray as $pluginId) {
987 /** @var RteHtmlAreaApi $plugin */
988 $plugin = $this->registeredPlugins[$pluginId];
989 $extensionKey = is_object($plugin) ? $plugin->getExtensionKey() : 'rtehtmlarea';
990 $fileName = 'EXT:' . $extensionKey . '/Resources/Private/Language/Plugins/' . $pluginId . '/locallang_js.xlf';
991 $newLabels = $this->getMergedLabelsFromFile($fileName);
992 if (!empty($newLabels)) {
993 $labelArray[$pluginId] = $newLabels;
994 }
995 }
996 $javaScriptString = 'TYPO3.jQuery(function() {';
997 $javaScriptString .= 'HTMLArea.I18N = new Object();' . LF;
998 $javaScriptString .= 'HTMLArea.I18N = ' . json_encode($labelArray);
999 $javaScriptString .= '});';
1000 $this->resultArray['additionalJavaScriptPost'][] = $javaScriptString;
1001 }
1002
1003 /**
1004 * Get all labels from a specific label file, merge default
1005 * labels and target language labels.
1006 *
1007 * @param string $fileName The file to merge labels from
1008 * @return array Label keys and values
1009 */
1010 protected function getMergedLabelsFromFile($fileName)
1011 {
1012 /** @var $languageFactory LocalizationFactory */
1013 $languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
1014 $localizationArray = $languageFactory->getParsedData($fileName, $this->language, 'utf-8', 1);
1015 if (is_array($localizationArray) && !empty($localizationArray)) {
1016 if (!empty($localizationArray[$this->language])) {
1017 $finalLocalLang = $localizationArray['default'];
1018 ArrayUtility::mergeRecursiveWithOverrule($finalLocalLang, $localizationArray[$this->language], true, false);
1019 $localizationArray[$this->language] = $finalLocalLang;
1020 } else {
1021 $localizationArray[$this->language] = $localizationArray['default'];
1022 }
1023 } else {
1024 $localizationArray = array();
1025 }
1026 return $localizationArray[$this->language];
1027 }
1028
1029 /**
1030 * Return the JS code of the toolbar configuration for the HTMLArea editor
1031 *
1032 * @return string The JS code as nested JS arrays
1033 */
1034 protected function getJSToolbarArray()
1035 {
1036 // The toolbar array
1037 $toolbar = array();
1038 // The current row; a "linebreak" ends the current row
1039 $row = array();
1040 // The current group; each group is between "bar"s; a "linebreak" ends the current group
1041 $group = array();
1042 // Process each toolbar item in the toolbar order list
1043 foreach ($this->toolbarOrderArray as $item) {
1044 switch ($item) {
1045 case 'linebreak':
1046 // Add row to toolbar if not empty
1047 if (!empty($group)) {
1048 $row[] = $group;
1049 $group = array();
1050 }
1051 if (!empty($row)) {
1052 $toolbar[] = $row;
1053 $row = array();
1054 }
1055 break;
1056 case 'bar':
1057 // Add group to row if not empty
1058 if (!empty($group)) {
1059 $row[] = $group;
1060 $group = array();
1061 }
1062 break;
1063 case 'space':
1064 if (end($group) != $this->convertToolbarForHtmlAreaArray[$item]) {
1065 $group[] = $this->convertToolbarForHtmlAreaArray[$item];
1066 }
1067 break;
1068 default:
1069 if (in_array($item, $this->toolbar)) {
1070 // Add the item to the group
1071 $convertedItem = $this->convertToolbarForHtmlAreaArray[$item];
1072 if ($convertedItem) {
1073 $group[] = $convertedItem;
1074 }
1075 }
1076 }
1077 }
1078 // Add the last group and last line, if not empty
1079 if (!empty($group)) {
1080 $row[] = $group;
1081 }
1082 if (!empty($row)) {
1083 $toolbar[] = $row;
1084 }
1085 return json_encode($toolbar);
1086 }
1087
1088 /**
1089 * Make a file name relative to the PATH_site or to the PATH_typo3
1090 *
1091 * @param string $filename: a file name of the form EXT:.... or relative to the PATH_site
1092 * @return string the file name relative to the PATH_site if in frontend or relative to the PATH_typo3 if in backend
1093 */
1094 protected function getFullFileName($filename)
1095 {
1096 if (substr($filename, 0, 4) === 'EXT:') {
1097 // extension
1098 list($extKey, $local) = explode('/', substr($filename, 4), 2);
1099 $newFilename = '';
1100 if ((string)$extKey !== '' && ExtensionManagementUtility::isLoaded($extKey) && (string)$local !== '') {
1101 $newFilename = ($this->isFrontendEditActive()
1102 ? ExtensionManagementUtility::siteRelPath($extKey)
1103 : ExtensionManagementUtility::extRelPath($extKey))
1104 . $local;
1105 }
1106 } else {
1107 $path = ($this->isFrontendEditActive() ? '' : '../');
1108 $newFilename = $path . ($filename[0] === '/' ? substr($filename, 1) : $filename);
1109 }
1110 return GeneralUtility::resolveBackPath($newFilename);
1111 }
1112
1113 /**
1114 * Return the Javascript code for copying the HTML code from the editor into the hidden input field.
1115 *
1116 * @return void
1117 */
1118 protected function addOnSubmitJavaScriptCode()
1119 {
1120 $onSubmitCode = array();
1121 $onSubmitCode[] = 'if (RTEarea[' . GeneralUtility::quoteJSvalue($this->domIdentifier) . ']) {';
1122 $onSubmitCode[] = 'document.editform[' . GeneralUtility::quoteJSvalue($this->data['parameterArray']['itemFormElName']) . '].value = RTEarea[' . GeneralUtility::quoteJSvalue($this->domIdentifier) . '].editor.getHTML();';
1123 $onSubmitCode[] = '} else {';
1124 $onSubmitCode[] = 'OK = 0;';
1125 $onSubmitCode[] = '};';
1126 $this->resultArray['additionalJavaScriptSubmit'][] = implode(LF, $onSubmitCode);
1127 }
1128
1129 /**
1130 * Checks if frontend editing is active.
1131 *
1132 * @return bool TRUE if frontend editing is active
1133 */
1134 protected function isFrontendEditActive()
1135 {
1136 return is_object($GLOBALS['TSFE']) && $GLOBALS['TSFE']->beUserLogin && $GLOBALS['BE_USER']->frontendEdit instanceof FrontendEditingController;
1137 }
1138
1139 /**
1140 * Client Browser Information
1141 *
1142 * @return array Contains keys "user agent", "browser", "version", "system"
1143 */
1144 protected function clientInfo()
1145 {
1146 $userAgent = GeneralUtility::getIndpEnv('HTTP_USER_AGENT');
1147 $browserInfo = ClientUtility::getBrowserInfo($userAgent);
1148 // Known engines: order is not irrelevant!
1149 $knownEngines = array('opera', 'msie', 'gecko', 'webkit');
1150 if (is_array($browserInfo['all'])) {
1151 foreach ($knownEngines as $engine) {
1152 if ($browserInfo['all'][$engine]) {
1153 $browserInfo['browser'] = $engine;
1154 $browserInfo['version'] = ClientUtility::getVersion($browserInfo['all'][$engine]);
1155 break;
1156 }
1157 }
1158 }
1159 return $browserInfo;
1160 }
1161
1162 /**
1163 * Initialize a couple of language related local properties
1164 *
1165 * @return void
1166 */
1167 public function initializeLanguageRelatedProperties()
1168 {
1169 $this->language = $GLOBALS['LANG']->lang;
1170 if ($this->language === 'default' || !$this->language) {
1171 $this->language = 'en';
1172 }
1173 $currentLanguageUid = $this->data['databaseRow']['sys_language_uid'];
1174 if (is_array($currentLanguageUid)) {
1175 $currentLanguageUid = $currentLanguageUid[0];
1176 }
1177 $this->contentLanguageUid = (int)max($currentLanguageUid, 0);
1178 if ($this->contentLanguageUid) {
1179 $this->contentISOLanguage = $this->language;
1180 if (ExtensionManagementUtility::isLoaded('static_info_tables')) {
1181 $tableA = 'sys_language';
1182 $tableB = 'static_languages';
1183
1184 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1185 ->getQueryBuilderForTable($tableA);
1186
1187 $result = $queryBuilder
1188 ->select('a.uid', 'b.lg_iso_2', 'b.lg_country_iso_2')
1189 ->from($tableA)
1190 ->where('a.uid', (int)$this->contentLanguageUid)
1191 ->leftJoin(
1192 'a',
1193 $tableB,
1194 'b',
1195 $queryBuilder->expr()->eq('a.static_lang_isocode', $queryBuilder->quoteIdentifier('b.uid'))
1196 )
1197 ->execute();
1198
1199 while ($languageRow = $result->fetch()) {
1200 $this->contentISOLanguage = strtolower(trim($languageRow['lg_iso_2']) . (trim($languageRow['lg_country_iso_2']) ? '_' . trim($languageRow['lg_country_iso_2']) : ''));
1201 }
1202 }
1203 } else {
1204 $this->contentISOLanguage = trim($this->processedRteConfiguration['defaultContentLanguage']) ?: 'en';
1205 $languageCodeParts = explode('_', $this->contentISOLanguage);
1206 $this->contentISOLanguage = strtolower($languageCodeParts[0]) . ($languageCodeParts[1] ? '_' . strtoupper($languageCodeParts[1]) : '');
1207 // Find the configured language in the list of localization locales
1208 /** @var $locales Locales */
1209 $locales = GeneralUtility::makeInstance(Locales::class);
1210 // If not found, default to 'en'
1211 if (!in_array($this->contentISOLanguage, $locales->getLocales())) {
1212 $this->contentISOLanguage = 'en';
1213 }
1214 }
1215 $this->contentTypo3Language = $this->contentISOLanguage === 'en' ? 'default' : $this->contentISOLanguage;
1216 }
1217
1218 /**
1219 * Log usage of deprecated Page TS Config Property
1220 *
1221 * @param string $deprecatedProperty: Name of deprecated property
1222 * @param string $useProperty: Name of property to use instead
1223 * @param string $version: Version of TYPO3 in which the property will be removed
1224 * @return void
1225 */
1226 protected function logDeprecatedProperty($deprecatedProperty, $useProperty, $version)
1227 {
1228 $backendUser = $this->getBackendUserAuthentication();
1229 if (!$this->processedRteConfiguration['logDeprecatedProperties.']['disabled']) {
1230 $message = sprintf(
1231 '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.',
1232 $deprecatedProperty,
1233 $useProperty,
1234 $version,
1235 $this->data['databaseRow']['pid']
1236 );
1237 GeneralUtility::deprecationLog($message);
1238 if ($this->processedRteConfiguration['logDeprecatedProperties.']['logAlsoToBELog']) {
1239 $message = sprintf(
1240 $this->getLanguageService()->sL('LLL:EXT:rtehtmlarea/Resources/Private/Language/locallang.xlf:deprecatedPropertyMessage'),
1241 $deprecatedProperty,
1242 $useProperty,
1243 $version,
1244 $this->data['databaseRow']['pid']
1245 );
1246 $backendUser->simplelog($message, 'rtehtmlarea');
1247 }
1248 }
1249 }
1250
1251 /**
1252 * A list of parameters that is mostly given as GET/POST to other RTE controllers.
1253 *
1254 * @return string
1255 */
1256 protected function RTEtsConfigParams()
1257 {
1258 $parameters = BackendUtility::getSpecConfParametersFromArray($this->defaultExtras['rte_transform']['parameters']);
1259 $result = array(
1260 $this->data['tableName'],
1261 $this->data['databaseRow']['uid'],
1262 $this->data['fieldName'],
1263 $this->pidOfVersionedMotherRecord,
1264 $this->data['recordTypeValue'],
1265 $this->pidOfPageRecord,
1266 $parameters['imgpath'],
1267 );
1268 return implode(':', $result);
1269 }
1270
1271 /**
1272 * Clean list
1273 *
1274 * @param string $str String to clean
1275 * @return string Cleaned string
1276 */
1277 protected function cleanList($str)
1278 {
1279 if (strstr($str, '*')) {
1280 $str = '*';
1281 } else {
1282 $str = implode(',', array_unique(GeneralUtility::trimExplode(',', $str, true)));
1283 }
1284 return $str;
1285 }
1286
1287 /**
1288 * Performs transformation of content from database to richtext editor
1289 *
1290 * @param string $value Value to transform.
1291 * @return string Transformed content
1292 */
1293 protected function transformDatabaseContentToEditor($value)
1294 {
1295 // change <strong> to <b>
1296 $value = preg_replace('/<(\\/?)strong/i', '<$1b', $value);
1297 // change <em> to <i>
1298 $value = preg_replace('/<(\\/?)em([^b>]*>)/i', '<$1i$2', $value);
1299
1300 if ($this->defaultExtras['rte_transform']) {
1301 /** @var RteHtmlParser $parseHTML */
1302 $parseHTML = GeneralUtility::makeInstance(RteHtmlParser::class);
1303 $parseHTML->init($this->data['table'] . ':' . $this->data['fieldName'], $this->pidOfVersionedMotherRecord);
1304 $value = $parseHTML->RTE_transform($value, $this->defaultExtras, 'rte', $this->processedRteConfiguration);
1305 }
1306 return $value;
1307 }
1308
1309 /**
1310 * True if RTE is in full screen mode / called via wizard controller
1311 *
1312 * @return bool
1313 */
1314 protected function isInFullScreenMode()
1315 {
1316 return GeneralUtility::_GP('M') === 'wizard_rte';
1317 }
1318
1319 /**
1320 * @return LanguageService
1321 */
1322 protected function getLanguageService()
1323 {
1324 return $GLOBALS['LANG'];
1325 }
1326
1327 /**
1328 * @return BackendUserAuthentication
1329 */
1330 protected function getBackendUserAuthentication()
1331 {
1332 return $GLOBALS['BE_USER'];
1333 }
1334 }