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.} * or {} * * Both examples are equal to "$formRuntime->getFormState()->getFormValues()[]" * 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; } }