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