401 lines
13 KiB
PHP
401 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
/*
|
|
* Inspired by and partially taken from the Neos.Form package (www.neos.io)
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
|
|
|
use Psr\Log\LoggerAwareInterface;
|
|
use Psr\Log\LoggerAwareTrait;
|
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
|
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
|
|
use TYPO3\CMS\Core\View\ViewFactoryData;
|
|
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
|
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
|
|
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
|
|
use TYPO3\CMS\Form\Domain\Model\FormElements\StringableFormElementInterface;
|
|
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
|
|
use TYPO3\CMS\Form\Service\TranslationService;
|
|
|
|
/**
|
|
* Finisher base class.
|
|
*
|
|
* Scope: frontend
|
|
* **This class is meant to be sub classed by developers**
|
|
*/
|
|
abstract class AbstractFinisher implements FinisherInterface, LoggerAwareInterface
|
|
{
|
|
use LoggerAwareTrait;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $finisherIdentifier = '';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $shortFinisherIdentifier = '';
|
|
|
|
/**
|
|
* The options which have been set from the outside. Instead of directly
|
|
* accessing them, you should rather use parseOption().
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $options = [];
|
|
|
|
/**
|
|
* These are the default options of the finisher.
|
|
* Override them in your concrete implementation.
|
|
* Default options should not be changed from "outside"
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $defaultOptions = [];
|
|
|
|
/**
|
|
* @var FinisherContext
|
|
*/
|
|
protected $finisherContext;
|
|
|
|
private ViewFactoryInterface $viewFactory;
|
|
|
|
private TranslationService $translationService;
|
|
|
|
public function injectViewFactory(ViewFactoryInterface $viewFactory)
|
|
{
|
|
$this->viewFactory = $viewFactory;
|
|
}
|
|
|
|
public function injectTranslationService(TranslationService $translationService)
|
|
{
|
|
$this->translationService = $translationService;
|
|
}
|
|
|
|
/**
|
|
* @param string $finisherIdentifier The identifier for this finisher
|
|
*/
|
|
public function setFinisherIdentifier(string $finisherIdentifier): void
|
|
{
|
|
$this->finisherIdentifier = $finisherIdentifier;
|
|
$this->shortFinisherIdentifier = preg_replace('/Finisher$/', '', $finisherIdentifier) ?? '';
|
|
}
|
|
|
|
public function getFinisherIdentifier(): string
|
|
{
|
|
return $this->finisherIdentifier;
|
|
}
|
|
|
|
/**
|
|
* @param array $options configuration options in the format ['option1' => 'value1', 'option2' => 'value2', ...]
|
|
*/
|
|
public function setOptions(array $options)
|
|
{
|
|
$this->options = $options;
|
|
}
|
|
|
|
/**
|
|
* Sets a single finisher option (@see setOptions())
|
|
*
|
|
* @param string $optionName name of the option to be set
|
|
* @param mixed $optionValue value of the option
|
|
*/
|
|
public function setOption(string $optionName, $optionValue)
|
|
{
|
|
$this->options[$optionName] = $optionValue;
|
|
}
|
|
|
|
/**
|
|
* Executes the finisher
|
|
*
|
|
* @param FinisherContext $finisherContext The Finisher context that contains the current Form Runtime and Response
|
|
* @return string|null
|
|
*/
|
|
final public function execute(FinisherContext $finisherContext)
|
|
{
|
|
$this->finisherContext = $finisherContext;
|
|
|
|
if (!$this->isEnabled()) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return $this->executeInternal();
|
|
} catch (FinisherException $e) {
|
|
$this->logger->error('Failed to execute finisher', ['exception' => $e]);
|
|
$this->finisherContext->cancel();
|
|
$formRuntime = $this->finisherContext->getFormRuntime();
|
|
$renderingOptions = $formRuntime->getRenderingOptions();
|
|
$viewFactoryData = new ViewFactoryData(
|
|
templateRootPaths: is_array($renderingOptions['templateRootPaths'] ?? null) ? $renderingOptions['templateRootPaths'] : [],
|
|
partialRootPaths: is_array($renderingOptions['partialRootPaths'] ?? null) ? $renderingOptions['partialRootPaths'] : [],
|
|
layoutRootPaths: is_array($renderingOptions['layoutRootPaths'] ?? null) ? $renderingOptions['layoutRootPaths'] : [],
|
|
request: $this->finisherContext->getRequest(),
|
|
);
|
|
$view = $this->viewFactory->create($viewFactoryData);
|
|
$message = $this->parseOption('errorMessage') ?: $this->translationService->translate('form.finisher.error', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
|
|
$view->assign('message', $message);
|
|
return $view->render('Finishers/Error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method is called in the concrete finisher whenever self::execute() is called.
|
|
*
|
|
* Override and fill with your own implementation!
|
|
*
|
|
* @throws FinisherException
|
|
* @return string|void|null
|
|
*/
|
|
abstract protected function executeInternal();
|
|
|
|
/**
|
|
* Read the option called $optionName from $this->options, and parse {...}
|
|
* as object accessors.
|
|
*
|
|
* Then translate the value.
|
|
*
|
|
* If $optionName was not found, the corresponding default option is returned (from $this->defaultOptions)
|
|
*
|
|
* @param string $optionName
|
|
* @return string|array|int|bool|\Closure|callable|null
|
|
*/
|
|
protected function parseOption(string $optionName)
|
|
{
|
|
if ($optionName === 'translation') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$optionValue = ArrayUtility::getValueByPath($this->options, $optionName, '.');
|
|
} catch (MissingArrayPathException $exception) {
|
|
$optionValue = null;
|
|
}
|
|
try {
|
|
$defaultValue = ArrayUtility::getValueByPath($this->defaultOptions, $optionName, '.');
|
|
} catch (MissingArrayPathException $exception) {
|
|
$defaultValue = null;
|
|
}
|
|
|
|
if ($optionValue === null && $defaultValue !== null) {
|
|
$optionValue = $defaultValue;
|
|
}
|
|
|
|
if ($optionValue === null) {
|
|
return null;
|
|
}
|
|
|
|
if (!is_string($optionValue) && !is_array($optionValue)) {
|
|
return $optionValue;
|
|
}
|
|
|
|
$formRuntime = $this->finisherContext->getFormRuntime();
|
|
$optionValue = $this->substituteRuntimeReferences($optionValue, $formRuntime);
|
|
|
|
if (is_string($optionValue)) {
|
|
$translationOptions = is_array($this->options['translation'] ?? null)
|
|
? $this->options['translation']
|
|
: [];
|
|
|
|
$optionValue = $this->translateFinisherOption(
|
|
$optionValue,
|
|
$formRuntime,
|
|
$optionName,
|
|
$optionValue,
|
|
$translationOptions
|
|
);
|
|
|
|
$optionValue = $this->substituteRuntimeReferences($optionValue, $formRuntime);
|
|
}
|
|
|
|
if (empty($optionValue)) {
|
|
if ($defaultValue !== null) {
|
|
$optionValue = $defaultValue;
|
|
}
|
|
}
|
|
return $optionValue;
|
|
}
|
|
|
|
/**
|
|
* Wraps TranslationService::translateFinisherOption to recursively
|
|
* invoke all array items of resolved form state values or nested
|
|
* finisher option configuration settings.
|
|
*
|
|
* @param string|array $subject
|
|
* @param FormRuntime $formRuntime
|
|
* @param string|array $optionValue
|
|
* @return array|string
|
|
*/
|
|
protected function translateFinisherOption(
|
|
$subject,
|
|
FormRuntime $formRuntime,
|
|
string $optionName,
|
|
$optionValue,
|
|
array $translationOptions
|
|
) {
|
|
if (is_array($subject)) {
|
|
foreach ($subject as $key => $value) {
|
|
$subject[$key] = $this->translateFinisherOption(
|
|
$value,
|
|
$formRuntime,
|
|
$optionName . '.' . $value,
|
|
$value,
|
|
$translationOptions
|
|
);
|
|
}
|
|
return $subject;
|
|
}
|
|
|
|
return $this->translationService->translateFinisherOption(
|
|
$formRuntime,
|
|
$this->finisherIdentifier,
|
|
$optionName,
|
|
$optionValue,
|
|
$translationOptions
|
|
);
|
|
}
|
|
|
|
/**
|
|
* You can encapsulate an option value with {}.
|
|
* This enables you to access every gettable property from the
|
|
* TYPO3\CMS\Form\Domain\Runtime\FormRuntime.
|
|
*
|
|
* For example: {formState.formValues.<elementIdentifier>}
|
|
* or {<elementIdentifier>}
|
|
*
|
|
* Both examples are equal to "$formRuntime->getFormState()->getFormValues()[<elementIdentifier>]"
|
|
* There is a special option value '{__currentTimestamp}'.
|
|
* This will be replaced with the current timestamp.
|
|
*
|
|
* @param string|array $needle
|
|
* @param FormRuntime $formRuntime
|
|
* @return mixed
|
|
*/
|
|
protected function substituteRuntimeReferences($needle, FormRuntime $formRuntime)
|
|
{
|
|
// neither array nor string, directly return
|
|
if (!is_array($needle) && !is_string($needle)) {
|
|
return $needle;
|
|
}
|
|
|
|
// resolve (recursively) all array items
|
|
if (is_array($needle)) {
|
|
$substitutedNeedle = [];
|
|
foreach ($needle as $key => $item) {
|
|
$key = $this->substituteRuntimeReferences($key, $formRuntime);
|
|
$item = $this->substituteRuntimeReferences($item, $formRuntime);
|
|
$substitutedNeedle[$key] = $item;
|
|
}
|
|
return $substitutedNeedle;
|
|
}
|
|
|
|
// substitute one(!) variable in string which either could result
|
|
// again in a string or an array representing multiple values
|
|
if (preg_match('/^{([^}]+)}$/', $needle, $matches)) {
|
|
return $this->resolveRuntimeReference(
|
|
$matches[1],
|
|
$formRuntime
|
|
);
|
|
}
|
|
|
|
// in case string contains more than just one variable or just a static
|
|
// value that does not need to be substituted at all, candidates are:
|
|
// * "prefix{variable}suffix
|
|
// * "{variable-1},{variable-2}"
|
|
// * "some static value"
|
|
// * mixed cases of the above
|
|
return preg_replace_callback(
|
|
'/{([^}]+)}/',
|
|
function ($matches) use ($formRuntime) {
|
|
$value = $this->resolveRuntimeReference(
|
|
$matches[1],
|
|
$formRuntime
|
|
);
|
|
|
|
// substitute each match by returning the resolved value
|
|
if (!is_array($value)) {
|
|
return $value;
|
|
}
|
|
|
|
// now the resolve value is an array that shall substitute
|
|
// a variable in a string that probably is not the only one
|
|
// or is wrapped with other static string content (see above)
|
|
// ... which is just not possible
|
|
throw new FinisherException(
|
|
'Cannot convert array to string',
|
|
1519239265
|
|
);
|
|
},
|
|
$needle
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolving property by name from submitted form data.
|
|
*
|
|
* @return int|string|array
|
|
*/
|
|
protected function resolveRuntimeReference(string $property, FormRuntime $formRuntime)
|
|
{
|
|
if ($property === '__currentTimestamp') {
|
|
return time();
|
|
}
|
|
|
|
// try to resolve the path '{...}' within the FormRuntime
|
|
$value = ObjectAccess::getPropertyPath($formRuntime, $property);
|
|
|
|
if (is_object($value)) {
|
|
$element = $formRuntime->getFormDefinition()->getElementByIdentifier($property);
|
|
|
|
if (!$element instanceof StringableFormElementInterface) {
|
|
throw new FinisherException(
|
|
sprintf('Cannot convert object value of "%s" to string', $property),
|
|
1574362327
|
|
);
|
|
}
|
|
|
|
$value = $element->valueToString($value);
|
|
}
|
|
|
|
if ($value === null) {
|
|
// try to resolve the path '{...}' within the FinisherVariableProvider
|
|
$value = ObjectAccess::getPropertyPath(
|
|
$this->finisherContext->getFinisherVariableProvider(),
|
|
$property
|
|
);
|
|
}
|
|
|
|
if ($value !== null) {
|
|
return $value;
|
|
}
|
|
|
|
// in case no value could be resolved
|
|
return '{' . $property . '}';
|
|
}
|
|
|
|
/**
|
|
* Returns whether this finisher is enabled
|
|
*/
|
|
public function isEnabled(): bool
|
|
{
|
|
return !isset($this->options['renderingOptions']['enabled']) || (bool)$this->parseOption('renderingOptions.enabled') === true;
|
|
}
|
|
}
|