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