‪TYPO3CMS  10.4
RichTextElement.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\EventDispatcher\EventDispatcherInterface;
32 
38 {
44  protected ‪$defaultFieldInformation = [
45  'tcaDescription' => [
46  'renderType' => 'tcaDescription',
47  ],
48  ];
49 
55  protected ‪$defaultFieldWizard = [
56  'localizationStateSelector' => [
57  'renderType' => 'localizationStateSelector',
58  ],
59  'otherLanguageContent' => [
60  'renderType' => 'otherLanguageContent',
61  'after' => [
62  'localizationStateSelector'
63  ],
64  ],
65  'defaultLanguageDifferences' => [
66  'renderType' => 'defaultLanguageDifferences',
67  'after' => [
68  'otherLanguageContent',
69  ],
70  ],
71  ];
72 
79  protected ‪$rteConfiguration = [];
80 
84  protected ‪$eventDispatcher;
85 
93  public function ‪__construct(‪NodeFactory ‪$nodeFactory, array ‪$data, EventDispatcherInterface ‪$eventDispatcher = null)
94  {
95  parent::__construct(‪$nodeFactory, ‪$data);
96  $this->eventDispatcher = ‪$eventDispatcher ?? GeneralUtility::getContainer()->get(EventDispatcherInterface::class);
97  }
98 
105  public function ‪render(): array
106  {
107  $resultArray = $this->‪initializeResultArray();
108  $parameterArray = $this->data['parameterArray'];
109  $config = $parameterArray['fieldConf']['config'];
110 
111  $fieldId = $this->‪sanitizeFieldId($parameterArray['itemFormElName']);
112  $itemFormElementName = $this->data['parameterArray']['itemFormElName'];
113 
114  $value = $this->data['parameterArray']['itemFormElValue'] ?? '';
115 
116  $fieldInformationResult = $this->‪renderFieldInformation();
117  $fieldInformationHtml = $fieldInformationResult['html'];
118  $resultArray = $this->‪mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
119 
120  $fieldControlResult = $this->‪renderFieldControl();
121  $fieldControlHtml = $fieldControlResult['html'];
122  $resultArray = $this->‪mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
123 
124  $fieldWizardResult = $this->‪renderFieldWizard();
125  $fieldWizardHtml = $fieldWizardResult['html'];
126  $resultArray = $this->‪mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
127 
128  $attributes = [
129  'style' => 'display:none',
130  'data-formengine-validation-rules' => $this->‪getValidationDataAsJsonString($config),
131  'id' => $fieldId,
132  'name' => htmlspecialchars($itemFormElementName),
133  ];
134 
135  $html = [];
136  $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
137  $html[] = $fieldInformationHtml;
138  $html[] = '<div class="form-control-wrap">';
139  $html[] = '<div class="form-wizards-wrap">';
140  $html[] = '<div class="form-wizards-element">';
141  $html[] = '<textarea ' . GeneralUtility::implodeAttributes($attributes, true) . '>';
142  $html[] = htmlspecialchars($value);
143  $html[] = '</textarea>';
144  $html[] = '</div>';
145  if (!empty($fieldControlHtml)) {
146  $html[] = '<div class="form-wizards-items-aside">';
147  $html[] = '<div class="btn-group">';
148  $html[] = $fieldControlHtml;
149  $html[] = '</div>';
150  $html[] = '</div>';
151  }
152  if (!empty($fieldWizardHtml)) {
153  $html[] = '<div class="form-wizards-items-bottom">';
154  $html[] = $fieldWizardHtml;
155  $html[] = '</div>';
156  }
157  $html[] = '</div>';
158  $html[] = '</div>';
159  $html[] = '</div>';
160 
161  $resultArray['html'] = implode(LF, $html);
162 
163  $this->rteConfiguration = $config['richtextConfiguration']['editor'];
164  $resultArray['requireJsModules'][] = [
165  'ckeditor' => $this->‪getCkEditorRequireJsModuleCode($fieldId)
166  ];
167 
168  return $resultArray;
169  }
170 
176  protected function ‪getLanguageIsoCodeOfContent(): string
177  {
178  $currentLanguageUid = $this->data['databaseRow']['sys_language_uid'];
179  if (is_array($currentLanguageUid)) {
180  $currentLanguageUid = $currentLanguageUid[0];
181  }
182  $contentLanguageUid = (int)max($currentLanguageUid, 0);
183  if ($contentLanguageUid) {
184  // the language rows might not be fully inialized, so we fallback to en_US in this case
185  $contentLanguage = $this->data['systemLanguageRows'][$currentLanguageUid]['iso'] ?? 'en_US';
186  } else {
187  $contentLanguage = $this->rteConfiguration['config']['defaultContentLanguage'] ?? 'en_US';
188  }
189  $languageCodeParts = explode('_', $contentLanguage);
190  $contentLanguage = strtolower($languageCodeParts[0]) . ($languageCodeParts[1] ? '_' . strtoupper($languageCodeParts[1]) : '');
191  // Find the configured language in the list of localization locales
192  ‪$locales = GeneralUtility::makeInstance(Locales::class);
193  // If not found, default to 'en'
194  if (!in_array($contentLanguage, ‪$locales->getLocales(), true)) {
195  $contentLanguage = 'en';
196  }
197  return $contentLanguage;
198  }
199 
207  protected function ‪getCkEditorRequireJsModuleCode(string $fieldId): string
208  {
209  $configuration = $this->‪prepareConfigurationForEditor();
210 
211  $externalPlugins = '';
212  foreach ($this->‪getExtraPlugins() as $extraPluginName => $extraPluginConfig) {
213  $configName = $extraPluginConfig['configName'] ?? $extraPluginName;
214  if (!empty($extraPluginConfig['config']) && is_array($extraPluginConfig['config'])) {
215  if (empty($configuration[$configName])) {
216  $configuration[$configName] = $extraPluginConfig['config'];
217  } elseif (is_array($configuration[$configName])) {
218  $configuration[$configName] = array_replace_recursive($extraPluginConfig['config'], $configuration[$configName]);
219  }
220  }
221  $configuration['extraPlugins'] .= ',' . $extraPluginName;
222 
223  $externalPlugins .= 'CKEDITOR.plugins.addExternal(';
224  $externalPlugins .= GeneralUtility::quoteJSvalue($extraPluginName) . ',';
225  $externalPlugins .= GeneralUtility::quoteJSvalue($extraPluginConfig['resource']) . ',';
226  $externalPlugins .= '\'\');';
227  }
228 
229  $jsonConfiguration = (string)json_encode($configuration);
230 
231  // Make a hash of the configuration and append it to CKEDITOR.timestamp
232  // This will mitigate browser caching issue when plugins are updated
233  $configurationHash = GeneralUtility::shortMD5($jsonConfiguration);
234 
235  return 'function(CKEDITOR) {
236  CKEDITOR.timestamp += "-' . $configurationHash . '";
237  ' . $externalPlugins . '
238  require([\'jquery\', \'TYPO3/CMS/Backend/FormEngine\'], function($, FormEngine) {
239  $(function(){
240  var escapedFieldSelector = \'#\' + $.escapeSelector(\'' . $fieldId . '\');
241  CKEDITOR.replace("' . $fieldId . '", ' . $jsonConfiguration . ');
242  CKEDITOR.instances["' . $fieldId . '"].on(\'change\', function(e) {
243  var commands = e.sender.commands;
244  CKEDITOR.instances["' . $fieldId . '"].updateElement();
245  FormEngine.Validation.validateField($(escapedFieldSelector));
246  FormEngine.Validation.markFieldAsChanged($(escapedFieldSelector));
247 
248  // remember changes done in maximized state and mark field as changed, once minimized again
249  if (typeof commands.maximize !== \'undefined\' && commands.maximize.state === 1) {
250  CKEDITOR.instances["' . $fieldId . '"].on(\'maximize\', function(e) {
251  $(this).off(\'maximize\');
252  FormEngine.Validation.markFieldAsChanged($(escapedFieldSelector));
253  });
254  }
255  });
256  CKEDITOR.instances["' . $fieldId . '"].on(\'mode\', function() {
257  // detect field changes in source mode
258  if (this.mode === \'source\') {
259  var sourceArea = CKEDITOR.instances["' . $fieldId . '"].editable();
260  sourceArea.attachListener(sourceArea, \'change\', function() {
261  FormEngine.Validation.markFieldAsChanged($(escapedFieldSelector));
262  });
263  }
264  });
265  $(document).on(\'inline:sorting-changed\', function() {
266  CKEDITOR.instances["' . $fieldId . '"].destroy();
267  CKEDITOR.replace("' . $fieldId . '", ' . $jsonConfiguration . ');
268  });
269  $(document).on(\'flexform:sorting-changed\', function() {
270  CKEDITOR.instances["' . $fieldId . '"].destroy();
271  CKEDITOR.replace("' . $fieldId . '", ' . $jsonConfiguration . ');
272  });
273  });
274  });
275  }';
276  }
277 
283  protected function ‪getExtraPlugins(): array
284  {
285  $externalPlugins = $this->rteConfiguration['externalPlugins'] ?? [];
286  $externalPlugins = $this->eventDispatcher
287  ->dispatch(new BeforeGetExternalPluginsEvent($externalPlugins, $this->data))
288  ->getConfiguration();
289 
290  $urlParameters = [
291  'P' => [
292  'table' => $this->data['tableName'],
293  'uid' => $this->data['databaseRow']['uid'],
294  'fieldName' => $this->data['fieldName'],
295  'recordType' => $this->data['recordTypeValue'],
296  'pid' => $this->data['effectivePid'],
297  'richtextConfigurationName' => $this->data['parameterArray']['fieldConf']['config']['richtextConfigurationName']
298  ]
299  ];
300 
301  $pluginConfiguration = [];
302  $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
303  foreach ($externalPlugins as $pluginName => $configuration) {
304  $pluginConfiguration[$pluginName] = [
305  'configName' => $configuration['configName'] ?? $pluginName,
306  'resource' => $this->‪resolveUrlPath($configuration['resource'])
307  ];
308  unset($configuration['configName']);
309  unset($configuration['resource']);
310 
311  if ($configuration['route']) {
312  $configuration['routeUrl'] = (string)$uriBuilder->buildUriFromRoute($configuration['route'], $urlParameters);
313  }
314 
315  $pluginConfiguration[$pluginName]['config'] = $configuration;
316  }
317 
318  $pluginConfiguration = $this->eventDispatcher
319  ->dispatch(new AfterGetExternalPluginsEvent($pluginConfiguration, $this->data))
320  ->getConfiguration();
321  return $pluginConfiguration;
322  }
323 
330  protected function ‪replaceLanguageFileReferences(array $configuration): array
331  {
332  foreach ($configuration as $key => $value) {
333  if (is_array($value)) {
334  $configuration[$key] = $this->‪replaceLanguageFileReferences($value);
335  } elseif (is_string($value) && stripos($value, 'LLL:') === 0) {
336  $configuration[$key] = $this->‪getLanguageService()->‪sL($value);
337  }
338  }
339  return $configuration;
340  }
341 
348  protected function ‪replaceAbsolutePathsToRelativeResourcesPath(array $configuration): array
349  {
350  foreach ($configuration as $key => $value) {
351  if (is_array($value)) {
352  $configuration[$key] = $this->‪replaceAbsolutePathsToRelativeResourcesPath($value);
353  } elseif (is_string($value) && stripos($value, 'EXT:') === 0) {
354  $configuration[$key] = $this->‪resolveUrlPath($value);
355  }
356  }
357  return $configuration;
358  }
359 
366  protected function ‪resolveUrlPath(string $value): string
367  {
368  $value = GeneralUtility::getFileAbsFileName($value);
369  return ‪PathUtility::getAbsoluteWebPath($value);
370  }
371 
378  protected function ‪prepareConfigurationForEditor(): array
379  {
380  // Ensure custom config is empty so nothing additional is loaded
381  // Of course this can be overridden by the editor configuration below
382  $configuration = [
383  'customConfig' => '',
384  ];
385 
386  if (is_array($this->rteConfiguration['config'])) {
387  $configuration = array_replace_recursive($configuration, $this->rteConfiguration['config']);
388  }
389 
390  $configuration = $this->eventDispatcher
391  ->dispatch(new BeforePrepareConfigurationForEditorEvent($configuration, $this->data))
392  ->getConfiguration();
393 
394  // Set the UI language of the editor if not hard-coded by the existing configuration
395  if (empty($configuration['language'])) {
396  $configuration['language'] = $this->‪getBackendUser()->uc['lang'] ?: ($this->‪getBackendUser()->user['lang'] ?: 'en');
397  }
398  $configuration['contentsLanguage'] = $this->‪getLanguageIsoCodeOfContent();
399 
400  // Replace all label references
401  $configuration = $this->‪replaceLanguageFileReferences($configuration);
402  // Replace all paths
403  $configuration = $this->‪replaceAbsolutePathsToRelativeResourcesPath($configuration);
404 
405  // there are some places where we define an array, but it needs to be a list in order to work
406  if (is_array($configuration['extraPlugins'])) {
407  $configuration['extraPlugins'] = implode(',', $configuration['extraPlugins']);
408  }
409  if (is_array($configuration['removePlugins'])) {
410  $configuration['removePlugins'] = implode(',', $configuration['removePlugins']);
411  }
412  if (is_array($configuration['removeButtons'])) {
413  $configuration['removeButtons'] = implode(',', $configuration['removeButtons']);
414  }
415 
416  $configuration = $this->eventDispatcher
417  ->dispatch(new AfterPrepareConfigurationForEditorEvent($configuration, $this->data))
418  ->getConfiguration();
419 
420  return $configuration;
421  }
422 
427  protected function ‪sanitizeFieldId(string $itemFormElementName): string
428  {
429  $fieldId = (string)preg_replace('/[^a-zA-Z0-9_:.-]/', '_', $itemFormElementName);
430  return htmlspecialchars((string)preg_replace('/^[^a-zA-Z]/', 'x', $fieldId));
431  }
432 
436  protected function ‪getBackendUser()
437  {
438  return ‪$GLOBALS['BE_USER'];
439  }
440 }
‪TYPO3\CMS\Backend\Form\Element\AbstractFormElement\renderFieldInformation
‪array renderFieldInformation()
Definition: AbstractFormElement.php:72
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:24
‪TYPO3\CMS\RteCKEditor\Form\Element
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\replaceLanguageFileReferences
‪array replaceLanguageFileReferences(array $configuration)
Definition: RichTextElement.php:326
‪TYPO3\CMS\Backend\Form\AbstractNode\mergeChildReturnIntoExistingResult
‪array mergeChildReturnIntoExistingResult(array $existing, array $childReturn, bool $mergeHtml=true)
Definition: AbstractNode.php:116
‪TYPO3\CMS\Backend\Form\AbstractNode\initializeResultArray
‪array initializeResultArray()
Definition: AbstractNode.php:90
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement
Definition: RichTextElement.php:38
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\replaceAbsolutePathsToRelativeResourcesPath
‪array replaceAbsolutePathsToRelativeResourcesPath(array $configuration)
Definition: RichTextElement.php:344
‪TYPO3\CMS\Backend\Form\AbstractNode\$nodeFactory
‪NodeFactory $nodeFactory
Definition: AbstractNode.php:36
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\$rteConfiguration
‪array $rteConfiguration
Definition: RichTextElement.php:76
‪TYPO3\CMS\RteCKEditor\Form\Element\Event\BeforeGetExternalPluginsEvent
Definition: BeforeGetExternalPluginsEvent.php:24
‪TYPO3\CMS\RteCKEditor\Form\Element\Event\AfterPrepareConfigurationForEditorEvent
Definition: AfterPrepareConfigurationForEditorEvent.php:24
‪TYPO3\CMS\Backend\Form\Element\AbstractFormElement
Definition: AbstractFormElement.php:32
‪TYPO3\CMS\Core\Localization\Locales
Definition: Locales.php:30
‪TYPO3\CMS\Core\Localization\LanguageService\sL
‪string sL($input)
Definition: LanguageService.php:194
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\resolveUrlPath
‪string resolveUrlPath(string $value)
Definition: RichTextElement.php:362
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\render
‪array render()
Definition: RichTextElement.php:101
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\sanitizeFieldId
‪string sanitizeFieldId(string $itemFormElementName)
Definition: RichTextElement.php:423
‪TYPO3\CMS\Backend\Form\AbstractNode\getValidationDataAsJsonString
‪string getValidationDataAsJsonString(array $config)
Definition: AbstractNode.php:151
‪TYPO3\CMS\RteCKEditor\Form\Element\Event\AfterGetExternalPluginsEvent
Definition: AfterGetExternalPluginsEvent.php:24
‪TYPO3\CMS\Backend\Form\Element\AbstractFormElement\renderFieldControl
‪array renderFieldControl()
Definition: AbstractFormElement.php:88
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\getLanguageIsoCodeOfContent
‪string getLanguageIsoCodeOfContent()
Definition: RichTextElement.php:172
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:38
‪TYPO3\CMS\Backend\Form\AbstractNode\$data
‪array $data
Definition: AbstractNode.php:42
‪$locales
‪$locales
Definition: be_users.php:7
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Backend\Form\NodeFactory
Definition: NodeFactory.php:37
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\$defaultFieldInformation
‪array $defaultFieldInformation
Definition: RichTextElement.php:43
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\$eventDispatcher
‪EventDispatcherInterface $eventDispatcher
Definition: RichTextElement.php:80
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\$defaultFieldWizard
‪array $defaultFieldWizard
Definition: RichTextElement.php:53
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\getCkEditorRequireJsModuleCode
‪string getCkEditorRequireJsModuleCode(string $fieldId)
Definition: RichTextElement.php:203
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\getExtraPlugins
‪array getExtraPlugins()
Definition: RichTextElement.php:279
‪TYPO3\CMS\Backend\Form\Element\AbstractFormElement\getLanguageService
‪LanguageService getLanguageService()
Definition: AbstractFormElement.php:390
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\prepareConfigurationForEditor
‪array prepareConfigurationForEditor()
Definition: RichTextElement.php:374
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\__construct
‪__construct(NodeFactory $nodeFactory, array $data, EventDispatcherInterface $eventDispatcher=null)
Definition: RichTextElement.php:89
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Core\Utility\PathUtility\getAbsoluteWebPath
‪static string getAbsoluteWebPath($targetPath)
Definition: PathUtility.php:43
‪TYPO3\CMS\RteCKEditor\Form\Element\Event\BeforePrepareConfigurationForEditorEvent
Definition: BeforePrepareConfigurationForEditorEvent.php:24
‪TYPO3\CMS\Backend\Form\Element\AbstractFormElement\renderFieldWizard
‪array renderFieldWizard()
Definition: AbstractFormElement.php:104
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\getBackendUser
‪BackendUserAuthentication getBackendUser()
Definition: RichTextElement.php:432