Completed
Push — master ( 02e81a...06188d )
by
unknown
17:11
created

RichTextElement::render()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 64
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 48
nc 4
nop 0
dl 0
loc 64
rs 9.1344
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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