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