[BUGFIX] CKEditor: UI language should respect BE user language
[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 */
28 class RichTextElement extends AbstractFormElement
29 {
30 /**
31 * Default field wizards enabled for this element.
32 *
33 * @var array
34 */
35 protected $defaultFieldWizard = [
36 'otherLanguageContent' => [
37 'renderType' => 'otherLanguageContent',
38 ],
39 'defaultLanguageDifferences' => [
40 'renderType' => 'defaultLanguageDifferences',
41 'after' => [
42 'otherLanguageContent',
43 ],
44 ],
45 ];
46
47 /**
48 * This property contains configuration related to the RTE
49 * But only the .editor configuration part
50 *
51 * @var array
52 */
53 protected $rteConfiguration = [];
54
55 /**
56 * Renders the ckeditor element
57 *
58 * @return array
59 * @throws \InvalidArgumentException
60 */
61 public function render() : array
62 {
63 $resultArray = $this->initializeResultArray();
64 $parameterArray = $this->data['parameterArray'];
65 $config = $parameterArray['fieldConf']['config'];
66
67 $fieldId = $this->sanitizeFieldId($parameterArray['itemFormElName']);
68 $itemFormElementName = $this->data['parameterArray']['itemFormElName'];
69
70 $value = $this->data['parameterArray']['itemFormElValue'] ?? '';
71
72 $legacyWizards = $this->renderWizards();
73 $legacyFieldControlHtml = implode(LF, $legacyWizards['fieldControl']);
74 $legacyFieldWizardHtml = implode(LF, $legacyWizards['fieldWizard']);
75
76 $fieldInformationResult = $this->renderFieldInformation();
77 $fieldInformationHtml = $fieldInformationResult['html'];
78 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
79
80 $fieldControlResult = $this->renderFieldControl();
81 $fieldControlHtml = $legacyFieldControlHtml . $fieldControlResult['html'];
82 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
83
84 $fieldWizardResult = $this->renderFieldWizard();
85 $fieldWizardHtml = $legacyFieldWizardHtml . $fieldWizardResult['html'];
86 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
87
88 $attributes = [
89 'style' => 'display:none',
90 'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
91 'id' => $fieldId,
92 'name' => htmlspecialchars($itemFormElementName),
93 ];
94
95 $html = [];
96 $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
97 $html[] = $fieldInformationHtml;
98 $html[] = '<div class="form-control-wrap">';
99 $html[] = '<div class="form-wizards-wrap">';
100 $html[] = '<div class="form-wizards-element">';
101 $html[] = '<textarea ' . GeneralUtility::implodeAttributes($attributes, true) . '>';
102 $html[] = htmlspecialchars($value);
103 $html[] = '</textarea>';
104 $html[] = '</div>';
105 $html[] = '<div class="form-wizards-items-aside">';
106 $html[] = '<div class="btn-group">';
107 $html[] = $fieldControlHtml;
108 $html[] = '</div>';
109 $html[] = '</div>';
110 $html[] = '<div class="form-wizards-items-bottom">';
111 $html[] = $fieldWizardHtml;
112 $html[] = '</div>';
113 $html[] = '</div>';
114 $html[] = '</div>';
115 $html[] = '</div>';
116
117 $resultArray['html'] = implode(LF, $html);
118
119 $this->rteConfiguration = $config['richtextConfiguration']['editor'];
120 $resultArray['requireJsModules'] = [];
121 $resultArray['requireJsModules'][] = [
122 'ckeditor' => $this->getCkEditorRequireJsModuleCode($fieldId)
123 ];
124
125 return $resultArray;
126 }
127
128 /**
129 * Determine the contents language iso code
130 *
131 * @return string
132 */
133 protected function getLanguageIsoCodeOfContent(): string
134 {
135 $currentLanguageUid = $this->data['databaseRow']['sys_language_uid'];
136 if (is_array($currentLanguageUid)) {
137 $currentLanguageUid = $currentLanguageUid[0];
138 }
139 $contentLanguageUid = (int)max($currentLanguageUid, 0);
140 if ($contentLanguageUid) {
141 $contentLanguage = $this->data['systemLanguageRows'][$currentLanguageUid]['iso'];
142 } else {
143 $contentLanguage = $this->rteConfiguration['config']['defaultContentLanguage'] ?? 'en_US';
144 $languageCodeParts = explode('_', $contentLanguage);
145 $contentLanguage = strtolower($languageCodeParts[0]) . ($languageCodeParts[1] ? '_' . strtoupper($languageCodeParts[1]) : '');
146 // Find the configured language in the list of localization locales
147 $locales = GeneralUtility::makeInstance(Locales::class);
148 // If not found, default to 'en'
149 if (!in_array($contentLanguage, $locales->getLocales(), true)) {
150 $contentLanguage = 'en';
151 }
152 }
153 return $contentLanguage;
154 }
155
156 /**
157 * Gets the JavaScript code for CKEditor module
158 * Compiles the configuration, and then adds plugins
159 *
160 * @param string $fieldId
161 * @return string
162 */
163 protected function getCkEditorRequireJsModuleCode(string $fieldId) : string
164 {
165 $configuration = $this->prepareConfigurationForEditor();
166
167 $externalPlugins = '';
168 foreach ($this->getExtraPlugins() as $pluginName => $config) {
169 $configuration[$pluginName] = $config['config'];
170 $configuration['extraPlugins'] .= ',' . $pluginName;
171
172 $externalPlugins .= 'CKEDITOR.plugins.addExternal(';
173 $externalPlugins .= GeneralUtility::quoteJSvalue($pluginName) . ',';
174 $externalPlugins .= GeneralUtility::quoteJSvalue($config['resource']) . ',';
175 $externalPlugins .= '\'\');';
176 }
177
178 return 'function(CKEDITOR) {
179 ' . $externalPlugins . '
180 CKEDITOR.replace("' . $fieldId . '", ' . json_encode($configuration) . ');
181 }';
182 }
183
184 /**
185 * Get configuration of external/additional plugins
186 *
187 * @return array
188 */
189 protected function getExtraPlugins(): array
190 {
191 $urlParameters = [
192 'P' => [
193 'table' => $this->data['tableName'],
194 'uid' => $this->data['databaseRow']['uid'],
195 'fieldName' => $this->data['fieldName'],
196 'recordType' => $this->data['recordTypeValue'],
197 'pid' => $this->data['effectivePid'],
198 ]
199 ];
200
201 $pluginConfiguration = [];
202 if (isset($this->rteConfiguration['externalPlugins']) && is_array($this->rteConfiguration['externalPlugins'])) {
203 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
204 foreach ($this->rteConfiguration['externalPlugins'] as $pluginName => $configuration) {
205 $pluginConfiguration[$pluginName] = [
206 'resource' => $this->resolveUrlPath($configuration['resource'])
207 ];
208 unset($configuration['resource']);
209
210 if ($configuration['route']) {
211 $configuration['routeUrl'] = (string)$uriBuilder->buildUriFromRoute($configuration['route'], $urlParameters);
212 }
213
214 $pluginConfiguration[$pluginName]['config'] = $configuration;
215 }
216 }
217 return $pluginConfiguration;
218 }
219
220 /**
221 * Add configuration to replace absolute EXT: paths with relative ones
222 * @param array $configuration
223 *
224 * @return array
225 */
226 protected function replaceAbsolutePathsToRelativeResourcesPath(array $configuration): array
227 {
228 foreach ($configuration as $key => $value) {
229 if (is_array($value)) {
230 $configuration[$key] = $this->replaceAbsolutePathsToRelativeResourcesPath($value);
231 } elseif (is_string($value) && substr($value, 0, 4) === 'EXT:') {
232 $configuration[$key] = $this->resolveUrlPath($value);
233 }
234 }
235 return $configuration;
236 }
237
238 /**
239 * Resolves an EXT: syntax file to an absolute web URL
240 *
241 * @param string $value
242 * @return string
243 */
244 protected function resolveUrlPath(string $value): string
245 {
246 $value = GeneralUtility::getFileAbsFileName($value);
247 return PathUtility::getAbsoluteWebPath($value);
248 }
249
250 /**
251 * Compiles the configuration set from the outside
252 * to have it easily injected into the CKEditor.
253 *
254 * @return array the configuration
255 */
256 protected function prepareConfigurationForEditor(): array
257 {
258 // Ensure custom config is empty so nothing additional is loaded
259 // Of course this can be overridden by the editor configuration below
260 $configuration = [
261 'customConfig' => '',
262 ];
263
264 if (is_array($this->rteConfiguration['config'])) {
265 $configuration = array_replace_recursive($configuration, $this->rteConfiguration['config']);
266 }
267 // Set the UI language of the editor if not hard-coded by the existing configuration
268 if (empty($configuration['language'])) {
269 $configuration['language'] = $this->getBackendUser()->uc['lang'] ?: ($this->getBackendUser()->user['lang'] ?: 'en');
270 }
271 $configuration['contentsLanguage'] = $this->getLanguageIsoCodeOfContent();
272
273 // replace all paths
274 $configuration = $this->replaceAbsolutePathsToRelativeResourcesPath($configuration);
275
276 // there are some places where we define an array, but it needs to be a list in order to work
277 if (is_array($configuration['extraPlugins'])) {
278 $configuration['extraPlugins'] = implode(',', $configuration['extraPlugins']);
279 }
280 if (is_array($configuration['removePlugins'])) {
281 $configuration['removePlugins'] = implode(',', $configuration['removePlugins']);
282 }
283 if (is_array($configuration['removeButtons'])) {
284 $configuration['removeButtons'] = implode(',', $configuration['removeButtons']);
285 }
286
287 return $configuration;
288 }
289
290 /**
291 * @param string $itemFormElementName
292 * @return string
293 */
294 protected function sanitizeFieldId(string $itemFormElementName): string
295 {
296 $fieldId = preg_replace('/[^a-zA-Z0-9_:.-]/', '_', $itemFormElementName);
297 return htmlspecialchars(preg_replace('/^[^a-zA-Z]/', 'x', $fieldId));
298 }
299
300 /**
301 * @return BackendUserAuthentication
302 */
303 protected function getBackendUser()
304 {
305 return $GLOBALS['BE_USER'];
306 }
307 }