1<?php 2declare(strict_types = 1); 3namespace TYPO3\CMS\Form\Domain\Finishers; 4 5/* 6 * This file is part of the TYPO3 CMS project. 7 * 8 * It originated from the Neos.Form package (www.neos.io) 9 * 10 * It is free software; you can redistribute it and/or modify it under 11 * the terms of the GNU General Public License, either version 2 12 * of the License, or any later version. 13 * 14 * For the full copyright and license information, please read the 15 * LICENSE.txt file that was distributed with this source code. 16 * 17 * The TYPO3 project - inspiring people to share! 18 */ 19 20use TYPO3\CMS\Core\Utility\ArrayUtility; 21use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException; 22use TYPO3\CMS\Extbase\Reflection\ObjectAccess; 23use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException; 24use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; 25use TYPO3\CMS\Form\Service\TranslationService; 26use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; 27 28/** 29 * Finisher base class. 30 * 31 * Scope: frontend 32 * **This class is meant to be sub classed by developers** 33 */ 34abstract class AbstractFinisher implements FinisherInterface 35{ 36 37 /** 38 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface 39 */ 40 protected $objectManager; 41 42 /** 43 * @var string 44 */ 45 protected $finisherIdentifier = ''; 46 47 /** 48 * @var string 49 */ 50 protected $shortFinisherIdentifier = ''; 51 52 /** 53 * The options which have been set from the outside. Instead of directly 54 * accessing them, you should rather use parseOption(). 55 * 56 * @var array 57 */ 58 protected $options = []; 59 60 /** 61 * These are the default options of the finisher. 62 * Override them in your concrete implementation. 63 * Default options should not be changed from "outside" 64 * 65 * @var array 66 */ 67 protected $defaultOptions = []; 68 69 /** 70 * @var \TYPO3\CMS\Form\Domain\Finishers\FinisherContext 71 */ 72 protected $finisherContext; 73 74 /** 75 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager 76 * @internal 77 */ 78 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager) 79 { 80 $this->objectManager = $objectManager; 81 } 82 83 /** 84 * @param string $finisherIdentifier The identifier for this finisher 85 */ 86 public function __construct(string $finisherIdentifier = '') 87 { 88 if (empty($finisherIdentifier)) { 89 $this->finisherIdentifier = (new \ReflectionClass($this))->getShortName(); 90 } else { 91 $this->finisherIdentifier = $finisherIdentifier; 92 } 93 94 $this->shortFinisherIdentifier = preg_replace('/Finisher$/', '', $this->finisherIdentifier); 95 } 96 97 /** 98 * @return string 99 */ 100 public function getFinisherIdentifier(): string 101 { 102 return $this->finisherIdentifier; 103 } 104 105 /** 106 * @param array $options configuration options in the format ['option1' => 'value1', 'option2' => 'value2', ...] 107 */ 108 public function setOptions(array $options) 109 { 110 $this->options = $options; 111 } 112 113 /** 114 * Sets a single finisher option (@see setOptions()) 115 * 116 * @param string $optionName name of the option to be set 117 * @param mixed $optionValue value of the option 118 */ 119 public function setOption(string $optionName, $optionValue) 120 { 121 $this->options[$optionName] = $optionValue; 122 } 123 124 /** 125 * Executes the finisher 126 * 127 * @param FinisherContext $finisherContext The Finisher context that contains the current Form Runtime and Response 128 * @return string|null 129 */ 130 final public function execute(FinisherContext $finisherContext) 131 { 132 $this->finisherContext = $finisherContext; 133 134 if (!$this->isEnabled()) { 135 return null; 136 } 137 138 return $this->executeInternal(); 139 } 140 141 /** 142 * This method is called in the concrete finisher whenever self::execute() is called. 143 * 144 * Override and fill with your own implementation! 145 * 146 * @return string|null 147 */ 148 abstract protected function executeInternal(); 149 150 /** 151 * Read the option called $optionName from $this->options, and parse {...} 152 * as object accessors. 153 * 154 * Then translate the value. 155 * 156 * If $optionName was not found, the corresponding default option is returned (from $this->defaultOptions) 157 * 158 * @param string $optionName 159 * @return string|array|null 160 */ 161 protected function parseOption(string $optionName) 162 { 163 if ($optionName === 'translation') { 164 return null; 165 } 166 167 try { 168 $optionValue = ArrayUtility::getValueByPath($this->options, $optionName, '.'); 169 } catch (MissingArrayPathException $exception) { 170 $optionValue = null; 171 } 172 try { 173 $defaultValue = ArrayUtility::getValueByPath($this->defaultOptions, $optionName, '.'); 174 } catch (MissingArrayPathException $exception) { 175 $defaultValue = null; 176 } 177 178 if ($optionValue === null && $defaultValue !== null) { 179 $optionValue = $defaultValue; 180 } 181 182 if ($optionValue === null) { 183 return null; 184 } 185 186 if (!is_string($optionValue) && !is_array($optionValue)) { 187 return $optionValue; 188 } 189 190 $formRuntime = $this->finisherContext->getFormRuntime(); 191 $optionValue = $this->substituteRuntimeReferences($optionValue, $formRuntime); 192 193 if (is_string($optionValue)) { 194 $translationOptions = isset($this->options['translation']) && \is_array($this->options['translation']) 195 ? $this->options['translation'] 196 : []; 197 198 $optionValue = $this->translateFinisherOption( 199 $optionValue, 200 $formRuntime, 201 $optionName, 202 $optionValue, 203 $translationOptions 204 ); 205 206 $optionValue = $this->substituteRuntimeReferences($optionValue, $formRuntime); 207 } 208 209 if (empty($optionValue)) { 210 if ($defaultValue !== null) { 211 $optionValue = $defaultValue; 212 } 213 } 214 return $optionValue; 215 } 216 217 /** 218 * Wraps TranslationService::translateFinisherOption to recursively 219 * invoke all array items of resolved form state values or nested 220 * finisher option configuration settings. 221 * 222 * @param string|array $subject 223 * @param FormRuntime $formRuntime 224 * @param string $optionName 225 * @param string|array $optionValue 226 * @param array $translationOptions 227 * @return array|string 228 */ 229 protected function translateFinisherOption( 230 $subject, 231 FormRuntime $formRuntime, 232 string $optionName, 233 $optionValue, 234 array $translationOptions 235 ) { 236 if (is_array($subject)) { 237 foreach ($subject as $key => $value) { 238 $subject[$key] = $this->translateFinisherOption( 239 $value, 240 $formRuntime, 241 $optionName . '.' . $value, 242 $value, 243 $translationOptions 244 ); 245 } 246 return $subject; 247 } 248 249 return TranslationService::getInstance()->translateFinisherOption( 250 $formRuntime, 251 $this->finisherIdentifier, 252 $optionName, 253 $optionValue, 254 $translationOptions 255 ); 256 } 257 258 /** 259 * You can encapsulate a option value with {}. 260 * This enables you to access every getable property from the 261 * TYPO3\CMS\Form\Domain\Runtime\FormRuntime. 262 * 263 * For example: {formState.formValues.<elemenIdentifier>} 264 * or {<elemenIdentifier>} 265 * 266 * Both examples are equal to "$formRuntime->getFormState()->getFormValues()[<elemenIdentifier>]" 267 * There is a special option value '{__currentTimestamp}'. 268 * This will be replaced with the current timestamp. 269 * 270 * @param string|array $needle 271 * @param FormRuntime $formRuntime 272 * @return mixed 273 */ 274 protected function substituteRuntimeReferences($needle, FormRuntime $formRuntime) 275 { 276 // neither array nor string, directly return 277 if (!is_array($needle) && !is_string($needle)) { 278 return $needle; 279 } 280 281 // resolve (recursively) all array items 282 if (is_array($needle)) { 283 return array_map( 284 function ($item) use ($formRuntime) { 285 return $this->substituteRuntimeReferences($item, $formRuntime); 286 }, 287 $needle 288 ); 289 } 290 291 // substitute one(!) variable in string which either could result 292 // again in a string or an array representing multiple values 293 if (preg_match('/^{([^}]+)}$/', $needle, $matches)) { 294 return $this->resolveRuntimeReference( 295 $matches[1], 296 $formRuntime 297 ); 298 } 299 300 // in case string contains more than just one variable or just a static 301 // value that does not need to be substituted at all, candidates are: 302 // * "prefix{variable}suffix 303 // * "{variable-1},{variable-2}" 304 // * "some static value" 305 // * mixed cases of the above 306 return preg_replace_callback( 307 '/{([^}]+)}/', 308 function ($matches) use ($formRuntime) { 309 $value = $this->resolveRuntimeReference( 310 $matches[1], 311 $formRuntime 312 ); 313 314 // substitute each match by returning the resolved value 315 if (!is_array($value)) { 316 return $value; 317 } 318 319 // now the resolve value is an array that shall substitute 320 // a variable in a string that probably is not the only one 321 // or is wrapped with other static string content (see above) 322 // ... which is just not possible 323 throw new FinisherException( 324 'Cannot convert array to string', 325 1519239265 326 ); 327 }, 328 $needle 329 ); 330 } 331 332 /** 333 * Resolving property by name from submitted form data. 334 * 335 * @param string $property 336 * @param FormRuntime $formRuntime 337 * @return int|string|array 338 */ 339 protected function resolveRuntimeReference(string $property, FormRuntime $formRuntime) 340 { 341 if ($property === '__currentTimestamp') { 342 return time(); 343 } 344 // try to resolve the path '{...}' within the FormRuntime 345 $value = ObjectAccess::getPropertyPath($formRuntime, $property); 346 if ($value === null) { 347 // try to resolve the path '{...}' within the FinisherVariableProvider 348 $value = ObjectAccess::getPropertyPath( 349 $this->finisherContext->getFinisherVariableProvider(), 350 $property 351 ); 352 } 353 if ($value !== null) { 354 return $value; 355 } 356 // in case no value could be resolved 357 return '{' . $property . '}'; 358 } 359 360 /** 361 * Returns whether this finisher is enabled 362 * 363 * @return bool 364 */ 365 public function isEnabled(): bool 366 { 367 return !isset($this->options['renderingOptions']['enabled']) || (bool)$this->parseOption('renderingOptions.enabled') === true; 368 } 369 370 /** 371 * @return TypoScriptFrontendController 372 */ 373 protected function getTypoScriptFrontendController() 374 { 375 return $GLOBALS['TSFE']; 376 } 377} 378