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