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