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