‪TYPO3CMS  ‪main
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 
81  public function ‪__construct(
82  private readonly EventDispatcherInterface $eventDispatcher,
83  private readonly ‪UriBuilder $uriBuilder,
84  private readonly ‪Locales $locales,
85  ) {}
86 
92  public function ‪render(): array
93  {
94  $resultArray = $this->‪initializeResultArray();
95  $parameterArray = $this->data['parameterArray'];
96  $config = $parameterArray['fieldConf']['config'];
97 
98  $fieldId = $this->‪sanitizeFieldId($parameterArray['itemFormElName']);
99  $itemFormElementName = $this->data['parameterArray']['itemFormElName'];
100 
101  $value = $this->data['parameterArray']['itemFormElValue'] ?? '';
102 
103  $fieldInformationResult = $this->‪renderFieldInformation();
104  $fieldInformationHtml = $fieldInformationResult['html'];
105  $resultArray = $this->‪mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
106 
107  $fieldControlResult = $this->‪renderFieldControl();
108  $fieldControlHtml = $fieldControlResult['html'];
109  $resultArray = $this->‪mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
110 
111  $fieldWizardResult = $this->‪renderFieldWizard();
112  $fieldWizardHtml = $fieldWizardResult['html'];
113  $resultArray = $this->‪mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
114 
115  $this->rteConfiguration = $config['richtextConfiguration']['editor'] ?? [];
116  $ckeditorConfiguration = $this->‪resolveCkEditorConfiguration();
117 
118  $ckeditorAttributes = GeneralUtility::implodeAttributes([
119  'id' => $fieldId . 'ckeditor5',
120  'options' => GeneralUtility::jsonEncodeForHtmlAttribute($ckeditorConfiguration, false),
121  'form-engine' => GeneralUtility::jsonEncodeForHtmlAttribute([
122  'id' => $fieldId,
123  'name' => $itemFormElementName,
124  'value' => $value,
125  'validationRules' => $this->‪getValidationDataAsJsonString($config),
126  ], false),
127  ], true);
128 
129  $html = [];
130  $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
131  $html[] = $fieldInformationHtml;
132  $html[] = '<div class="form-control-wrap">';
133  $html[] = '<div class="form-wizards-wrap">';
134  $html[] = '<div class="form-wizards-element">';
135  $html[] = '<typo3-rte-ckeditor-ckeditor5 ' . $ckeditorAttributes . '>';
136  $html[] = '</typo3-rte-ckeditor-ckeditor5>';
137  $html[] = '</div>';
138  if (!empty($fieldControlHtml)) {
139  $html[] = '<div class="form-wizards-items-aside form-wizards-items-aside--field-control">';
140  $html[] = '<div class="btn-group">';
141  $html[] = $fieldControlHtml;
142  $html[] = '</div>';
143  $html[] = '</div>';
144  }
145  if (!empty($fieldWizardHtml)) {
146  $html[] = '<div class="form-wizards-items-bottom">';
147  $html[] = $fieldWizardHtml;
148  $html[] = '</div>';
149  }
150  $html[] = '</div>';
151  $html[] = '</div>';
152  $html[] = '</div>';
153 
154  $resultArray['html'] = $this->‪wrapWithFieldsetAndLegend(implode(LF, $html));
155  $resultArray['javaScriptModules'][] = ‪JavaScriptModuleInstruction::create('@typo3/rte-ckeditor/ckeditor5.js');
156 
157  $uiLanguage = $ckeditorConfiguration['language']['ui'];
158  if ($this->‪translationExists($uiLanguage)) {
159  $resultArray['javaScriptModules'][] = ‪JavaScriptModuleInstruction::create('@typo3/ckeditor5/translations/' . $uiLanguage . '.js');
160  }
161 
162  $contentLanguage = $ckeditorConfiguration['language']['content'];
163  if ($this->‪translationExists($contentLanguage)) {
164  $resultArray['javaScriptModules'][] = ‪JavaScriptModuleInstruction::create('@typo3/ckeditor5/translations/' . $contentLanguage . '.js');
165  }
166 
167  $resultArray['stylesheetFiles'][] = 'EXT:rte_ckeditor/Resources/Public/Css/editor.css';
168 
169  return $resultArray;
170  }
171 
175  protected function ‪getLanguageIsoCodeOfContent(): string
176  {
177  $currentLanguageUid = ($this->data['databaseRow']['sys_language_uid'] ?? 0);
178  if (is_array($currentLanguageUid)) {
179  $currentLanguageUid = $currentLanguageUid[0];
180  }
181  $contentLanguageUid = (int)max($currentLanguageUid, 0);
182  if ($contentLanguageUid) {
183  // the language rows might not be fully initialized, so we fall back to en-US in this case
184  $contentLanguage = $this->data['systemLanguageRows'][$currentLanguageUid]['iso'] ?? 'en-US';
185  } else {
186  $contentLanguage = $this->rteConfiguration['config']['defaultContentLanguage'] ?? 'en-US';
187  }
188  $languageCodeParts = explode('_', $contentLanguage);
189  $contentLanguage = strtolower($languageCodeParts[0]) . (!empty($languageCodeParts[1]) ? '_' . strtoupper($languageCodeParts[1]) : '');
190  // Find the configured language in the list of localization locales, if not found, default to 'en'.
191  if ($contentLanguage === 'default' || !$this->locales->isValidLanguageKey($contentLanguage)) {
192  $contentLanguage = 'en';
193  }
194  return $contentLanguage;
195  }
196 
197  protected function ‪resolveCkEditorConfiguration(): array
198  {
199  $configuration = $this->‪prepareConfigurationForEditor();
200 
201  foreach ($this->‪getExtraPlugins() as $extraPluginName => $extraPluginConfig) {
202  $configName = $extraPluginConfig['configName'] ?? $extraPluginName;
203  if (!empty($extraPluginConfig['config']) && is_array($extraPluginConfig['config'])) {
204  if (empty($configuration[$configName])) {
205  $configuration[$configName] = $extraPluginConfig['config'];
206  } elseif (is_array($configuration[$configName])) {
207  $configuration[$configName] = array_replace_recursive($extraPluginConfig['config'], $configuration[$configName]);
208  }
209  }
210  }
211  if (isset($this->data['parameterArray']['fieldConf']['config']['placeholder'])) {
212  $configuration['placeholder'] = (string)$this->data['parameterArray']['fieldConf']['config']['placeholder'];
213  }
214  return $configuration;
215  }
216 
220  protected function ‪getExtraPlugins(): array
221  {
222  $externalPlugins = $this->rteConfiguration['externalPlugins'] ?? [];
223  $externalPlugins = $this->eventDispatcher
224  ->dispatch(new BeforeGetExternalPluginsEvent($externalPlugins, $this->data))
225  ->getConfiguration();
226 
227  $urlParameters = [
228  'P' => [
229  'table' => $this->data['tableName'],
230  'uid' => $this->data['databaseRow']['uid'],
231  'fieldName' => $this->data['fieldName'],
232  'recordType' => $this->data['recordTypeValue'],
233  'pid' => $this->data['effectivePid'],
234  'richtextConfigurationName' => $this->data['parameterArray']['fieldConf']['config']['richtextConfigurationName'],
235  ],
236  ];
237 
238  $pluginConfiguration = [];
239  foreach ($externalPlugins as $pluginName => $configuration) {
240  $pluginConfiguration[$pluginName] = [
241  'configName' => $configuration['configName'] ?? $pluginName,
242  ];
243  unset($configuration['configName']);
244  // CKEditor4 style config, unused in CKEditor5 and not forwarded to the resutling plugin config
245  unset($configuration['resource']);
246 
247  if ($configuration['route'] ?? null) {
248  $configuration['routeUrl'] = (string)$this->uriBuilder->buildUriFromRoute($configuration['route'], $urlParameters);
249  }
250 
251  $pluginConfiguration[$pluginName]['config'] = $configuration;
252  }
253 
254  $pluginConfiguration = $this->eventDispatcher
255  ->dispatch(new AfterGetExternalPluginsEvent($pluginConfiguration, $this->data))
256  ->getConfiguration();
257  return $pluginConfiguration;
258  }
259 
263  protected function ‪replaceLanguageFileReferences(array $configuration): array
264  {
265  foreach ($configuration as $key => $value) {
266  if (is_array($value)) {
267  $configuration[$key] = $this->‪replaceLanguageFileReferences($value);
268  } elseif (is_string($value)) {
269  $configuration[$key] = $this->‪getLanguageService()->sL($value);
270  }
271  }
272  return $configuration;
273  }
274 
278  protected function ‪replaceAbsolutePathsToRelativeResourcesPath(array $configuration): array
279  {
280  foreach ($configuration as $key => $value) {
281  if (is_array($value)) {
282  $configuration[$key] = $this->‪replaceAbsolutePathsToRelativeResourcesPath($value);
283  } elseif (is_string($value) && ‪PathUtility::isExtensionPath(strtoupper($value))) {
284  $configuration[$key] = $this->‪resolveUrlPath($value);
285  }
286  }
287  return $configuration;
288  }
289 
293  protected function ‪resolveUrlPath(string $value): string
294  {
296  }
297 
304  protected function ‪prepareConfigurationForEditor(): array
305  {
306  // Ensure custom config is empty so nothing additional is loaded
307  // Of course this can be overridden by the editor configuration below
308  $configuration = [
309  'customConfig' => '',
310  ];
311 
312  if ($this->data['parameterArray']['fieldConf']['config']['readOnly'] ?? false) {
313  $configuration['readOnly'] = true;
314  }
315 
316  if (is_array($this->rteConfiguration['config'] ?? null)) {
317  $configuration = array_replace_recursive($configuration, $this->rteConfiguration['config']);
318  }
319 
320  $configuration = $this->eventDispatcher
321  ->dispatch(new BeforePrepareConfigurationForEditorEvent($configuration, $this->data))
322  ->getConfiguration();
323 
324  // Set the UI language of the editor if not hard-coded by the existing configuration
325  if (empty($configuration['language']) ||
326  (is_array($configuration['language']) && empty($configuration['language']['ui']))
327  ) {
328  $userLang = (string)($this->‪getBackendUser()->user['lang'] ?: 'en');
329  $configuration['language']['ui'] = $userLang === 'default' ? 'en' : $userLang;
330  } elseif (!is_array($configuration['language'])) {
331  $configuration['language'] = [
332  'ui' => $configuration['language'],
333  ];
334  }
335  $configuration['language']['content'] = $this->‪getLanguageIsoCodeOfContent();
336 
337  // Replace all label references
338  $configuration = $this->‪replaceLanguageFileReferences($configuration);
339  // Replace all paths
340  $configuration = $this->‪replaceAbsolutePathsToRelativeResourcesPath($configuration);
341 
342  // unless explicitly set, the debug mode is enabled in development context
343  if (!isset($configuration['debug'])) {
344  $configuration['debug'] = (‪$GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] ?? false) && ‪Environment::getContext()->isDevelopment();
345  }
346 
347  $configuration = $this->eventDispatcher
348  ->dispatch(new AfterPrepareConfigurationForEditorEvent($configuration, $this->data))
349  ->getConfiguration();
350 
351  return $configuration;
352  }
353 
354  protected function ‪sanitizeFieldId(string $itemFormElementName): string
355  {
356  $fieldId = (string)preg_replace('/[^a-zA-Z0-9_:-]/', '_', $itemFormElementName);
357  return htmlspecialchars((string)preg_replace('/^[^a-zA-Z]/', 'x', $fieldId));
358  }
359 
360  protected function ‪translationExists(string $language): bool
361  {
362  $fileName = GeneralUtility::getFileAbsFileName('EXT:rte_ckeditor/Resources/Public/Contrib/translations/' . $language . '.js');
363  return file_exists($fileName);
364  }
365 }
‪TYPO3\CMS\Core\Utility\PathUtility\isExtensionPath
‪static isExtensionPath(string $path)
Definition: PathUtility.php:117
‪TYPO3\CMS\Backend\Form\Element\AbstractFormElement\renderFieldInformation
‪array renderFieldInformation()
Definition: AbstractFormElement.php:73
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:27
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\translationExists
‪translationExists(string $language)
Definition: RichTextElement.php:357
‪TYPO3\CMS\RteCKEditor\Form\Element
‪TYPO3\CMS\Backend\Form\AbstractNode\mergeChildReturnIntoExistingResult
‪array mergeChildReturnIntoExistingResult(array $existing, array $childReturn, bool $mergeHtml=true)
Definition: AbstractNode.php:104
‪TYPO3\CMS\Backend\Form\Element\AbstractFormElement\getBackendUser
‪getBackendUser()
Definition: AbstractFormElement.php:461
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement
Definition: RichTextElement.php:38
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\getExtraPlugins
‪getExtraPlugins()
Definition: RichTextElement.php:217
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\replaceLanguageFileReferences
‪replaceLanguageFileReferences(array $configuration)
Definition: RichTextElement.php:260
‪TYPO3\CMS\Core\Page\JavaScriptModuleInstruction\create
‪static create(string $name, string $exportName=null)
Definition: JavaScriptModuleInstruction.php:47
‪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:37
‪TYPO3\CMS\Core\Localization\Locales
Definition: Locales.php:36
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\__construct
‪__construct(private readonly EventDispatcherInterface $eventDispatcher, private readonly UriBuilder $uriBuilder, private readonly Locales $locales,)
Definition: RichTextElement.php:78
‪TYPO3\CMS\Core\Page\JavaScriptModuleInstruction
Definition: JavaScriptModuleInstruction.php:23
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\resolveUrlPath
‪resolveUrlPath(string $value)
Definition: RichTextElement.php:290
‪TYPO3\CMS\Backend\Form\Element\AbstractFormElement\wrapWithFieldsetAndLegend
‪wrapWithFieldsetAndLegend(string $innerHTML)
Definition: AbstractFormElement.php:133
‪TYPO3\CMS\RteCKEditor\Form\Element\Event\AfterGetExternalPluginsEvent
Definition: AfterGetExternalPluginsEvent.php:24
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\replaceAbsolutePathsToRelativeResourcesPath
‪replaceAbsolutePathsToRelativeResourcesPath(array $configuration)
Definition: RichTextElement.php:275
‪TYPO3\CMS\Backend\Form\Element\AbstractFormElement\renderFieldControl
‪array renderFieldControl()
Definition: AbstractFormElement.php:89
‪TYPO3\CMS\Core\Utility\PathUtility\getPublicResourceWebPath
‪static getPublicResourceWebPath(string $resourcePath, bool $prefixWithSitePath=true)
Definition: PathUtility.php:97
‪TYPO3\CMS\Backend\Form\Element\AbstractFormElement\getLanguageService
‪getLanguageService()
Definition: AbstractFormElement.php:456
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:44
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\getLanguageIsoCodeOfContent
‪getLanguageIsoCodeOfContent()
Definition: RichTextElement.php:172
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\resolveCkEditorConfiguration
‪resolveCkEditorConfiguration()
Definition: RichTextElement.php:194
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\$defaultFieldInformation
‪array $defaultFieldInformation
Definition: RichTextElement.php:43
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\$defaultFieldWizard
‪array $defaultFieldWizard
Definition: RichTextElement.php:53
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Backend\Form\AbstractNode\getValidationDataAsJsonString
‪getValidationDataAsJsonString(array $config)
Definition: AbstractNode.php:133
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\render
‪render()
Definition: RichTextElement.php:89
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\prepareConfigurationForEditor
‪array prepareConfigurationForEditor()
Definition: RichTextElement.php:301
‪TYPO3\CMS\RteCKEditor\Form\Element\RichTextElement\sanitizeFieldId
‪sanitizeFieldId(string $itemFormElementName)
Definition: RichTextElement.php:351
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\RteCKEditor\Form\Element\Event\BeforePrepareConfigurationForEditorEvent
Definition: BeforePrepareConfigurationForEditorEvent.php:24
‪TYPO3\CMS\Backend\Form\Element\AbstractFormElement\renderFieldWizard
‪array renderFieldWizard()
Definition: AbstractFormElement.php:105
‪TYPO3\CMS\Core\Core\Environment\getContext
‪static getContext()
Definition: Environment.php:128
‪TYPO3\CMS\Backend\Form\AbstractNode\initializeResultArray
‪initializeResultArray()
Definition: AbstractNode.php:77