1<?php
2declare(strict_types = 1);
3namespace TYPO3\CMS\Backend\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
18use TYPO3\CMS\Backend\Form\NodeFactory;
19use TYPO3\CMS\Backend\Routing\UriBuilder;
20use TYPO3\CMS\Core\Imaging\ImageManipulation\Area;
21use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
22use TYPO3\CMS\Core\Imaging\ImageManipulation\InvalidConfigurationException;
23use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
24use TYPO3\CMS\Core\Resource\File;
25use TYPO3\CMS\Core\Resource\ResourceFactory;
26use TYPO3\CMS\Core\Utility\GeneralUtility;
27use TYPO3\CMS\Core\Utility\MathUtility;
28use TYPO3\CMS\Core\Utility\StringUtility;
29use TYPO3\CMS\Fluid\View\StandaloneView;
30
31/**
32 * Generation of image manipulation FormEngine element.
33 * This is typically used in FAL relations to cut images.
34 */
35class ImageManipulationElement extends AbstractFormElement
36{
37    /**
38     * @var string
39     */
40    private $wizardRouteName = 'ajax_wizard_image_manipulation';
41
42    /**
43     * Default element configuration
44     *
45     * @var array
46     */
47    protected static $defaultConfig = [
48        'file_field' => 'uid_local',
49        'allowedExtensions' => null, // default: $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
50        'cropVariants' => [
51            'default' => [
52                'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.crop_variant.default',
53                'allowedAspectRatios' => [
54                    '16:9' => [
55                        'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.16_9',
56                        'value' => 16 / 9
57                    ],
58                    '3:2' => [
59                        'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.3_2',
60                        'value' => 3 / 2
61                    ],
62                    '4:3' => [
63                        'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.4_3',
64                        'value' => 4 / 3
65                    ],
66                    '1:1' => [
67                        'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.1_1',
68                        'value' => 1.0
69                    ],
70                    'NaN' => [
71                        'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.free',
72                        'value' => 0.0
73                    ],
74                ],
75                'selectedRatio' => 'NaN',
76                'cropArea' => [
77                    'x' => 0.0,
78                    'y' => 0.0,
79                    'width' => 1.0,
80                    'height' => 1.0,
81                ],
82            ],
83        ]
84    ];
85
86    /**
87     * Default field information enabled for this element.
88     *
89     * @var array
90     */
91    protected $defaultFieldInformation = [
92        'tcaDescription' => [
93            'renderType' => 'tcaDescription',
94        ],
95    ];
96
97    /**
98     * Default field wizards enabled for this element.
99     *
100     * @var array
101     */
102    protected $defaultFieldWizard = [
103        'localizationStateSelector' => [
104            'renderType' => 'localizationStateSelector',
105        ],
106        'otherLanguageContent' => [
107            'renderType' => 'otherLanguageContent',
108            'after' => [
109                'localizationStateSelector'
110            ],
111        ],
112        'defaultLanguageDifferences' => [
113            'renderType' => 'defaultLanguageDifferences',
114            'after' => [
115                'otherLanguageContent',
116            ],
117        ],
118    ];
119
120    /**
121     * @var StandaloneView
122     */
123    protected $templateView;
124
125    /**
126     * @var UriBuilder
127     */
128    protected $uriBuilder;
129
130    /**
131     * @param NodeFactory $nodeFactory
132     * @param array $data
133     */
134    public function __construct(NodeFactory $nodeFactory, array $data)
135    {
136        parent::__construct($nodeFactory, $data);
137        // Would be great, if we could inject the view here, but since the constructor is in the interface, we can't
138        $this->templateView = GeneralUtility::makeInstance(StandaloneView::class);
139        $this->templateView->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts/')]);
140        $this->templateView->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials/ImageManipulation/')]);
141        $this->templateView->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/ImageManipulation/ImageManipulationElement.html'));
142        $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
143    }
144
145    /**
146     * This will render an imageManipulation field
147     *
148     * @return array As defined in initializeResultArray() of AbstractNode
149     * @throws \TYPO3\CMS\Core\Imaging\ImageManipulation\InvalidConfigurationException
150     */
151    public function render()
152    {
153        $resultArray = $this->initializeResultArray();
154        $parameterArray = $this->data['parameterArray'];
155        $config = $this->populateConfiguration($parameterArray['fieldConf']['config']);
156
157        $file = $this->getFile($this->data['databaseRow'], $config['file_field']);
158        if (!$file) {
159            // Early return in case we do not find a file
160            return $resultArray;
161        }
162
163        $config = $this->processConfiguration($config, $parameterArray['itemFormElValue'], $file);
164
165        $fieldInformationResult = $this->renderFieldInformation();
166        $fieldInformationHtml = $fieldInformationResult['html'];
167        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
168
169        $fieldControlResult = $this->renderFieldControl();
170        $fieldControlHtml = $fieldControlResult['html'];
171        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
172
173        $fieldWizardResult = $this->renderFieldWizard();
174        $fieldWizardHtml = $fieldWizardResult['html'];
175        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
176
177        $arguments = [
178            'fieldInformation' => $fieldInformationHtml,
179            'fieldControl' => $fieldControlHtml,
180            'fieldWizard' => $fieldWizardHtml,
181            'isAllowedFileExtension' => in_array(strtolower($file->getExtension()), GeneralUtility::trimExplode(',', strtolower($config['allowedExtensions'])), true),
182            'image' => $file,
183            'formEngine' => [
184                'field' => [
185                    'value' => $parameterArray['itemFormElValue'],
186                    'name' => $parameterArray['itemFormElName']
187                ],
188                'validation' => '[]'
189            ],
190            'config' => $config,
191            'wizardUri' => $this->getWizardUri(),
192            'wizardPayload' => json_encode($this->getWizardPayload($config['cropVariants'], $file)),
193            'previewUrl' => $this->getPreviewUrl($this->data['databaseRow'], $file),
194        ];
195
196        if ($arguments['isAllowedFileExtension']) {
197            $resultArray['requireJsModules'][] = [
198                'TYPO3/CMS/Backend/ImageManipulation' => 'function (ImageManipulation) {top.require(["cropper"], function() { ImageManipulation.initializeTrigger(); }); }'
199            ];
200            $arguments['formEngine']['field']['id'] = StringUtility::getUniqueId('formengine-image-manipulation-');
201            if (GeneralUtility::inList($config['eval'], 'required')) {
202                $arguments['formEngine']['validation'] = $this->getValidationDataAsJsonString(['required' => true]);
203            }
204        }
205        $this->templateView->assignMultiple($arguments);
206        $resultArray['html'] = $this->templateView->render();
207
208        return $resultArray;
209    }
210
211    /**
212     * Get file object
213     *
214     * @param array $row
215     * @param string $fieldName
216     * @return File|null
217     */
218    protected function getFile(array $row, $fieldName)
219    {
220        $file = null;
221        $fileUid = !empty($row[$fieldName]) ? $row[$fieldName] : null;
222        if (is_array($fileUid) && isset($fileUid[0]['uid'])) {
223            $fileUid = $fileUid[0]['uid'];
224        }
225        if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
226            try {
227                $file = ResourceFactory::getInstance()->getFileObject($fileUid);
228            } catch (FileDoesNotExistException $e) {
229            } catch (\InvalidArgumentException $e) {
230            }
231        }
232        return $file;
233    }
234
235    /**
236     * @param array $databaseRow
237     * @param File $file
238     * @return string
239     */
240    protected function getPreviewUrl(array $databaseRow, File $file): string
241    {
242        $previewUrl = '';
243        // Hook to generate a preview URL
244        $hookParameters = [
245            'databaseRow' => $databaseRow,
246            'file' => $file,
247            'previewUrl' => $previewUrl,
248        ];
249        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Backend/Form/Element/ImageManipulationElement']['previewUrl'] ?? [] as $listener) {
250            $previewUrl = GeneralUtility::callUserFunction($listener, $hookParameters, $this);
251        }
252        return $previewUrl;
253    }
254
255    /**
256     * @param array $baseConfiguration
257     * @return array
258     * @throws InvalidConfigurationException
259     */
260    protected function populateConfiguration(array $baseConfiguration)
261    {
262        $defaultConfig = self::$defaultConfig;
263
264        // If ratios are set do not add default options
265        if (isset($baseConfiguration['cropVariants'])) {
266            unset($defaultConfig['cropVariants']);
267        }
268
269        $config = array_replace_recursive($defaultConfig, $baseConfiguration);
270
271        if (!is_array($config['cropVariants'])) {
272            throw new InvalidConfigurationException('Crop variants configuration must be an array', 1485377267);
273        }
274
275        $cropVariants = [];
276        foreach ($config['cropVariants'] as $id => $cropVariant) {
277            // Ignore disabled crop variants
278            if (!empty($cropVariant['disabled'])) {
279                continue;
280            }
281            // Enforce a crop area (default is full image)
282            if (empty($cropVariant['cropArea'])) {
283                $cropVariant['cropArea'] = Area::createEmpty()->asArray();
284            }
285            $cropVariants[$id] = $cropVariant;
286        }
287
288        $config['cropVariants'] = $cropVariants;
289
290        // By default we allow all image extensions that can be handled by the GFX functionality
291        if ($config['allowedExtensions'] === null) {
292            $config['allowedExtensions'] = $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'];
293        }
294        return $config;
295    }
296
297    /**
298     * @param array $config
299     * @param string $elementValue
300     * @param File $file
301     * @return array
302     * @throws \TYPO3\CMS\Core\Imaging\ImageManipulation\InvalidConfigurationException
303     */
304    protected function processConfiguration(array $config, string &$elementValue, File $file)
305    {
306        $cropVariantCollection = CropVariantCollection::create($elementValue, $config['cropVariants']);
307        if (empty($config['readOnly']) && !empty($file->getProperty('width'))) {
308            $cropVariantCollection = $cropVariantCollection->applyRatioRestrictionToSelectedCropArea($file);
309            $elementValue = (string)$cropVariantCollection;
310        }
311        $config['cropVariants'] = $cropVariantCollection->asArray();
312        $config['allowedExtensions'] = implode(', ', GeneralUtility::trimExplode(',', $config['allowedExtensions'], true));
313        return $config;
314    }
315
316    /**
317     * @return string
318     */
319    protected function getWizardUri(): string
320    {
321        return (string)$this->uriBuilder->buildUriFromRoute($this->wizardRouteName);
322    }
323
324    /**
325     * @param array $cropVariants
326     * @param File $image
327     * @return array
328     */
329    protected function getWizardPayload(array $cropVariants, File $image): array
330    {
331        $arguments = [
332            'cropVariants' => $cropVariants,
333            'image' => $image->getUid(),
334        ];
335        $uriArguments['arguments'] = json_encode($arguments);
336        $uriArguments['signature'] = GeneralUtility::hmac($uriArguments['arguments'], $this->wizardRouteName);
337
338        return $uriArguments;
339    }
340}
341