TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
FormRuntime.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Form\Domain\Runtime;
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
26 use TYPO3\CMS\Extbase\Property\Exception as PropertyException;
38 
78 class FormRuntime implements RootRenderableInterface, \ArrayAccess
79 {
80  const HONEYPOT_NAME_SESSION_IDENTIFIER = 'tx_form_honeypot_name_';
81 
85  protected $objectManager;
86 
90  protected $formDefinition;
91 
95  protected $request;
96 
100  protected $response;
101 
105  protected $formState;
106 
117  protected $currentPage = null;
118 
125  protected $lastDisplayedPage = null;
126 
130  protected $hashService;
131 
137  public function injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
138  {
139  $this->hashService = $hashService;
140  }
141 
146  public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
147  {
148  $this->objectManager = $objectManager;
149  }
150 
158  {
159  $this->formDefinition = $formDefinition;
160  $arguments = $request->getArguments();
161  $this->request = clone $request;
162  $formIdentifier = $this->formDefinition->getIdentifier();
163  if (isset($arguments[$formIdentifier])) {
164  $this->request->setArguments($arguments[$formIdentifier]);
165  }
166 
167  $this->response = $response;
168  }
169 
174  public function initializeObject()
175  {
179 
180  if (!$this->isFirstRequest() && $this->getRequest()->getMethod() === 'POST') {
182  }
183 
184  $this->renderHoneypot();
185  }
186 
190  protected function initializeFormStateFromRequest()
191  {
192  $serializedFormStateWithHmac = $this->request->getInternalArgument('__state');
193  if ($serializedFormStateWithHmac === null) {
194  $this->formState = GeneralUtility::makeInstance(FormState::class);
195  } else {
196  $serializedFormState = $this->hashService->validateAndStripHmac($serializedFormStateWithHmac);
197  $this->formState = unserialize(base64_decode($serializedFormState));
198  }
199  }
200 
204  protected function initializeCurrentPageFromRequest()
205  {
206  if (!$this->formState->isFormSubmitted()) {
207  $this->currentPage = $this->formDefinition->getPageByIndex(0);
208  return;
209  }
210  $this->lastDisplayedPage = $this->formDefinition->getPageByIndex($this->formState->getLastDisplayedPageIndex());
211 
212  // We know now that lastDisplayedPage is filled
213  $currentPageIndex = (int)$this->request->getInternalArgument('__currentPage');
214  if ($currentPageIndex > $this->lastDisplayedPage->getIndex() + 1) {
215  // We only allow jumps to following pages
216  $currentPageIndex = $this->lastDisplayedPage->getIndex() + 1;
217  }
218 
219  // We now know that the user did not try to skip a page
220  if ($currentPageIndex === count($this->formDefinition->getPages())) {
221  // Last Page
222  $this->currentPage = null;
223  } else {
224  $this->currentPage = $this->formDefinition->getPageByIndex($currentPageIndex);
225  }
226  }
227 
231  protected function initializeHoneypotFromRequest()
232  {
233  $renderingOptions = $this->formDefinition->getRenderingOptions();
234  if (!isset($renderingOptions['honeypot']['enable']) || $renderingOptions['honeypot']['enable'] === false || TYPO3_MODE === 'BE') {
235  return;
236  }
237 
238  ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']);
239 
240  if (!$this->isFirstRequest()) {
241  $elementsCount = count($this->lastDisplayedPage->getElements());
242  if ($elementsCount === 0) {
243  return;
244  }
245 
246  $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage);
247  if ($honeypotNameFromSession) {
248  $honeypotElement = $this->lastDisplayedPage->createElement($honeypotNameFromSession, $renderingOptions['honeypot']['formElementToUse']);
249  $validator = $this->objectManager->get(EmptyValidator::class);
250  $honeypotElement->addValidator($validator);
251  }
252  }
253  }
254 
258  protected function renderHoneypot()
259  {
260  $renderingOptions = $this->formDefinition->getRenderingOptions();
261  if (!isset($renderingOptions['honeypot']['enable']) || $renderingOptions['honeypot']['enable'] === false || TYPO3_MODE === 'BE') {
262  return;
263  }
264 
265  ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']);
266 
267  if (!$this->isAfterLastPage()) {
268  $elementsCount = count($this->currentPage->getElements());
269  if ($elementsCount === 0) {
270  return;
271  }
272 
273  if (!$this->isFirstRequest()) {
274  $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage);
275  if ($honeypotNameFromSession) {
276  $honeypotElement = $this->formDefinition->getElementByIdentifier($honeypotNameFromSession);
277  if ($honeypotElement instanceof FormElementInterface) {
278  $this->lastDisplayedPage->removeElement($honeypotElement);
279  }
280  }
281  }
282 
283  $elementsCount = count($this->currentPage->getElements());
284  $randomElementNumber = mt_rand(0, ($elementsCount - 1));
285  $honeypotName = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, mt_rand(5, 26));
286 
287  $referenceElement = $this->currentPage->getElements()[$randomElementNumber];
288  $honeypotElement = $this->currentPage->createElement($honeypotName, $renderingOptions['honeypot']['formElementToUse']);
289  $validator = $this->objectManager->get(EmptyValidator::class);
290 
291  $honeypotElement->addValidator($validator);
292  if (mt_rand(0, 1) === 1) {
293  $this->currentPage->moveElementAfter($honeypotElement, $referenceElement);
294  } else {
295  $this->currentPage->moveElementBefore($honeypotElement, $referenceElement);
296  }
297  $this->setHoneypotNameInSession($this->currentPage, $honeypotName);
298  }
299  }
300 
305  protected function getHoneypotNameFromSession(Page $page)
306  {
307  if ($this->getTypoScriptFrontendController()->loginUser) {
308  $honeypotNameFromSession = $this->getTypoScriptFrontendController()->fe_user->getKey(
309  'user',
310  self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier()
311  );
312  } else {
313  $honeypotNameFromSession = $this->getTypoScriptFrontendController()->fe_user->getKey(
314  'ses',
315  self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier()
316  );
317  }
318  return $honeypotNameFromSession;
319  }
320 
326  protected function setHoneypotNameInSession(Page $page, string $honeypotName)
327  {
328  if ($this->getTypoScriptFrontendController()->loginUser) {
329  $this->getTypoScriptFrontendController()->fe_user->setKey(
330  'user',
331  self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(),
332  $honeypotName
333  );
334  } else {
335  $this->getTypoScriptFrontendController()->fe_user->setKey(
336  'ses',
337  self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(),
338  $honeypotName
339  );
340  }
341  }
342 
348  protected function isAfterLastPage(): bool
349  {
350  return $this->currentPage === null;
351  }
352 
358  protected function isFirstRequest(): bool
359  {
360  return $this->lastDisplayedPage === null;
361  }
362 
366  protected function processSubmittedFormValues()
367  {
368  $result = $this->mapAndValidatePage($this->lastDisplayedPage);
369  if ($result->hasErrors() && !$this->userWentBackToPreviousStep()) {
370  $this->currentPage = $this->lastDisplayedPage;
371  $this->request->setOriginalRequestMappingResults($result);
372  }
373  }
374 
380  protected function userWentBackToPreviousStep(): bool
381  {
382  return !$this->isAfterLastPage() && !$this->isFirstRequest() && $this->currentPage->getIndex() < $this->lastDisplayedPage->getIndex();
383  }
384 
390  protected function mapAndValidatePage(Page $page): Result
391  {
392  $result = $this->objectManager->get(Result::class);
393  $requestArguments = $this->request->getArguments();
394 
395  $propertyPathsForWhichPropertyMappingShouldHappen = [];
396  $registerPropertyPaths = function ($propertyPath) use (&$propertyPathsForWhichPropertyMappingShouldHappen) {
397  $propertyPathParts = explode('.', $propertyPath);
398  $accumulatedPropertyPathParts = [];
399  foreach ($propertyPathParts as $propertyPathPart) {
400  $accumulatedPropertyPathParts[] = $propertyPathPart;
401  $temporaryPropertyPath = implode('.', $accumulatedPropertyPathParts);
402  $propertyPathsForWhichPropertyMappingShouldHappen[$temporaryPropertyPath] = $temporaryPropertyPath;
403  }
404  };
405  foreach ($page->getElementsRecursively() as $element) {
406  try {
407  $value = ArrayUtility::getValueByPath($requestArguments, $element->getIdentifier(), '.');
408  } catch (\RuntimeException $exception) {
409  $value = null;
410  }
411  $element->onSubmit($this, $value, $requestArguments);
412 
413  $this->formState->setFormValue($element->getIdentifier(), $value);
414  $registerPropertyPaths($element->getIdentifier());
415  }
416 
417  // The more parts the path has, the more early it is processed
418  usort($propertyPathsForWhichPropertyMappingShouldHappen, function ($a, $b) {
419  return substr_count($b, '.') - substr_count($a, '.');
420  });
421 
422  $processingRules = $this->formDefinition->getProcessingRules();
423 
424  foreach ($propertyPathsForWhichPropertyMappingShouldHappen as $propertyPath) {
425  if (isset($processingRules[$propertyPath])) {
426  $processingRule = $processingRules[$propertyPath];
427  $value = $this->formState->getFormValue($propertyPath);
428  try {
429  $value = $processingRule->process($value);
430  } catch (PropertyException $exception) {
431  throw new PropertyMappingException(
432  'Failed to process FormValue at "' . $propertyPath . '" from "' . gettype($value) . '" to "' . $processingRule->getDataType() . '"',
433  1480024933,
434  $exception
435  );
436  }
437  $result->forProperty($propertyPath)->merge($processingRule->getProcessingMessages());
438  $this->formState->setFormValue($propertyPath, $value);
439  }
440  }
441 
442  return $result;
443  }
444 
455  public function overrideCurrentPage(int $pageIndex)
456  {
457  $this->currentPage = $this->formDefinition->getPageByIndex($pageIndex);
458  }
459 
467  public function render()
468  {
469  if ($this->isAfterLastPage()) {
470  $this->invokeFinishers();
471  return $this->response->getContent();
472  }
473 
474  $this->formState->setLastDisplayedPageIndex($this->currentPage->getIndex());
475 
476  if ($this->formDefinition->getRendererClassName() === null) {
477  throw new RenderingException(sprintf('The form definition "%s" does not have a rendererClassName set.', $this->formDefinition->getIdentifier()), 1326095912);
478  }
479  $rendererClassName = $this->formDefinition->getRendererClassName();
480  $renderer = $this->objectManager->get($rendererClassName);
481  if (!($renderer instanceof RendererInterface)) {
482  throw new RenderingException(sprintf('The renderer "%s" des not implement RendererInterface', $rendererClassName), 1326096024);
483  }
484 
485  $controllerContext = $this->getControllerContext();
486 
487  $renderer->setControllerContext($controllerContext);
488  $renderer->setFormRuntime($this);
489  return $renderer->render($this);
490  }
491 
497  protected function invokeFinishers()
498  {
499  $finisherContext = $this->objectManager->get(FinisherContext::class,
500  $this,
501  $this->getControllerContext()
502  );
503  foreach ($this->formDefinition->getFinishers() as $finisher) {
504  $finisher->execute($finisherContext);
505  if ($finisherContext->isCancelled()) {
506  break;
507  }
508  }
509  }
510 
515  public function getIdentifier(): string
516  {
517  return $this->formDefinition->getIdentifier();
518  }
519 
529  public function getRequest(): Request
530  {
531  return $this->request;
532  }
533 
543  public function getResponse(): Response
544  {
545  return $this->response;
546  }
547 
554  public function getCurrentPage(): Page
555  {
556  return $this->currentPage;
557  }
558 
565  public function getPreviousPage()
566  {
567  $previousPageIndex = $this->currentPage->getIndex() - 1;
568  if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) {
569  return $this->formDefinition->getPageByIndex($previousPageIndex);
570  }
571  return null;
572  }
573 
580  public function getNextPage()
581  {
582  $nextPageIndex = $this->currentPage->getIndex() + 1;
583  if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) {
584  return $this->formDefinition->getPageByIndex($nextPageIndex);
585  }
586  return null;
587  }
588 
593  {
594  $uriBuilder = $this->objectManager->get(UriBuilder::class);
595  $uriBuilder->setRequest($this->request);
596  $controllerContext = $this->objectManager->get(ControllerContext::class);
597  $controllerContext->setRequest($this->request);
598  $controllerContext->setResponse($this->response);
599  $controllerContext->setArguments($this->objectManager->get(Arguments::class, []));
600  $controllerContext->setUriBuilder($uriBuilder);
601  return $controllerContext;
602  }
603 
612  public function getType(): string
613  {
614  return $this->formDefinition->getType();
615  }
616 
622  public function offsetExists($identifier)
623  {
624  if ($this->getElementValue($identifier) !== null) {
625  return true;
626  }
627 
628  if (is_callable([$this, 'get' . ucfirst($identifier)])) {
629  return true;
630  }
631  if (is_callable([$this, 'has' . ucfirst($identifier)])) {
632  return true;
633  }
634  if (is_callable([$this, 'is' . ucfirst($identifier)])) {
635  return true;
636  }
637  if (property_exists($this, $identifier)) {
638  $propertyReflection = new PropertyReflection($this, $identifier);
639  return $propertyReflection->isPublic();
640  }
641 
642  return false;
643  }
644 
650  public function offsetGet($identifier)
651  {
652  if ($this->getElementValue($identifier) !== null) {
653  return $this->getElementValue($identifier);
654  }
655  $getterMethodName = 'get' . ucfirst($identifier);
656  if (is_callable([$this, $getterMethodName])) {
657  return $this->{$getterMethodName}();
658  }
659  return null;
660  }
661 
668  public function offsetSet($identifier, $value)
669  {
670  $this->formState->setFormValue($identifier, $value);
671  }
672 
678  public function offsetUnset($identifier)
679  {
680  $this->formState->setFormValue($identifier, null);
681  }
682 
690  public function getElementValue(string $identifier)
691  {
692  $formValue = $this->formState->getFormValue($identifier);
693  if ($formValue !== null) {
694  return $formValue;
695  }
696  return $this->formDefinition->getElementDefaultValueByIdentifier($identifier);
697  }
698 
703  public function getPages(): array
704  {
705  return $this->formDefinition->getPages();
706  }
707 
712  public function getFormState(): FormState
713  {
714  return $this->formState;
715  }
716 
723  public function getRenderingOptions(): array
724  {
725  return $this->formDefinition->getRenderingOptions();
726  }
727 
735  public function getRendererClassName(): string
736  {
737  return $this->formDefinition->getRendererClassName();
738  }
739 
746  public function getLabel(): string
747  {
748  return $this->formDefinition->getLabel();
749  }
750 
758  {
759  return $this->formDefinition;
760  }
761 
771  public function beforeRendering(FormRuntime $formRuntime)
772  {
773  }
774 
778  protected function getTypoScriptFrontendController()
779  {
780  return $GLOBALS['TSFE'];
781  }
782 }
static assertAllArrayKeysAreValid(array $arrayToTest, array $allowedArrayKeys)
injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
beforeRendering(FormRuntime $formRuntime)
__construct(FormDefinition $formDefinition, Request $request, Response $response)
static getValueByPath(array $array, $path, $delimiter= '/')
setHoneypotNameInSession(Page $page, string $honeypotName)
setRequest(\TYPO3\CMS\Extbase\Mvc\Request $request)
if(TYPO3_MODE=== 'BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static makeInstance($className,...$constructorArguments)
injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)