[TASK] Update phpunit/phpunit to 8.3.3
[Packages/TYPO3.CMS.git] / typo3 / sysext / rte_ckeditor / Classes / Form / Element / RichTextElement.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\RteCKEditor\Form\Element;
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
18 use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
19 use TYPO3\CMS\Backend\Routing\UriBuilder;
20 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21 use TYPO3\CMS\Core\Localization\Locales;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Core\Utility\PathUtility;
24
25 /**
26 * Render rich text editor in FormEngine
27 * @internal This is a specific Backend FormEngine implementation and is not considered part of the Public TYPO3 API.
28 */
29 class RichTextElement extends AbstractFormElement
30 {
31 /**
32 * Default field information enabled for this element.
33 *
34 * @var array
35 */
36 protected $defaultFieldInformation = [
37 'tcaDescription' => [
38 'renderType' => 'tcaDescription',
39 ],
40 ];
41
42 /**
43 * Default field wizards enabled for this element.
44 *
45 * @var array
46 */
47 protected $defaultFieldWizard = [
48 'localizationStateSelector' => [
49 'renderType' => 'localizationStateSelector',
50 ],
51 'otherLanguageContent' => [
52 'renderType' => 'otherLanguageContent',
53 'after' => [
54 'localizationStateSelector'
55 ],
56 ],
57 'defaultLanguageDifferences' => [
58 'renderType' => 'defaultLanguageDifferences',
59 'after' => [
60 'otherLanguageContent',
61 ],
62 ],
63 ];
64
65 /**
66 * This property contains configuration related to the RTE
67 * But only the .editor configuration part
68 *
69 * @var array
70 */
71 protected $rteConfiguration = [];
72
73 /**
74 * Renders the ckeditor element
75 *
76 * @return array
77 * @throws \InvalidArgumentException
78 */
79 public function render(): array
80 {
81 $resultArray = $this->initializeResultArray();
82 $parameterArray = $this->data['parameterArray'];
83 $config = $parameterArray['fieldConf']['config'];
84
85 $fieldId = $this->sanitizeFieldId($parameterArray['itemFormElName']);
86 $itemFormElementName = $this->data['parameterArray']['itemFormElName'];
87
88 $value = $this->data['parameterArray']['itemFormElValue'] ?? '';
89
90 $fieldInformationResult = $this->renderFieldInformation();
91 $fieldInformationHtml = $fieldInformationResult['html'];
92 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
93
94 $fieldControlResult = $this->renderFieldControl();
95 $fieldControlHtml = $fieldControlResult['html'];
96 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
97
98 $fieldWizardResult = $this->renderFieldWizard();
99 $fieldWizardHtml = $fieldWizardResult['html'];
100 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
101
102 $attributes = [
103 'style' => 'display:none',
104 'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
105 'id' => $fieldId,
106 'name' => htmlspecialchars($itemFormElementName),
107 ];
108
109 $html = [];
110 $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
111 $html[] = $fieldInformationHtml;
112 $html[] = '<div class="form-control-wrap">';
113 $html[] = '<div class="form-wizards-wrap">';
114 $html[] = '<div class="form-wizards-element">';
115 $html[] = '<textarea ' . GeneralUtility::implodeAttributes($attributes, true) . '>';
116 $html[] = htmlspecialchars($value);
117 $html[] = '</textarea>';
118 $html[] = '</div>';
119 if (!empty($fieldControlHtml)) {
120 $html[] = '<div class="form-wizards-items-aside">';
121 $html[] = '<div class="btn-group">';
122 $html[] = $fieldControlHtml;
123 $html[] = '</div>';
124 $html[] = '</div>';
125 }
126 if (!empty($fieldWizardHtml)) {
127 $html[] = '<div class="form-wizards-items-bottom">';
128 $html[] = $fieldWizardHtml;
129 $html[] = '</div>';
130 }
131 $html[] = '</div>';
132 $html[] = '</div>';
133 $html[] = '</div>';
134
135 $resultArray['html'] = implode(LF, $html);
136
137 $this->rteConfiguration = $config['richtextConfiguration']['editor'];
138 $resultArray['requireJsModules'][] = [
139 'ckeditor' => $this->getCkEditorRequireJsModuleCode($fieldId)
140 ];
141
142 return $resultArray;
143 }
144
145 /**
146 * Determine the contents language iso code
147 *
148 * @return string
149 */
150 protected function getLanguageIsoCodeOfContent(): string
151 {
152 $currentLanguageUid = $this->data['databaseRow']['sys_language_uid'];
153 if (is_array($currentLanguageUid)) {
154 $currentLanguageUid = $currentLanguageUid[0];
155 }
156 $contentLanguageUid = (int)max($currentLanguageUid, 0);
157 if ($contentLanguageUid) {
158 $contentLanguage = $this->data['systemLanguageRows'][$currentLanguageUid]['iso'];
159 } else {
160 $contentLanguage = $this->rteConfiguration['config']['defaultContentLanguage'] ?? 'en_US';
161 $languageCodeParts = explode('_', $contentLanguage);
162 $contentLanguage = strtolower($languageCodeParts[0]) . ($languageCodeParts[1] ? '_' . strtoupper($languageCodeParts[1]) : '');
163 // Find the configured language in the list of localization locales
164 $locales = GeneralUtility::makeInstance(Locales::class);
165 // If not found, default to 'en'
166 if (!in_array($contentLanguage, $locales->getLocales(), true)) {
167 $contentLanguage = 'en';
168 }
169 }
170 return $contentLanguage;
171 }
172
173 /**
174 * Gets the JavaScript code for CKEditor module
175 * Compiles the configuration, and then adds plugins
176 *
177 * @param string $fieldId
178 * @return string
179 */
180 protected function getCkEditorRequireJsModuleCode(string $fieldId): string
181 {
182 $configuration = $this->prepareConfigurationForEditor();
183
184 $externalPlugins = '';
185 foreach ($this->getExtraPlugins() as $pluginName => $config) {
186 if (!empty($config['config']) && !empty($configuration[$pluginName])) {
187 $config['config'] = array_replace_recursive($config['config'], $configuration[$pluginName]);
188 }
189 $configuration[$pluginName] = $config['config'];
190 $configuration['extraPlugins'] .= ',' . $pluginName;
191
192 $externalPlugins .= 'CKEDITOR.plugins.addExternal(';
193 $externalPlugins .= GeneralUtility::quoteJSvalue($pluginName) . ',';
194 $externalPlugins .= GeneralUtility::quoteJSvalue($config['resource']) . ',';
195 $externalPlugins .= '\'\');';
196 }
197
198 $jsonConfiguration = json_encode($configuration);
199
200 // Make a hash of the configuration and append it to CKEDITOR.timestamp
201 // This will mitigate browser caching issue when plugins are updated
202 $configurationHash = GeneralUtility::shortMD5($jsonConfiguration);
203
204 return 'function(CKEDITOR) {
205 CKEDITOR.timestamp += "-' . $configurationHash . '";
206 ' . $externalPlugins . '
207 require([\'jquery\', \'TYPO3/CMS/Backend/FormEngine\'], function($, FormEngine) {
208 $(function(){
209 CKEDITOR.replace("' . $fieldId . '", ' . $jsonConfiguration . ');
210 CKEDITOR.instances["' . $fieldId . '"].on(\'change\', function() {
211 CKEDITOR.instances["' . $fieldId . '"].updateElement();
212 FormEngine.Validation.validate();
213 FormEngine.Validation.markFieldAsChanged($(\'#' . $fieldId . '\'));
214 });
215 $(document).on(\'inline:sorting-changed\', function() {
216 CKEDITOR.instances["' . $fieldId . '"].destroy();
217 CKEDITOR.replace("' . $fieldId . '", ' . $jsonConfiguration . ');
218 });
219 $(document).on(\'flexform:sorting-changed\', function() {
220 CKEDITOR.instances["' . $fieldId . '"].destroy();
221 CKEDITOR.replace("' . $fieldId . '", ' . $jsonConfiguration . ');
222 });
223 });
224 });
225 }';
226 }
227
228 /**
229 * Get configuration of external/additional plugins
230 *
231 * @return array
232 */
233 protected function getExtraPlugins(): array
234 {
235 $urlParameters = [
236 'P' => [
237 'table' => $this->data['tableName'],
238 'uid' => $this->data['databaseRow']['uid'],
239 'fieldName' => $this->data['fieldName'],
240 'recordType' => $this->data['recordTypeValue'],
241 'pid' => $this->data['effectivePid'],
242 'richtextConfigurationName' => $this->data['parameterArray']['fieldConf']['config']['richtextConfigurationName']
243 ]
244 ];
245
246 $pluginConfiguration = [];
247 if (isset($this->rteConfiguration['externalPlugins']) && is_array($this->rteConfiguration['externalPlugins'])) {
248 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
249 foreach ($this->rteConfiguration['externalPlugins'] as $pluginName => $configuration) {
250 $pluginConfiguration[$pluginName] = [
251 'resource' => $this->resolveUrlPath($configuration['resource'])
252 ];
253 unset($configuration['resource']);
254
255 if ($configuration['route']) {
256 $configuration['routeUrl'] = (string)$uriBuilder->buildUriFromRoute($configuration['route'], $urlParameters);
257 }
258
259 $pluginConfiguration[$pluginName]['config'] = $configuration;
260 }
261 }
262 return $pluginConfiguration;
263 }
264
265 /**
266 * Add configuration to replace LLL: references with the translated value
267 * @param array $configuration
268 *
269 * @return array
270 */
271 protected function replaceLanguageFileReferences(array $configuration): array
272 {
273 foreach ($configuration as $key => $value) {
274 if (is_array($value)) {
275 $configuration[$key] = $this->replaceLanguageFileReferences($value);
276 } elseif (is_string($value) && stripos($value, 'LLL:') === 0) {
277 $configuration[$key] = $this->getLanguageService()->sL($value);
278 }
279 }
280 return $configuration;
281 }
282
283 /**
284 * Add configuration to replace absolute EXT: paths with relative ones
285 * @param array $configuration
286 *
287 * @return array
288 */
289 protected function replaceAbsolutePathsToRelativeResourcesPath(array $configuration): array
290 {
291 foreach ($configuration as $key => $value) {
292 if (is_array($value)) {
293 $configuration[$key] = $this->replaceAbsolutePathsToRelativeResourcesPath($value);
294 } elseif (is_string($value) && stripos($value, 'EXT:') === 0) {
295 $configuration[$key] = $this->resolveUrlPath($value);
296 }
297 }
298 return $configuration;
299 }
300
301 /**
302 * Resolves an EXT: syntax file to an absolute web URL
303 *
304 * @param string $value
305 * @return string
306 */
307 protected function resolveUrlPath(string $value): string
308 {
309 $value = GeneralUtility::getFileAbsFileName($value);
310 return PathUtility::getAbsoluteWebPath($value);
311 }
312
313 /**
314 * Compiles the configuration set from the outside
315 * to have it easily injected into the CKEditor.
316 *
317 * @return array the configuration
318 */
319 protected function prepareConfigurationForEditor(): array
320 {
321 // Ensure custom config is empty so nothing additional is loaded
322 // Of course this can be overridden by the editor configuration below
323 $configuration = [
324 'customConfig' => '',
325 ];
326
327 if (is_array($this->rteConfiguration['config'])) {
328 $configuration = array_replace_recursive($configuration, $this->rteConfiguration['config']);
329 }
330 // Set the UI language of the editor if not hard-coded by the existing configuration
331 if (empty($configuration['language'])) {
332 $configuration['language'] = $this->getBackendUser()->uc['lang'] ?: ($this->getBackendUser()->user['lang'] ?: 'en');
333 }
334 $configuration['contentsLanguage'] = $this->getLanguageIsoCodeOfContent();
335
336 // Replace all label references
337 $configuration = $this->replaceLanguageFileReferences($configuration);
338 // Replace all paths
339 $configuration = $this->replaceAbsolutePathsToRelativeResourcesPath($configuration);
340
341 // there are some places where we define an array, but it needs to be a list in order to work
342 if (is_array($configuration['extraPlugins'])) {
343 $configuration['extraPlugins'] = implode(',', $configuration['extraPlugins']);
344 }
345 if (is_array($configuration['removePlugins'])) {
346 $configuration['removePlugins'] = implode(',', $configuration['removePlugins']);
347 }
348 if (is_array($configuration['removeButtons'])) {
349 $configuration['removeButtons'] = implode(',', $configuration['removeButtons']);
350 }
351
352 return $configuration;
353 }
354
355 /**
356 * @param string $itemFormElementName
357 * @return string
358 */
359 protected function sanitizeFieldId(string $itemFormElementName): string
360 {
361 $fieldId = preg_replace('/[^a-zA-Z0-9_:.-]/', '_', $itemFormElementName);
362 return htmlspecialchars(preg_replace('/^[^a-zA-Z]/', 'x', $fieldId));
363 }
364
365 /**
366 * @return BackendUserAuthentication
367 */
368 protected function getBackendUser()
369 {
370 return $GLOBALS['BE_USER'];
371 }
372 }