337ddabe8173ba2906bf2a02cc77bc3c1aa8fa9a
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Domain / Finishers / AbstractFinisher.php
1 <?php
2 declare(strict_types = 1);
3 namespace 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
20 use TYPO3\CMS\Core\Utility\ArrayUtility;
21 use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
22 use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
23 use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
24 use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
25 use TYPO3\CMS\Form\Service\TranslationService;
26 use 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 */
34 abstract 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 * @param array $options configuration options in the format ['option1' => 'value1', 'option2' => 'value2', ...]
99 */
100 public function setOptions(array $options)
101 {
102 $this->options = $options;
103 }
104
105 /**
106 * Sets a single finisher option (@see setOptions())
107 *
108 * @param string $optionName name of the option to be set
109 * @param mixed $optionValue value of the option
110 */
111 public function setOption(string $optionName, $optionValue)
112 {
113 $this->options[$optionName] = $optionValue;
114 }
115
116 /**
117 * Executes the finisher
118 *
119 * @param FinisherContext $finisherContext The Finisher context that contains the current Form Runtime and Response
120 * @return string|null
121 */
122 final public function execute(FinisherContext $finisherContext)
123 {
124 $this->finisherContext = $finisherContext;
125
126 if (!$this->isEnabled()) {
127 return null;
128 }
129
130 return $this->executeInternal();
131 }
132
133 /**
134 * This method is called in the concrete finisher whenever self::execute() is called.
135 *
136 * Override and fill with your own implementation!
137 *
138 * @return string|null
139 */
140 abstract protected function executeInternal();
141
142 /**
143 * Read the option called $optionName from $this->options, and parse {...}
144 * as object accessors.
145 *
146 * Then translate the value.
147 *
148 * If $optionName was not found, the corresponding default option is returned (from $this->defaultOptions)
149 *
150 * @param string $optionName
151 * @return string|array|null
152 */
153 protected function parseOption(string $optionName)
154 {
155 if ($optionName === 'translation') {
156 return null;
157 }
158
159 try {
160 $optionValue = ArrayUtility::getValueByPath($this->options, $optionName, '.');
161 } catch (MissingArrayPathException $exception) {
162 $optionValue = null;
163 }
164 try {
165 $defaultValue = ArrayUtility::getValueByPath($this->defaultOptions, $optionName, '.');
166 } catch (MissingArrayPathException $exception) {
167 $defaultValue = null;
168 }
169
170 if ($optionValue === null && $defaultValue !== null) {
171 $optionValue = $defaultValue;
172 }
173
174 if ($optionValue === null) {
175 return null;
176 }
177
178 if (!is_string($optionValue) && !is_array($optionValue)) {
179 return $optionValue;
180 }
181
182 $formRuntime = $this->finisherContext->getFormRuntime();
183 $optionValue = $this->substituteRuntimeReferences($optionValue, $formRuntime);
184
185 if (is_string($optionValue)) {
186 $translationOptions = isset($this->options['translation']) && \is_array($this->options['translation'])
187 ? $this->options['translation']
188 : [];
189
190 $optionValue = $this->translateFinisherOption(
191 $optionValue,
192 $formRuntime,
193 $optionName,
194 $optionValue,
195 $translationOptions
196 );
197
198 $optionValue = $this->substituteRuntimeReferences($optionValue, $formRuntime);
199 }
200
201 if (empty($optionValue)) {
202 if ($defaultValue !== null) {
203 $optionValue = $defaultValue;
204 }
205 }
206 return $optionValue;
207 }
208
209 /**
210 * Wraps TranslationService::translateFinisherOption to recursively
211 * invoke all array items of resolved form state values or nested
212 * finisher option configuration settings.
213 *
214 * @param string|array $subject
215 * @param FormRuntime $formRuntime
216 * @param string $optionName
217 * @param string|array $optionValue
218 * @param array $translationOptions
219 * @return array|string
220 */
221 protected function translateFinisherOption(
222 $subject,
223 FormRuntime $formRuntime,
224 string $optionName,
225 $optionValue,
226 array $translationOptions
227 ) {
228 if (is_array($subject)) {
229 foreach ($subject as $key => $value) {
230 $subject[$key] = $this->translateFinisherOption(
231 $value,
232 $formRuntime,
233 $optionName . '.' . $value,
234 $value,
235 $translationOptions
236 );
237 }
238 return $subject;
239 }
240
241 return TranslationService::getInstance()->translateFinisherOption(
242 $formRuntime,
243 $this->finisherIdentifier,
244 $optionName,
245 $optionValue,
246 $translationOptions
247 );
248 }
249
250 /**
251 * You can encapsulate a option value with {}.
252 * This enables you to access every getable property from the
253 * TYPO3\CMS\Form\Domain\Runtime\FormRuntime.
254 *
255 * For example: {formState.formValues.<elemenIdentifier>}
256 * or {<elemenIdentifier>}
257 *
258 * Both examples are equal to "$formRuntime->getFormState()->getFormValues()[<elemenIdentifier>]"
259 * There is a special option value '{__currentTimestamp}'.
260 * This will be replaced with the current timestamp.
261 *
262 * @param string|array $needle
263 * @param FormRuntime $formRuntime
264 * @return mixed
265 */
266 protected function substituteRuntimeReferences($needle, FormRuntime $formRuntime)
267 {
268 // neither array nor string, directly return
269 if (!is_array($needle) && !is_string($needle)) {
270 return $needle;
271 }
272
273 // resolve (recursively) all array items
274 if (is_array($needle)) {
275 return array_map(
276 function ($item) use ($formRuntime) {
277 return $this->substituteRuntimeReferences($item, $formRuntime);
278 },
279 $needle
280 );
281 }
282
283 // substitute one(!) variable in string which either could result
284 // again in a string or an array representing multiple values
285 if (preg_match('/^{([^}]+)}$/', $needle, $matches)) {
286 return $this->resolveRuntimeReference(
287 $matches[1],
288 $formRuntime
289 );
290 }
291
292 // in case string contains more than just one variable or just a static
293 // value that does not need to be substituted at all, candidates are:
294 // * "prefix{variable}suffix
295 // * "{variable-1},{variable-2}"
296 // * "some static value"
297 // * mixed cases of the above
298 return preg_replace_callback(
299 '/{([^}]+)}/',
300 function ($matches) use ($formRuntime) {
301 $value = $this->resolveRuntimeReference(
302 $matches[1],
303 $formRuntime
304 );
305
306 // substitute each match by returning the resolved value
307 if (!is_array($value)) {
308 return $value;
309 }
310
311 // now the resolve value is an array that shall substitute
312 // a variable in a string that probably is not the only one
313 // or is wrapped with other static string content (see above)
314 // ... which is just not possible
315 throw new FinisherException(
316 'Cannot convert array to string',
317 1519239265
318 );
319 },
320 $needle
321 );
322 }
323
324 /**
325 * Resolving property by name from submitted form data.
326 *
327 * @param string $property
328 * @param FormRuntime $formRuntime
329 * @return int|string|array
330 */
331 protected function resolveRuntimeReference(string $property, FormRuntime $formRuntime)
332 {
333 if ($property === '__currentTimestamp') {
334 return time();
335 }
336 // try to resolve the path '{...}' within the FormRuntime
337 $value = ObjectAccess::getPropertyPath($formRuntime, $property);
338 if ($value === null) {
339 // try to resolve the path '{...}' within the FinisherVariableProvider
340 $value = ObjectAccess::getPropertyPath(
341 $this->finisherContext->getFinisherVariableProvider(),
342 $property
343 );
344 }
345 if ($value !== null) {
346 return $value;
347 }
348 // in case no value could be resolved
349 return '{' . $property . '}';
350 }
351
352 /**
353 * Returns whether this finisher is enabled
354 *
355 * @return bool
356 */
357 public function isEnabled(): bool
358 {
359 return !isset($this->options['renderingOptions']['enabled']) || (bool)$this->parseOption('renderingOptions.enabled') === true;
360 }
361
362 /**
363 * @return TypoScriptFrontendController
364 */
365 protected function getTypoScriptFrontendController()
366 {
367 return $GLOBALS['TSFE'];
368 }
369 }