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