TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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 TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
|
||||
|
||||
/**
|
||||
* A simple finisher that invokes a closure when executed
|
||||
*
|
||||
* Usage:
|
||||
* //...
|
||||
* $closureFinisher = GeneralUtility::makeInstance(ClosureFinisher::class);
|
||||
* $closureFinisher->setOption('closure', function($finisherContext) {
|
||||
* $formRuntime = $finisherContext->getFormRuntime();
|
||||
* // ...
|
||||
* });
|
||||
* $formDefinition->addFinisher($closureFinisher);
|
||||
* // ...
|
||||
*
|
||||
* Scope: frontend
|
||||
*/
|
||||
class ClosureFinisher extends AbstractFinisher
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultOptions = [
|
||||
'closure' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* Executes this finisher
|
||||
* @see AbstractFinisher::execute()
|
||||
*
|
||||
* @throws FinisherException
|
||||
*/
|
||||
protected function executeInternal()
|
||||
{
|
||||
$closure = $this->parseOption('closure');
|
||||
if ($closure === null) {
|
||||
return;
|
||||
}
|
||||
if (!$closure instanceof \Closure) {
|
||||
throw new FinisherException(sprintf('The option "closure" must be of type Closure, "%s" given.', gettype($closure)), 1332155239);
|
||||
}
|
||||
$closure($this->finisherContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface as ExtbaseConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
|
||||
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
|
||||
use TYPO3\CMS\Form\ViewHelpers\RenderRenderableViewHelper;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
|
||||
/**
|
||||
* A finisher that outputs a given text
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - message: A hard-coded message to be rendered
|
||||
* - contentElementUid: A content element uid to be rendered
|
||||
*
|
||||
* Usage:
|
||||
* //...
|
||||
* $confirmationFinisher = GeneralUtility::makeInstance(ConfirmationFinisher::class);
|
||||
* $confirmationFinisher->setOptions(
|
||||
* [
|
||||
* 'message' => 'foo',
|
||||
* ]
|
||||
* );
|
||||
* $formDefinition->addFinisher($confirmationFinisher);
|
||||
* // ...
|
||||
*
|
||||
* Scope: frontend
|
||||
*/
|
||||
class ConfirmationFinisher extends AbstractFinisher
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultOptions = [
|
||||
'message' => 'The form has been submitted.',
|
||||
'contentElementUid' => 0,
|
||||
'typoscriptObjectPath' => 'lib.tx_form.contentElementRendering',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly ExtbaseConfigurationManagerInterface $extbaseConfigurationManager,
|
||||
private readonly ViewFactoryInterface $viewFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws FinisherException
|
||||
*/
|
||||
protected function executeInternal(): string
|
||||
{
|
||||
$options = $this->options;
|
||||
if (!isset($options['templateName']) || !is_string($options['templateName'])) {
|
||||
throw new FinisherException(
|
||||
'The option "templateName" must be set for the ConfirmationFinisher.',
|
||||
1521573955
|
||||
);
|
||||
}
|
||||
|
||||
$contentElementUid = $this->parseOption('contentElementUid');
|
||||
$typoscriptObjectPath = $this->parseOption('typoscriptObjectPath');
|
||||
$typoscriptObjectPath = is_string($typoscriptObjectPath) ? $typoscriptObjectPath : '';
|
||||
if (!empty($contentElementUid)) {
|
||||
$pathSegments = GeneralUtility::trimExplode('.', $typoscriptObjectPath);
|
||||
$lastSegment = array_pop($pathSegments);
|
||||
$setup = $this->extbaseConfigurationManager->getConfiguration(ExtbaseConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT);
|
||||
foreach ($pathSegments as $segment) {
|
||||
if (!array_key_exists($segment . '.', $setup)) {
|
||||
throw new FinisherException(
|
||||
sprintf('TypoScript object path "%s" does not exist', $typoscriptObjectPath),
|
||||
1489238980
|
||||
);
|
||||
}
|
||||
$setup = $setup[$segment . '.'];
|
||||
}
|
||||
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
||||
$contentObjectRenderer->setRequest($this->finisherContext->getRequest()->withoutAttribute('extbase'));
|
||||
$contentObjectRenderer->start([$contentElementUid]);
|
||||
$contentObjectRenderer->setCurrentVal((string)$contentElementUid);
|
||||
$message = $contentObjectRenderer->cObjGetSingle($setup[$lastSegment], $setup[$lastSegment . '.'], $lastSegment);
|
||||
} else {
|
||||
$message = $this->parseOption('message');
|
||||
}
|
||||
|
||||
$formRuntime = $this->finisherContext->getFormRuntime();
|
||||
$viewFactoryData = new ViewFactoryData(
|
||||
templateRootPaths: is_array($options['templateRootPaths'] ?? null) ? $options['templateRootPaths'] : [],
|
||||
partialRootPaths: is_array($options['partialRootPaths'] ?? null) ? $options['partialRootPaths'] : [],
|
||||
layoutRootPaths: is_array($options['layoutRootPaths'] ?? null) ? $options['layoutRootPaths'] : [],
|
||||
request: $this->finisherContext->getRequest(),
|
||||
);
|
||||
$view = $this->viewFactory->create($viewFactoryData);
|
||||
if ($view instanceof FluidViewAdapter) {
|
||||
$view->getRenderingContext()->getViewHelperVariableContainer()
|
||||
->addOrUpdate(RenderRenderableViewHelper::class, 'formRuntime', $formRuntime);
|
||||
}
|
||||
if (is_array($this->options['variables'] ?? null)) {
|
||||
$view->assignMultiple($this->options['variables']);
|
||||
}
|
||||
$view->assignMultiple([
|
||||
'form' => $formRuntime,
|
||||
'finisherVariableProvider' => $this->finisherContext->getFinisherVariableProvider(),
|
||||
'message' => $message,
|
||||
'isPreparedMessage' => !empty($contentElementUid),
|
||||
]);
|
||||
return $view->render($options['templateName']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\FileReference;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Extbase\Domain\Model\FileReference as ExtbaseFileReference;
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||
use TYPO3\CMS\Form\Domain\Model\FormElements\FileUpload;
|
||||
|
||||
/**
|
||||
* This finisher remove the submitted files.
|
||||
* Use this e.g after the email finisher if you don't want
|
||||
* to keep the files online.
|
||||
*
|
||||
* Scope: frontend
|
||||
*/
|
||||
class DeleteUploadsFinisher extends AbstractFinisher
|
||||
{
|
||||
/**
|
||||
* Executes this finisher
|
||||
* @see AbstractFinisher::execute()
|
||||
*/
|
||||
protected function executeInternal()
|
||||
{
|
||||
$formRuntime = $this->finisherContext->getFormRuntime();
|
||||
|
||||
$uploadFolders = [];
|
||||
$elements = $formRuntime->getFormDefinition()->getRenderablesRecursively();
|
||||
foreach ($elements as $element) {
|
||||
if (!$element instanceof FileUpload) {
|
||||
continue;
|
||||
}
|
||||
$file = $formRuntime[$element->getIdentifier()];
|
||||
if (!$file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($file instanceof ExtbaseFileReference) {
|
||||
$file = $file->getOriginalResource();
|
||||
}
|
||||
if ($file instanceof FileReference) {
|
||||
$this->deleteFileAndCollectFolder($file, $uploadFolders);
|
||||
} elseif ($file instanceof ObjectStorage) {
|
||||
foreach ($file as $singleFile) {
|
||||
if ($singleFile instanceof ExtbaseFileReference) {
|
||||
$singleFile = $singleFile->getOriginalResource();
|
||||
}
|
||||
if ($singleFile instanceof FileReference) {
|
||||
$this->deleteFileAndCollectFolder($singleFile, $uploadFolders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->deleteEmptyUploadFolders($uploadFolders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the file and collects its parent folder for later cleanup.
|
||||
*
|
||||
* @param array<string, Folder> $uploadFolders
|
||||
*/
|
||||
private function deleteFileAndCollectFolder(FileReference $file, array &$uploadFolders): void
|
||||
{
|
||||
$folder = $file->getParentFolder();
|
||||
if ($folder instanceof Folder) {
|
||||
$uploadFolders[$folder->getCombinedIdentifier()] = $folder;
|
||||
}
|
||||
$file->getStorage()->deleteFile($file->getOriginalFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* note:
|
||||
* TYPO3\CMS\Form\Mvc\Property\TypeConverter\UploadedFileReferenceConverter::importUploadedResource()
|
||||
* creates a sub-folder for file uploads (e.g. .../form_<40-chars-hash>/actual.file)
|
||||
* @param Folder[] $folders
|
||||
*/
|
||||
protected function deleteEmptyUploadFolders(array $folders): void
|
||||
{
|
||||
foreach ($folders as $folder) {
|
||||
if ($this->isEmptyFolder($folder)) {
|
||||
$folder->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function isEmptyFolder(Folder $folder): bool
|
||||
{
|
||||
return $folder->getFileCount() === 0
|
||||
&& $folder->getStorage()->countFoldersInFolder($folder) === 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
<?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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use TYPO3\CMS\Core\Mail\FluidEmail;
|
||||
use TYPO3\CMS\Core\Mail\MailerInterface;
|
||||
use TYPO3\CMS\Core\Mail\TemplatedEmailFactory;
|
||||
use TYPO3\CMS\Core\Resource\FileInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
|
||||
use TYPO3\CMS\Form\Domain\Model\FormElements\FileUpload;
|
||||
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
|
||||
use TYPO3\CMS\Form\Event\BeforeEmailFinisherInitializedEvent;
|
||||
use TYPO3\CMS\Form\ViewHelpers\RenderRenderableViewHelper;
|
||||
|
||||
/**
|
||||
* This finisher sends an email to one recipient
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - templateName (mandatory): Template name for the mail body
|
||||
* - templateRootPaths: root paths for the templates
|
||||
* - layoutRootPaths: root paths for the layouts
|
||||
* - partialRootPaths: root paths for the partials
|
||||
* - variables: associative array of variables which are available inside the Fluid template
|
||||
*
|
||||
* The following options control the mail sending. In all of them, placeholders in the form
|
||||
* of {...} are replaced with the corresponding form value; i.e. {email} as senderAddress
|
||||
* makes the recipient address configurable.
|
||||
*
|
||||
* - subject (mandatory): Subject of the email
|
||||
* - recipients (mandatory): Email addresses and human-readable names of the recipients
|
||||
* - senderAddress (mandatory): Email address of the sender
|
||||
* - senderName: Human-readable name of the sender
|
||||
* - replyToRecipients: Email addresses and human-readable names of the reply-to recipients
|
||||
* - carbonCopyRecipients: Email addresses and human-readable names of the copy recipients
|
||||
* - blindCarbonCopyRecipients: Email addresses and human-readable names of the blind copy recipients
|
||||
* - title: The title of the email - If not set "subject" is used by default
|
||||
*
|
||||
* Scope: frontend
|
||||
*/
|
||||
class EmailFinisher extends AbstractFinisher
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultOptions = [
|
||||
'recipientName' => '',
|
||||
'senderName' => '',
|
||||
'addHtmlPart' => true,
|
||||
'attachUploads' => true,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||
protected readonly TemplatedEmailFactory $templatedEmailFactory,
|
||||
protected readonly MailerInterface $mailer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Executes this finisher
|
||||
* @see AbstractFinisher::execute()
|
||||
*
|
||||
* @throws FinisherException
|
||||
*/
|
||||
protected function executeInternal(): void
|
||||
{
|
||||
$this->options = $this->eventDispatcher
|
||||
->dispatch(new BeforeEmailFinisherInitializedEvent($this->finisherContext, $this->options))
|
||||
->getOptions();
|
||||
// Flexform overrides write strings instead of integers so
|
||||
// we need to cast the string '0' to false.
|
||||
if (
|
||||
isset($this->options['addHtmlPart'])
|
||||
&& $this->options['addHtmlPart'] === '0'
|
||||
) {
|
||||
$this->options['addHtmlPart'] = false;
|
||||
}
|
||||
|
||||
$subject = (string)$this->parseOption('subject');
|
||||
$recipients = $this->getRecipients('recipients');
|
||||
$senderAddress = $this->parseOption('senderAddress');
|
||||
$senderAddress = is_string($senderAddress) ? $senderAddress : '';
|
||||
$senderName = $this->parseOption('senderName');
|
||||
$senderName = is_string($senderName) ? $senderName : '';
|
||||
$replyToRecipients = $this->getRecipients('replyToRecipients');
|
||||
$carbonCopyRecipients = $this->getRecipients('carbonCopyRecipients');
|
||||
$blindCarbonCopyRecipients = $this->getRecipients('blindCarbonCopyRecipients');
|
||||
$addHtmlPart = (bool)$this->parseOption('addHtmlPart');
|
||||
$attachUploads = $this->parseOption('attachUploads');
|
||||
$title = (string)$this->parseOption('title') ?: $subject;
|
||||
|
||||
if ($subject === '') {
|
||||
throw new FinisherException('The option "subject" must be set for the EmailFinisher.', 1327060320);
|
||||
}
|
||||
if (empty($recipients)) {
|
||||
throw new FinisherException('The option "recipients" must be set for the EmailFinisher.', 1327060200);
|
||||
}
|
||||
if (empty($senderAddress)) {
|
||||
throw new FinisherException('The option "senderAddress" must be set for the EmailFinisher.', 1327060210);
|
||||
}
|
||||
|
||||
$formRuntime = $this->finisherContext->getFormRuntime();
|
||||
|
||||
$mail = $this
|
||||
->initializeFluidEmail($formRuntime)
|
||||
->from(new Address($senderAddress, $senderName))
|
||||
->to(...$recipients)
|
||||
->subject($subject)
|
||||
->format($addHtmlPart ? FluidEmail::FORMAT_BOTH : FluidEmail::FORMAT_PLAIN)
|
||||
->assign('title', $title);
|
||||
|
||||
if (!empty($replyToRecipients)) {
|
||||
$mail->replyTo(...$replyToRecipients);
|
||||
}
|
||||
|
||||
if (!empty($carbonCopyRecipients)) {
|
||||
$mail->cc(...$carbonCopyRecipients);
|
||||
}
|
||||
|
||||
if (!empty($blindCarbonCopyRecipients)) {
|
||||
$mail->bcc(...$blindCarbonCopyRecipients);
|
||||
}
|
||||
|
||||
if (is_string($this->options['translation']['language'] ?? null) && $this->options['translation']['language'] !== '') {
|
||||
$mail->assign('languageKey', $this->options['translation']['language']);
|
||||
}
|
||||
|
||||
$message = $this->parseOption('message');
|
||||
if (is_string($message) && $message !== '') {
|
||||
// Remove whitespace between HTML tags to prevent lib.parseFunc_RTE
|
||||
// from converting newlines into additional blank lines in the email output
|
||||
$message = preg_replace('/>\s+</', '><', $message);
|
||||
$placeholderPos = strpos($message, '{formValues}');
|
||||
if ($placeholderPos !== false) {
|
||||
$mail->assign('messageBefore', substr($message, 0, $placeholderPos));
|
||||
$mail->assign('messageAfter', substr($message, $placeholderPos + strlen('{formValues}')));
|
||||
} else {
|
||||
// No placeholder - show message only, no form values
|
||||
$mail->assign('messageBefore', $message);
|
||||
$mail->assign('messageAfter', '');
|
||||
$mail->assign('hideFormValues', true);
|
||||
}
|
||||
}
|
||||
|
||||
if ($attachUploads) {
|
||||
foreach ($formRuntime->getFormDefinition()->getRenderablesRecursively() as $element) {
|
||||
if (!$element instanceof FileUpload) {
|
||||
continue;
|
||||
}
|
||||
$file = $formRuntime[$element->getIdentifier()];
|
||||
if ($file instanceof FileReference) {
|
||||
$file = $file->getOriginalResource();
|
||||
}
|
||||
if ($file instanceof FileInterface) {
|
||||
$mail->attach($file->getContents(), $file->getName(), $file->getMimeType());
|
||||
} elseif ($file instanceof ObjectStorage) {
|
||||
foreach ($file as $singleFile) {
|
||||
if ($singleFile instanceof FileReference) {
|
||||
$singleFile = $singleFile->getOriginalResource();
|
||||
}
|
||||
if ($singleFile instanceof FileInterface) {
|
||||
$mail->attach($singleFile->getContents(), $singleFile->getName(), $singleFile->getMimeType());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->mailer->send($mail);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
throw new FinisherException(
|
||||
'Failed to send the email: ' . $e->getMessage(),
|
||||
1754047320,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function initializeFluidEmail(FormRuntime $formRuntime): FluidEmail
|
||||
{
|
||||
$mailMessage = $this->templatedEmailFactory->createWithOverrides(
|
||||
$this->options['templateRootPaths'] ?? [],
|
||||
$this->options['layoutRootPaths'] ?? [],
|
||||
$this->options['partialRootPaths'] ?? [],
|
||||
$this->finisherContext->getRequest(),
|
||||
);
|
||||
|
||||
if (!isset($this->options['templateName']) || $this->options['templateName'] === '') {
|
||||
throw new FinisherException('The option "templateName" must be set to use FluidEmail.', 1599834020);
|
||||
}
|
||||
|
||||
// Migrate old template name to default FluidEmail name
|
||||
if ($this->options['templateName'] === '{@format}.html') {
|
||||
$this->options['templateName'] = 'Default';
|
||||
}
|
||||
|
||||
$mailMessage
|
||||
->setTemplate($this->options['templateName'])
|
||||
->assignMultiple([
|
||||
'finisherVariableProvider' => $this->finisherContext->getFinisherVariableProvider(),
|
||||
'form' => $formRuntime,
|
||||
]);
|
||||
|
||||
if (is_array($this->options['variables'] ?? null)) {
|
||||
$mailMessage->assignMultiple($this->options['variables']);
|
||||
}
|
||||
|
||||
$mailMessage
|
||||
->getViewHelperVariableContainer()
|
||||
->addOrUpdate(RenderRenderableViewHelper::class, 'formRuntime', $formRuntime);
|
||||
|
||||
return $mailMessage;
|
||||
}
|
||||
|
||||
protected function getRecipients(string $listOption): array
|
||||
{
|
||||
$recipients = $this->parseOption($listOption) ?? [];
|
||||
if (!is_array($recipients) || $recipients === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$addresses = [];
|
||||
foreach ($recipients as $address => $name) {
|
||||
// The if is needed to set address and name with TypoScript
|
||||
if (MathUtility::canBeInterpretedAsInteger($address)) {
|
||||
if (is_array($name)) {
|
||||
$address = $name[0] ?? '';
|
||||
$name = $name[1] ?? '';
|
||||
} else {
|
||||
$address = $name;
|
||||
$name = '';
|
||||
}
|
||||
}
|
||||
|
||||
$address = trim((string)$address);
|
||||
|
||||
if (!GeneralUtility::validEmail($address)) {
|
||||
// Drop entries without a valid address
|
||||
continue;
|
||||
}
|
||||
$addresses[] = new Address($address, $name);
|
||||
}
|
||||
return $addresses;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Form\Domain\Finishers\Exception;
|
||||
|
||||
use TYPO3\CMS\Form\Domain\Exception;
|
||||
|
||||
/**
|
||||
* This exception is thrown in Form Finishers
|
||||
*/
|
||||
class FinisherException extends Exception {}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?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 TYPO3\CMS\Extbase\Mvc\Request;
|
||||
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
|
||||
|
||||
/**
|
||||
* The context that is passed to each finisher when executed.
|
||||
* It acts like an EventObject that is able to stop propagation.
|
||||
*
|
||||
* Scope: frontend
|
||||
* **This class is NOT meant to be sub classed by developers.**
|
||||
* @internal
|
||||
*/
|
||||
class FinisherContext
|
||||
{
|
||||
/**
|
||||
* If TRUE further finishers won't be invoked
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $cancelled = false;
|
||||
|
||||
/**
|
||||
* A reference to the Form Runtime the finisher belongs to
|
||||
*/
|
||||
protected FormRuntime $formRuntime;
|
||||
|
||||
/**
|
||||
* The assigned controller context which might be needed by the finisher.
|
||||
*/
|
||||
protected FinisherVariableProvider $finisherVariableProvider;
|
||||
|
||||
private Request $request;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(FormRuntime $formRuntime, Request $request)
|
||||
{
|
||||
$this->formRuntime = $formRuntime;
|
||||
$this->request = $request;
|
||||
$this->finisherVariableProvider = new FinisherVariableProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the finisher invocation after the current finisher
|
||||
*/
|
||||
public function cancel()
|
||||
{
|
||||
$this->cancelled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* TRUE if no further finishers should be invoked. Defaults to FALSE
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function isCancelled(): bool
|
||||
{
|
||||
return $this->cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Form Runtime that is associated with the current finisher
|
||||
*/
|
||||
public function getFormRuntime(): FormRuntime
|
||||
{
|
||||
return $this->formRuntime;
|
||||
}
|
||||
|
||||
/**
|
||||
* The values of the submitted form (after validation and property mapping)
|
||||
*/
|
||||
public function getFormValues(): array
|
||||
{
|
||||
return $this->formRuntime->getFormState()->getFormValues();
|
||||
}
|
||||
|
||||
public function getFinisherVariableProvider(): FinisherVariableProvider
|
||||
{
|
||||
return $this->finisherVariableProvider;
|
||||
}
|
||||
|
||||
public function getRequest(): Request
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Finisher that can be attached to a form in order to be invoked
|
||||
* as soon as the complete form is submitted
|
||||
*
|
||||
* Scope: frontend
|
||||
*/
|
||||
interface FinisherInterface
|
||||
{
|
||||
/**
|
||||
* Executes the finisher
|
||||
*
|
||||
* @param FinisherContext $finisherContext The Finisher context that contains the current Form Runtime and Response
|
||||
* @return string|null
|
||||
*/
|
||||
public function execute(FinisherContext $finisherContext);
|
||||
|
||||
public function setFinisherIdentifier(string $finisherIdentifier): void;
|
||||
|
||||
/**
|
||||
* @param array $options configuration options in the format ['option1' => 'value1', 'option2' => 'value2', ...]
|
||||
*/
|
||||
public function setOptions(array $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);
|
||||
|
||||
/**
|
||||
* Returns whether this finisher is enabled
|
||||
*/
|
||||
public function isEnabled(): bool;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
|
||||
|
||||
/**
|
||||
* Store data for usage between the finishers.
|
||||
*
|
||||
* Scope: frontend
|
||||
* **This class is NOT meant to be sub classed by developers.**
|
||||
* @internal
|
||||
*/
|
||||
final class FinisherVariableProvider implements \ArrayAccess, \IteratorAggregate, \Countable
|
||||
{
|
||||
/**
|
||||
* Two-dimensional object array storing the values. The first dimension is the finisher identifier,
|
||||
* and the second dimension is the identifier for the data the finisher wants to store.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $objects = [];
|
||||
|
||||
/**
|
||||
* Add a variable to the finisher container.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function add(string $finisherIdentifier, string $key, $value)
|
||||
{
|
||||
$this->addOrUpdate($finisherIdentifier, $key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a variable to the Variable Container.
|
||||
* In case the value is already inside, it is silently overridden.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function addOrUpdate(string $finisherIdentifier, string $key, $value)
|
||||
{
|
||||
if (!array_key_exists($finisherIdentifier, $this->objects)) {
|
||||
$this->objects[$finisherIdentifier] = [];
|
||||
}
|
||||
$this->objects[$finisherIdentifier] = ArrayUtility::setValueByPath(
|
||||
$this->objects[$finisherIdentifier],
|
||||
$key,
|
||||
$value,
|
||||
'.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a variable which is stored
|
||||
*
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $finisherIdentifier, string $key, $default = null)
|
||||
{
|
||||
if ($this->exists($finisherIdentifier, $key)) {
|
||||
return ArrayUtility::getValueByPath($this->objects[$finisherIdentifier], $key, '.');
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether there is a variable stored for the given key
|
||||
*
|
||||
* @param string $finisherIdentifier
|
||||
* @param string $key
|
||||
*/
|
||||
public function exists($finisherIdentifier, $key): bool
|
||||
{
|
||||
try {
|
||||
ArrayUtility::getValueByPath($this->objects[$finisherIdentifier] ?? [], $key, '.');
|
||||
} catch (MissingArrayPathException $e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a value from the variable container
|
||||
*/
|
||||
public function remove(string $finisherIdentifier, string $key)
|
||||
{
|
||||
if ($this->exists($finisherIdentifier, $key)) {
|
||||
$this->objects[$finisherIdentifier] = ArrayUtility::removeByPath(
|
||||
$this->objects[$finisherIdentifier],
|
||||
$key,
|
||||
'.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up for serializing.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return ['objects'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an offset exists
|
||||
*
|
||||
* @link https://php.net/manual/en/arrayaccess.offsetexists.php
|
||||
* @param mixed $offset An offset to check for.
|
||||
* @return bool TRUE on success or FALSE on failure.
|
||||
*/
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return isset($this->objects[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to retrieve
|
||||
*
|
||||
* @link https://php.net/manual/en/arrayaccess.offsetget.php
|
||||
* @param mixed $offset The offset to retrieve.
|
||||
* @return mixed Can return all value types.
|
||||
*/
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
return $this->objects[$offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to set
|
||||
*
|
||||
* @link https://php.net/manual/en/arrayaccess.offsetset.php
|
||||
* @param mixed $offset The offset to assign the value to.
|
||||
* @param mixed $value The value to set.
|
||||
*/
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
$this->objects[$offset] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to unset
|
||||
*
|
||||
* @link https://php.net/manual/en/arrayaccess.offsetunset.php
|
||||
* @param mixed $offset The offset to unset.
|
||||
*/
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
unset($this->objects[$offset]);
|
||||
}
|
||||
|
||||
public function getIterator(): \Traversable
|
||||
{
|
||||
foreach ($this->objects as $offset => $value) {
|
||||
yield $offset => $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count elements of an object
|
||||
*
|
||||
* @link https://php.net/manual/en/countable.count.php
|
||||
* @return int The custom count as an integer.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->objects);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?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 TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Extbase\Error\Error;
|
||||
use TYPO3\CMS\Extbase\Error\Message;
|
||||
use TYPO3\CMS\Extbase\Error\Notice;
|
||||
use TYPO3\CMS\Extbase\Error\Warning;
|
||||
use TYPO3\CMS\Extbase\Service\ExtensionService;
|
||||
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
|
||||
|
||||
/**
|
||||
* A simple finisher that adds a message to the FlashMessageContainer
|
||||
*
|
||||
* Usage:
|
||||
* //...
|
||||
* $flashMessageFinisher = GeneralUtility::makeInstance(FlashMessageFinisher::class);
|
||||
* $flashMessageFinisher->setOptions(
|
||||
* [
|
||||
* 'messageBody' => 'Some message body',
|
||||
* 'messageTitle' => 'Some message title',
|
||||
* 'messageArguments' => ['foo' => 'bar'],
|
||||
* 'severity' => \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR
|
||||
* ]
|
||||
* );
|
||||
* $formDefinition->addFinisher($flashMessageFinisher);
|
||||
* // ...
|
||||
*
|
||||
* Scope: frontend
|
||||
*/
|
||||
class FlashMessageFinisher extends AbstractFinisher
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultOptions = [
|
||||
'messageBody' => null,
|
||||
'messageTitle' => '',
|
||||
'messageArguments' => [],
|
||||
'messageCode' => null,
|
||||
'severity' => ContextualFeedbackSeverity::OK,
|
||||
];
|
||||
|
||||
private ExtensionService $extensionService;
|
||||
private FlashMessageService $flashMessageService;
|
||||
|
||||
public function injectFlashMessageService(FlashMessageService $flashMessageService): void
|
||||
{
|
||||
$this->flashMessageService = $flashMessageService;
|
||||
}
|
||||
|
||||
public function injectExtensionService(ExtensionService $extensionService): void
|
||||
{
|
||||
$this->extensionService = $extensionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes this finisher
|
||||
* @see AbstractFinisher::execute()
|
||||
*
|
||||
* @throws FinisherException
|
||||
*/
|
||||
protected function executeInternal()
|
||||
{
|
||||
$messageBody = $this->parseOption('messageBody');
|
||||
if (!is_string($messageBody)) {
|
||||
throw new FinisherException(sprintf('The message body must be of type string, "%s" given.', gettype($messageBody)), 1335980069);
|
||||
}
|
||||
$messageTitle = $this->parseOption('messageTitle');
|
||||
$messageArguments = $this->parseOption('messageArguments');
|
||||
$messageCode = $this->parseOption('messageCode');
|
||||
$severity = $this->parseOption('severity');
|
||||
|
||||
if (MathUtility::canBeInterpretedAsInteger($severity)) {
|
||||
$severity = ContextualFeedbackSeverity::tryFrom((int)$severity);
|
||||
}
|
||||
if (!$severity instanceof ContextualFeedbackSeverity) {
|
||||
$severity = $this->defaultOptions['severity'];
|
||||
}
|
||||
|
||||
$messageClass = match ($severity) {
|
||||
ContextualFeedbackSeverity::NOTICE => Notice::class,
|
||||
ContextualFeedbackSeverity::WARNING => Warning::class,
|
||||
ContextualFeedbackSeverity::ERROR => Error::class,
|
||||
default => Message::class,
|
||||
};
|
||||
/** @var Message|Notice|Warning|Error $message */
|
||||
$message = GeneralUtility::makeInstance($messageClass, $messageBody, $messageCode, $messageArguments, $messageTitle);
|
||||
$flashMessage = new FlashMessage(
|
||||
$message->render(),
|
||||
$message->getTitle(),
|
||||
$severity,
|
||||
true
|
||||
);
|
||||
|
||||
// todo: this value has to be taken from the request directly in the future
|
||||
$pluginNamespace = $this->extensionService->getPluginNamespace(
|
||||
$this->finisherContext->getRequest()->getControllerExtensionName(),
|
||||
$this->finisherContext->getRequest()->getPluginName()
|
||||
);
|
||||
|
||||
$this->flashMessageService->getMessageQueueByIdentifier('extbase.flashmessages.' . $pluginNamespace)->addMessage($flashMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||
|
||||
use TYPO3\CMS\Core\Http\PropagateResponseException;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This finisher redirects to another Controller.
|
||||
*
|
||||
* Scope: frontend
|
||||
*/
|
||||
class RedirectFinisher extends AbstractFinisher
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultOptions = [
|
||||
'pageUid' => 1,
|
||||
'additionalParameters' => '',
|
||||
'statusCode' => 303,
|
||||
'fragment' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* Executes this finisher
|
||||
* @see AbstractFinisher::execute()
|
||||
*/
|
||||
protected function executeInternal(): void
|
||||
{
|
||||
$pageUid = $this->parseOption('pageUid');
|
||||
$pageUid = (int)str_replace('pages_', '', (string)$pageUid);
|
||||
$additionalParameters = $this->parseOption('additionalParameters');
|
||||
$additionalParameters = is_string($additionalParameters) ? $additionalParameters : '';
|
||||
$additionalParameters = '&' . ltrim($additionalParameters, '&');
|
||||
$statusCode = (int)$this->parseOption('statusCode');
|
||||
$fragment = (string)$this->parseOption('fragment');
|
||||
|
||||
$this->finisherContext->cancel();
|
||||
$this->redirect($pageUid, $additionalParameters, $fragment, $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the request to another page.
|
||||
*
|
||||
* Redirect will be sent to the client which then performs another request to the new URI.
|
||||
*
|
||||
* NOTE: This method only supports web requests and will thrown an exception
|
||||
* if used with other request types.
|
||||
*
|
||||
* @param int $pageUid Target page uid. If NULL, the current page uid is used
|
||||
* @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other"
|
||||
* @see forward()
|
||||
*/
|
||||
protected function redirect(int $pageUid, string $additionalParameters, string $fragment, int $statusCode): never
|
||||
{
|
||||
$redirectUri = $this->finisherContext->getRequest()->getAttribute('currentContentObject')->createUrl([
|
||||
'parameter' => $pageUid,
|
||||
'additionalParams' => $additionalParameters,
|
||||
'section' => $fragment,
|
||||
]);
|
||||
$this->redirectToUri($redirectUri, $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the web request to another uri.
|
||||
*
|
||||
* NOTE: This method only supports web requests and will throw an exception if used with other request types.
|
||||
*
|
||||
* @param string $uri A string representation of a URI
|
||||
* @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other
|
||||
* @throws PropagateResponseException
|
||||
*/
|
||||
protected function redirectToUri(string $uri, int $statusCode = 303): never
|
||||
{
|
||||
$uri = $this->addBaseUriIfNecessary($uri);
|
||||
$response = new RedirectResponse($uri, $statusCode);
|
||||
// End processing and dispatching by throwing a PropagateResponseException with our response.
|
||||
// @todo: Should be changed to *return* a response instead, but this requires the ContentObjectRender
|
||||
// @todo: to deal with responses instead of strings, if the form is used in a fluid template rendered by the
|
||||
// @todo: FluidTemplateContentObject and the extbase bootstrap isn't used.
|
||||
throw new PropagateResponseException($response, 1477070964);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the base uri if not already in place.
|
||||
*
|
||||
* @param string $uri The URI
|
||||
*/
|
||||
protected function addBaseUriIfNecessary(string $uri): string
|
||||
{
|
||||
return GeneralUtility::locationHeaderUrl($uri, $this->finisherContext->getRequest());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
<?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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Form\Domain\Finishers;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
|
||||
use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
|
||||
|
||||
/**
|
||||
* This finisher saves the data from a submitted form into
|
||||
* a database table.
|
||||
*
|
||||
* Configuration
|
||||
* =============
|
||||
*
|
||||
* options.table (mandatory)
|
||||
* -------------
|
||||
* Save or update values into this table
|
||||
*
|
||||
* options.mode (default: insert)
|
||||
* ------------
|
||||
* Possible values are 'insert' or 'update'.
|
||||
*
|
||||
* insert: will create a new database row with the values from the
|
||||
* submitted form and/or some predefined values.
|
||||
* See options.elements and options.databaseFieldMappings
|
||||
* update: will update a given database row with the values from the
|
||||
* submitted form and/or some predefined values.
|
||||
* 'options.whereClause' is then required.
|
||||
*
|
||||
* options.whereClause
|
||||
* -------------------
|
||||
* This where clause will be used for a database update action
|
||||
*
|
||||
* options.elements
|
||||
* ----------------
|
||||
* Use this to map form element values to existing database columns.
|
||||
* Each key within options.elements has to match with a
|
||||
* form element identifier within your form definition.
|
||||
* The value for each key within options.elements is an array with
|
||||
* additional information.
|
||||
*
|
||||
* options.elements.<elementIdentifier>.mapOnDatabaseColumn (mandatory)
|
||||
* --------------------------------------------------------
|
||||
* The value from the submitted form element with the identifier
|
||||
* '<elementIdentifier>' will be written into this database column
|
||||
*
|
||||
* options.elements.<elementIdentifier>.skipIfValueIsEmpty (default: false)
|
||||
* ------------------------------------------------------
|
||||
* Set this to true if the database column should not be written
|
||||
* if the value from the submitted form element with the identifier
|
||||
* '<elementIdentifier>' is empty (think about password fields etc.)
|
||||
*
|
||||
* options.elements.<elementIdentifier>.hashed (default: false)
|
||||
* ------------------------------------------------------
|
||||
* Set this to true if the value from the submitted form element
|
||||
* should be hashed before writing into the database.
|
||||
*
|
||||
* options.elements.<elementIdentifier>.saveFileIdentifierInsteadOfUid (default: false)
|
||||
* -------------------------------------------------------------------
|
||||
* This setting only rules for form elements which creates a FAL object
|
||||
* like FileUpload or ImageUpload.
|
||||
* By default, the uid of the FAL object will be written into
|
||||
* the database column. Set this to true if you want to store the
|
||||
* FAL identifier (1:/user_uploads/some_uploaded_pic.jpg) instead.
|
||||
*
|
||||
* options.databaseColumnMappings
|
||||
* ------------------------------
|
||||
* Use this to map database columns to static values (which can be
|
||||
* made dynamic through typoscript overrides of course).
|
||||
* Each key within options.databaseColumnMappings has to match with a
|
||||
* existing database column.
|
||||
* The value for each key within options.databaseColumnMappings is an
|
||||
* array with additional information.
|
||||
*
|
||||
* This mapping is done *before* the options.elements mapping.
|
||||
* This means if you map a database column to a value through
|
||||
* options.databaseColumnMappings and map a submitted form element
|
||||
* value to the same database column, the submitted form element value
|
||||
* will override the value you set within options.databaseColumnMappings.
|
||||
*
|
||||
* options.databaseColumnMappings.<databaseColumnName>.value
|
||||
* ---------------------------------------------------------
|
||||
* The value which will be written to the database column.
|
||||
* You can use the FormRuntime accessor feature to access every
|
||||
* getable property from the TYPO3\CMS\Form\Domain\Runtime\FormRuntime
|
||||
* Read the description within
|
||||
* TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher::parseOption
|
||||
* In short: use something like {<elementIdentifier>} to get the value
|
||||
* from the submitted form element with the identifier
|
||||
* <elementIdentifier>
|
||||
*
|
||||
* Don't be confused. If you use the FormRuntime accessor feature within
|
||||
* options.databaseColumnMappings, the functionality is nearly equal
|
||||
* to the options.elements configuration.
|
||||
*
|
||||
* options.databaseColumnMappings.<databaseColumnName>.skipIfValueIsEmpty (default: false)
|
||||
* ---------------------------------------------------------------------
|
||||
* Set this to true if the database column should not be written
|
||||
* if the value from
|
||||
* options.databaseColumnMappings.<databaseColumnName>.value is empty.
|
||||
*
|
||||
* Example
|
||||
* =======
|
||||
*
|
||||
* finishers:
|
||||
* -
|
||||
* identifier: SaveToDatabase
|
||||
* options:
|
||||
* table: 'fe_users'
|
||||
* mode: update
|
||||
* whereClause:
|
||||
* uid: 1
|
||||
* databaseColumnMappings:
|
||||
* pid:
|
||||
* value: 1
|
||||
* elements:
|
||||
* text-1:
|
||||
* mapOnDatabaseColumn: 'first_name'
|
||||
* text-2:
|
||||
* mapOnDatabaseColumn: 'last_name'
|
||||
* text-3:
|
||||
* mapOnDatabaseColumn: 'username'
|
||||
* advancedpassword-1:
|
||||
* mapOnDatabaseColumn: 'password'
|
||||
* skipIfValueIsEmpty: true
|
||||
* hashed: true
|
||||
*
|
||||
* Multiple database operations
|
||||
* ============================
|
||||
*
|
||||
* You can write options as an array to perform multiple database operations.
|
||||
*
|
||||
* finishers:
|
||||
* -
|
||||
* identifier: SaveToDatabase
|
||||
* options:
|
||||
* 1:
|
||||
* table: 'my_table'
|
||||
* mode: insert
|
||||
* databaseColumnMappings:
|
||||
* some_column:
|
||||
* value: 'cool'
|
||||
* 2:
|
||||
* table: 'my_other_table'
|
||||
* mode: update
|
||||
* whereClause:
|
||||
* pid: 1
|
||||
* databaseColumnMappings:
|
||||
* some_other_column:
|
||||
* value: '{SaveToDatabase.insertedUids.1}'
|
||||
*
|
||||
* This would perform 2 database operations.
|
||||
* One insert and one update.
|
||||
* You can access the inserted uids with '{SaveToDatabase.insertedUids.<theArrayKeyNumberWithinOptions>}'
|
||||
* If you perform an insert operation, the value of the inserted database row will be stored
|
||||
* within the FinisherVariableProvider.
|
||||
* <theArrayKeyNumberWithinOptions> references to the numeric key within options
|
||||
* within which the insert operation is executed.
|
||||
*
|
||||
* Scope: frontend
|
||||
*/
|
||||
class SaveToDatabaseFinisher extends AbstractFinisher
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultOptions = [
|
||||
'table' => null,
|
||||
'mode' => 'insert',
|
||||
'whereClause' => [],
|
||||
'elements' => [],
|
||||
'databaseColumnMappings' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var \TYPO3\CMS\Core\Database\Connection
|
||||
*/
|
||||
protected $databaseConnection;
|
||||
|
||||
/**
|
||||
* Executes this finisher
|
||||
* @see AbstractFinisher::execute()
|
||||
*
|
||||
* @throws FinisherException
|
||||
*/
|
||||
protected function executeInternal(): void
|
||||
{
|
||||
$options = [];
|
||||
if (isset($this->options['table'])) {
|
||||
$options[] = $this->options;
|
||||
} else {
|
||||
$options = $this->options;
|
||||
}
|
||||
|
||||
foreach ($options as $optionKey => $option) {
|
||||
$this->options = $option;
|
||||
$this->process($optionKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare data for saving to database
|
||||
*/
|
||||
protected function prepareData(array $elementsConfiguration, array $databaseData): array
|
||||
{
|
||||
foreach ($this->getFormValues() as $elementIdentifier => $elementValue) {
|
||||
if (
|
||||
($elementValue === null || $elementValue === '')
|
||||
&& isset($elementsConfiguration[$elementIdentifier])
|
||||
&& isset($elementsConfiguration[$elementIdentifier]['skipIfValueIsEmpty'])
|
||||
&& $elementsConfiguration[$elementIdentifier]['skipIfValueIsEmpty'] === true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$element = $this->getElementByIdentifier($elementIdentifier);
|
||||
if (
|
||||
!$element
|
||||
|| !isset($elementsConfiguration[$elementIdentifier])
|
||||
|| !isset($elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn'])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($elementsConfiguration[$elementIdentifier]['saveFileIdentifierInsteadOfUid'])) {
|
||||
$saveFileIdentifierInsteadOfUid = (bool)$elementsConfiguration[$elementIdentifier]['saveFileIdentifierInsteadOfUid'];
|
||||
} else {
|
||||
$saveFileIdentifierInsteadOfUid = false;
|
||||
}
|
||||
|
||||
if ($elementValue instanceof FileReference) {
|
||||
$elementValue = $this->prepareFileForDatabase($elementValue, $saveFileIdentifierInsteadOfUid);
|
||||
} elseif ($elementValue instanceof ObjectStorage) {
|
||||
$fileIdentifiers = [];
|
||||
foreach ($elementValue as $singleElement) {
|
||||
if ($singleElement instanceof FileReference) {
|
||||
$fileIdentifiers[] = $this->prepareFileForDatabase($singleElement, $saveFileIdentifierInsteadOfUid);
|
||||
}
|
||||
}
|
||||
$elementValue = implode(',', $fileIdentifiers);
|
||||
} elseif (is_array($elementValue)) {
|
||||
$elementValue = implode(',', $elementValue);
|
||||
} elseif ($elementValue instanceof \DateTimeInterface) {
|
||||
$format = $elementsConfiguration[$elementIdentifier]['dateFormat'] ?? 'U';
|
||||
$elementValue = $elementValue->format($format);
|
||||
} elseif ($elementValue && ($elementsConfiguration[$elementIdentifier]['hashed'] ?? false) === true) {
|
||||
$hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('FE');
|
||||
$elementValue = $hashInstance->getHashedPassword($elementValue);
|
||||
}
|
||||
|
||||
$databaseData[$elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn']] = $elementValue;
|
||||
}
|
||||
return $databaseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the current database operation
|
||||
* @throws FinisherException
|
||||
*/
|
||||
protected function process(int $iterationCount): void
|
||||
{
|
||||
$this->throwExceptionOnInconsistentConfiguration();
|
||||
|
||||
$table = $this->parseOption('table');
|
||||
$table = is_string($table) ? $table : '';
|
||||
$elementsConfiguration = $this->parseOption('elements');
|
||||
$elementsConfiguration = is_array($elementsConfiguration) ? $elementsConfiguration : [];
|
||||
$databaseColumnMappingsConfiguration = $this->parseOption('databaseColumnMappings');
|
||||
|
||||
$this->databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
|
||||
|
||||
$databaseData = [];
|
||||
foreach ($databaseColumnMappingsConfiguration as $databaseColumnName => $databaseColumnConfiguration) {
|
||||
$value = $this->parseOption('databaseColumnMappings.' . $databaseColumnName . '.value');
|
||||
if (
|
||||
empty($value)
|
||||
&& ($databaseColumnConfiguration['skipIfValueIsEmpty'] ?? false) === true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$databaseData[$databaseColumnName] = $value;
|
||||
}
|
||||
|
||||
$databaseData = $this->prepareData($elementsConfiguration, $databaseData);
|
||||
|
||||
try {
|
||||
$this->saveToDatabase($databaseData, $table, $iterationCount);
|
||||
} catch (Exception $e) {
|
||||
throw new FinisherException(
|
||||
'Failed to save data to database table: ' . $table . '. Error message:' . $e->getMessage(),
|
||||
1754050114,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or insert the values from
|
||||
* $databaseData into the table $table
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function saveToDatabase(array $databaseData, string $table, int $iterationCount): void
|
||||
{
|
||||
if (!empty($databaseData)) {
|
||||
if ($this->parseOption('mode') === 'update') {
|
||||
$whereClause = $this->parseOption('whereClause');
|
||||
foreach ($whereClause as $columnName => $columnValue) {
|
||||
$whereClause[$columnName] = $this->parseOption('whereClause.' . $columnName);
|
||||
}
|
||||
$this->databaseConnection->update(
|
||||
$table,
|
||||
$databaseData,
|
||||
$whereClause
|
||||
);
|
||||
} else {
|
||||
$this->databaseConnection->insert($table, $databaseData);
|
||||
try {
|
||||
$insertedUid = (int)$this->databaseConnection->lastInsertId();
|
||||
} catch (Exception) {
|
||||
// Some database tables like sys_category_record_mm may not
|
||||
// have an "identity" (uid column). In this case DBAL may
|
||||
// throw an exception, which we gracefully handle here.
|
||||
$insertedUid = 0;
|
||||
}
|
||||
$this->finisherContext->getFinisherVariableProvider()->add(
|
||||
$this->shortFinisherIdentifier,
|
||||
'insertedUids.' . $iterationCount,
|
||||
$insertedUid
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception if some inconsistent configuration
|
||||
* are detected.
|
||||
*
|
||||
* @throws FinisherException
|
||||
*/
|
||||
protected function throwExceptionOnInconsistentConfiguration(): void
|
||||
{
|
||||
if (
|
||||
$this->parseOption('mode') === 'update'
|
||||
&& empty($this->parseOption('whereClause'))
|
||||
) {
|
||||
throw new FinisherException(
|
||||
'An empty option "whereClause" is not allowed in update mode.',
|
||||
1480469086
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the values of the submitted form
|
||||
*/
|
||||
protected function getFormValues(): array
|
||||
{
|
||||
return $this->finisherContext->getFormValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a form element object for a given identifier.
|
||||
*
|
||||
* @return FormElementInterface|null
|
||||
*/
|
||||
protected function getElementByIdentifier(string $elementIdentifier): ?FormElementInterface
|
||||
{
|
||||
return $this
|
||||
->finisherContext
|
||||
->getFormRuntime()
|
||||
->getFormDefinition()
|
||||
->getElementByIdentifier($elementIdentifier);
|
||||
}
|
||||
|
||||
protected function prepareFileForDatabase(FileReference $fileReference, bool $saveFileIdentifierInsteadOfUid = false): int|string
|
||||
{
|
||||
if ($saveFileIdentifierInsteadOfUid) {
|
||||
$elementValue = $fileReference->getOriginalResource()->getCombinedIdentifier();
|
||||
} else {
|
||||
$elementValue = $fileReference->getOriginalResource()->getProperty('uid_local');
|
||||
}
|
||||
|
||||
return $elementValue;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user