TYPO3 CMS  TYPO3_8-7
FormRuntime.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types = 1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It originated from the Neos.Form package (www.neos.io)
9  *
10  * It is free software; you can redistribute it and/or modify it under
11  * the terms of the GNU General Public License, either version 2
12  * of the License, or any later version.
13  *
14  * For the full copyright and license information, please read the
15  * LICENSE.txt file that was distributed with this source code.
16  *
17  * The TYPO3 project - inspiring people to share!
18  */
19 
40 
80 class FormRuntime implements RootRenderableInterface, \ArrayAccess
81 {
82  const HONEYPOT_NAME_SESSION_IDENTIFIER = 'tx_form_honeypot_name_';
83 
87  protected $objectManager;
88 
92  protected $formDefinition;
93 
97  protected $request;
98 
102  protected $response;
103 
107  protected $formState;
108 
119  protected $currentPage = null;
120 
127  protected $lastDisplayedPage = null;
128 
132  protected $hashService;
133 
138  public function injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
139  {
140  $this->hashService = $hashService;
141  }
142 
147  public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
148  {
149  $this->objectManager = $objectManager;
150  }
151 
159  {
160  $this->formDefinition = $formDefinition;
161  $arguments = $request->getArguments();
162  $this->request = clone $request;
163  $formIdentifier = $this->formDefinition->getIdentifier();
164  if (isset($arguments[$formIdentifier])) {
165  $this->request->setArguments($arguments[$formIdentifier]);
166  }
167 
168  $this->response = $response;
169  }
170 
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  if (
209  isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'])
210  && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'])
211  ) {
212  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] as $className) {
213  $hookObj = GeneralUtility::makeInstance($className);
214  if (method_exists($hookObj, 'afterInitializeCurrentPage')) {
215  $this->currentPage = $hookObj->afterInitializeCurrentPage(
216  $this,
217  $this->currentPage,
218  null,
219  $this->request->getArguments()
220  );
221  }
222  }
223  }
224  return;
225  }
226  $this->lastDisplayedPage = $this->formDefinition->getPageByIndex($this->formState->getLastDisplayedPageIndex());
227 
228  // We know now that lastDisplayedPage is filled
229  $currentPageIndex = (int)$this->request->getInternalArgument('__currentPage');
230  if ($currentPageIndex > $this->lastDisplayedPage->getIndex() + 1) {
231  // We only allow jumps to following pages
232  $currentPageIndex = $this->lastDisplayedPage->getIndex() + 1;
233  }
234 
235  // We now know that the user did not try to skip a page
236  if ($currentPageIndex === count($this->formDefinition->getPages())) {
237  // Last Page
238  $this->currentPage = null;
239  } else {
240  $this->currentPage = $this->formDefinition->getPageByIndex($currentPageIndex);
241  }
242 
243  if (
244  isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'])
245  && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'])
246  ) {
247  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] as $className) {
248  $hookObj = GeneralUtility::makeInstance($className);
249  if (method_exists($hookObj, 'afterInitializeCurrentPage')) {
250  $this->currentPage = $hookObj->afterInitializeCurrentPage(
251  $this,
252  $this->currentPage,
253  $this->lastDisplayedPage,
254  $this->request->getArguments()
255  );
256  }
257  }
258  }
259  }
260 
264  protected function initializeHoneypotFromRequest()
265  {
266  $renderingOptions = $this->formDefinition->getRenderingOptions();
267  if (!isset($renderingOptions['honeypot']['enable']) || $renderingOptions['honeypot']['enable'] === false || TYPO3_MODE === 'BE') {
268  return;
269  }
270 
271  ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']);
272 
273  if (!$this->isFirstRequest()) {
274  $elementsCount = count($this->lastDisplayedPage->getElements());
275  if ($elementsCount === 0) {
276  return;
277  }
278 
279  $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage);
280  if ($honeypotNameFromSession) {
281  $honeypotElement = $this->lastDisplayedPage->createElement($honeypotNameFromSession, $renderingOptions['honeypot']['formElementToUse']);
282  $validator = $this->objectManager->get(EmptyValidator::class);
283  $honeypotElement->addValidator($validator);
284  }
285  }
286  }
287 
291  protected function renderHoneypot()
292  {
293  $renderingOptions = $this->formDefinition->getRenderingOptions();
294  if (!isset($renderingOptions['honeypot']['enable']) || $renderingOptions['honeypot']['enable'] === false || TYPO3_MODE === 'BE') {
295  return;
296  }
297 
298  ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']);
299 
300  if (!$this->isAfterLastPage()) {
301  $elementsCount = count($this->currentPage->getElements());
302  if ($elementsCount === 0) {
303  return;
304  }
305 
306  if (!$this->isFirstRequest()) {
307  $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage);
308  if ($honeypotNameFromSession) {
309  $honeypotElement = $this->formDefinition->getElementByIdentifier($honeypotNameFromSession);
310  if ($honeypotElement instanceof FormElementInterface) {
311  $this->lastDisplayedPage->removeElement($honeypotElement);
312  }
313  }
314  }
315 
316  $elementsCount = count($this->currentPage->getElements());
317  $randomElementNumber = mt_rand(0, ($elementsCount - 1));
318  $honeypotName = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, mt_rand(5, 26));
319 
320  $referenceElement = $this->currentPage->getElements()[$randomElementNumber];
321  $honeypotElement = $this->currentPage->createElement($honeypotName, $renderingOptions['honeypot']['formElementToUse']);
322  $validator = $this->objectManager->get(EmptyValidator::class);
323 
324  $honeypotElement->addValidator($validator);
325  if (mt_rand(0, 1) === 1) {
326  $this->currentPage->moveElementAfter($honeypotElement, $referenceElement);
327  } else {
328  $this->currentPage->moveElementBefore($honeypotElement, $referenceElement);
329  }
330  $this->setHoneypotNameInSession($this->currentPage, $honeypotName);
331  }
332  }
333 
338  protected function getHoneypotNameFromSession(Page $page)
339  {
340  if ($this->getTypoScriptFrontendController()->loginUser) {
341  $honeypotNameFromSession = $this->getTypoScriptFrontendController()->fe_user->getKey(
342  'user',
343  self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier()
344  );
345  } else {
346  $honeypotNameFromSession = $this->getTypoScriptFrontendController()->fe_user->getKey(
347  'ses',
348  self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier()
349  );
350  }
351  return $honeypotNameFromSession;
352  }
353 
358  protected function setHoneypotNameInSession(Page $page, string $honeypotName)
359  {
360  if ($this->getTypoScriptFrontendController()->loginUser) {
361  $this->getTypoScriptFrontendController()->fe_user->setKey(
362  'user',
363  self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(),
364  $honeypotName
365  );
366  } else {
367  $this->getTypoScriptFrontendController()->fe_user->setKey(
368  'ses',
369  self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(),
370  $honeypotName
371  );
372  }
373  }
374 
380  protected function isAfterLastPage(): bool
381  {
382  return $this->currentPage === null;
383  }
384 
390  protected function isFirstRequest(): bool
391  {
392  return $this->lastDisplayedPage === null;
393  }
394 
398  protected function processSubmittedFormValues()
399  {
400  $result = $this->mapAndValidatePage($this->lastDisplayedPage);
401  if ($result->hasErrors() && !$this->userWentBackToPreviousStep()) {
402  $this->currentPage = $this->lastDisplayedPage;
403  $this->request->setOriginalRequestMappingResults($result);
404  }
405  }
406 
412  protected function userWentBackToPreviousStep(): bool
413  {
414  return !$this->isAfterLastPage() && !$this->isFirstRequest() && $this->currentPage->getIndex() < $this->lastDisplayedPage->getIndex();
415  }
416 
422  protected function mapAndValidatePage(Page $page): Result
423  {
424  $result = $this->objectManager->get(Result::class);
425  $requestArguments = $this->request->getArguments();
426 
427  $propertyPathsForWhichPropertyMappingShouldHappen = [];
428  $registerPropertyPaths = function ($propertyPath) use (&$propertyPathsForWhichPropertyMappingShouldHappen) {
429  $propertyPathParts = explode('.', $propertyPath);
430  $accumulatedPropertyPathParts = [];
431  foreach ($propertyPathParts as $propertyPathPart) {
432  $accumulatedPropertyPathParts[] = $propertyPathPart;
433  $temporaryPropertyPath = implode('.', $accumulatedPropertyPathParts);
434  $propertyPathsForWhichPropertyMappingShouldHappen[$temporaryPropertyPath] = $temporaryPropertyPath;
435  }
436  };
437 
438  $value = null;
439 
440  GeneralUtility::deprecationLog('EXT:form - calls for "onSubmit" are deprecated since TYPO3 v8 and will be removed in TYPO3 v9');
441  $page->onSubmit($this, $value, $requestArguments);
442 
443  if (
444  isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'])
445  && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'])
446  ) {
447  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] as $className) {
448  $hookObj = GeneralUtility::makeInstance($className);
449  if (method_exists($hookObj, 'afterSubmit')) {
450  $value = $hookObj->afterSubmit(
451  $this,
452  $page,
453  $value,
454  $requestArguments
455  );
456  }
457  }
458  }
459 
460  foreach ($page->getElementsRecursively() as $element) {
461  try {
462  $value = ArrayUtility::getValueByPath($requestArguments, $element->getIdentifier(), '.');
463  } catch (\RuntimeException $exception) {
464  $value = null;
465  }
466 
467  GeneralUtility::deprecationLog('EXT:form - calls for "onSubmit" are deprecated since TYPO3 v8 and will be removed in TYPO3 v9');
468  $element->onSubmit($this, $value, $requestArguments);
469 
470  if (
471  isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'])
472  && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'])
473  ) {
474  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] as $className) {
475  $hookObj = GeneralUtility::makeInstance($className);
476  if (method_exists($hookObj, 'afterSubmit')) {
477  $value = $hookObj->afterSubmit(
478  $this,
479  $element,
480  $value,
481  $requestArguments
482  );
483  }
484  }
485  }
486 
487  $this->formState->setFormValue($element->getIdentifier(), $value);
488  $registerPropertyPaths($element->getIdentifier());
489  }
490 
491  // The more parts the path has, the more early it is processed
492  usort($propertyPathsForWhichPropertyMappingShouldHappen, function ($a, $b) {
493  return substr_count($b, '.') - substr_count($a, '.');
494  });
495 
496  $processingRules = $this->formDefinition->getProcessingRules();
497 
498  foreach ($propertyPathsForWhichPropertyMappingShouldHappen as $propertyPath) {
499  if (isset($processingRules[$propertyPath])) {
500  $processingRule = $processingRules[$propertyPath];
501  $value = $this->formState->getFormValue($propertyPath);
502  try {
503  $value = $processingRule->process($value);
504  } catch (PropertyException $exception) {
505  throw new PropertyMappingException(
506  'Failed to process FormValue at "' . $propertyPath . '" from "' . gettype($value) . '" to "' . $processingRule->getDataType() . '"',
507  1480024933,
508  $exception
509  );
510  }
511  $result->forProperty($this->getIdentifier() . '.' . $propertyPath)->merge($processingRule->getProcessingMessages());
512  $this->formState->setFormValue($propertyPath, $value);
513  }
514  }
515 
516  return $result;
517  }
518 
528  public function overrideCurrentPage(int $pageIndex)
529  {
530  $this->currentPage = $this->formDefinition->getPageByIndex($pageIndex);
531  }
532 
540  public function render()
541  {
542  if ($this->isAfterLastPage()) {
543  return $this->invokeFinishers();
544  }
545 
546  $this->formState->setLastDisplayedPageIndex($this->currentPage->getIndex());
547 
548  if ($this->formDefinition->getRendererClassName() === '') {
549  throw new RenderingException(sprintf('The form definition "%s" does not have a rendererClassName set.', $this->formDefinition->getIdentifier()), 1326095912);
550  }
551  $rendererClassName = $this->formDefinition->getRendererClassName();
552  $renderer = $this->objectManager->get($rendererClassName);
553  if (!($renderer instanceof RendererInterface)) {
554  throw new RenderingException(sprintf('The renderer "%s" des not implement RendererInterface', $rendererClassName), 1326096024);
555  }
556 
557  $controllerContext = $this->getControllerContext();
558 
559  $renderer->setControllerContext($controllerContext);
560  $renderer->setFormRuntime($this);
561  return $renderer->render($this);
562  }
563 
569  protected function invokeFinishers(): string
570  {
571  $finisherContext = $this->objectManager->get(
572  FinisherContext::class,
573  $this,
574  $this->getControllerContext()
575  );
576 
577  $output = '';
578  $originalContent = $this->response->getContent();
579  $this->response->setContent(null);
580  foreach ($this->formDefinition->getFinishers() as $finisher) {
581  $finisherOutput = $finisher->execute($finisherContext);
582  if (is_string($finisherOutput) && !empty($finisherOutput)) {
583  $output .= $finisherOutput;
584  } else {
585  $output .= $this->response->getContent();
586  $this->response->setContent(null);
587  }
588 
589  if ($finisherContext->isCancelled()) {
590  break;
591  }
592  }
593  $this->response->setContent($originalContent);
594 
595  return $output;
596  }
597 
602  public function getIdentifier(): string
603  {
604  return $this->formDefinition->getIdentifier();
605  }
606 
616  public function getRequest(): Request
617  {
618  return $this->request;
619  }
620 
630  public function getResponse(): Response
631  {
632  return $this->response;
633  }
634 
641  public function getCurrentPage(): Page
642  {
643  return $this->currentPage;
644  }
645 
652  public function getPreviousPage()
653  {
654  $previousPageIndex = $this->currentPage->getIndex() - 1;
655  if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) {
656  return $this->formDefinition->getPageByIndex($previousPageIndex);
657  }
658  return null;
659  }
660 
667  public function getNextPage()
668  {
669  $nextPageIndex = $this->currentPage->getIndex() + 1;
670  if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) {
671  return $this->formDefinition->getPageByIndex($nextPageIndex);
672  }
673  return null;
674  }
675 
680  {
681  $uriBuilder = $this->objectManager->get(UriBuilder::class);
682  $uriBuilder->setRequest($this->request);
683  $controllerContext = $this->objectManager->get(ControllerContext::class);
684  $controllerContext->setRequest($this->request);
685  $controllerContext->setResponse($this->response);
686  $controllerContext->setArguments($this->objectManager->get(Arguments::class, []));
687  $controllerContext->setUriBuilder($uriBuilder);
688  return $controllerContext;
689  }
690 
699  public function getType(): string
700  {
701  return $this->formDefinition->getType();
702  }
703 
709  public function offsetExists($identifier)
710  {
711  if ($this->getElementValue($identifier) !== null) {
712  return true;
713  }
714 
715  if (is_callable([$this, 'get' . ucfirst($identifier)])) {
716  return true;
717  }
718  if (is_callable([$this, 'has' . ucfirst($identifier)])) {
719  return true;
720  }
721  if (is_callable([$this, 'is' . ucfirst($identifier)])) {
722  return true;
723  }
724  if (property_exists($this, $identifier)) {
725  $propertyReflection = new PropertyReflection($this, $identifier);
726  return $propertyReflection->isPublic();
727  }
728 
729  return false;
730  }
731 
737  public function offsetGet($identifier)
738  {
739  if ($this->getElementValue($identifier) !== null) {
740  return $this->getElementValue($identifier);
741  }
742  $getterMethodName = 'get' . ucfirst($identifier);
743  if (is_callable([$this, $getterMethodName])) {
744  return $this->{$getterMethodName}();
745  }
746  return null;
747  }
748 
754  public function offsetSet($identifier, $value)
755  {
756  $this->formState->setFormValue($identifier, $value);
757  }
758 
763  public function offsetUnset($identifier)
764  {
765  $this->formState->setFormValue($identifier, null);
766  }
767 
775  public function getElementValue(string $identifier)
776  {
777  $formValue = $this->formState->getFormValue($identifier);
778  if ($formValue !== null) {
779  return $formValue;
780  }
781  return $this->formDefinition->getElementDefaultValueByIdentifier($identifier);
782  }
783 
788  public function getPages(): array
789  {
790  return $this->formDefinition->getPages();
791  }
792 
797  public function getFormState(): FormState
798  {
799  return $this->formState;
800  }
801 
808  public function getRenderingOptions(): array
809  {
810  return $this->formDefinition->getRenderingOptions();
811  }
812 
820  public function getRendererClassName(): string
821  {
822  return $this->formDefinition->getRendererClassName();
823  }
824 
831  public function getLabel(): string
832  {
833  return $this->formDefinition->getLabel();
834  }
835 
842  public function getTemplateName(): string
843  {
844  return $this->formDefinition->getTemplateName();
845  }
846 
854  {
855  return $this->formDefinition;
856  }
857 
867  public function beforeRendering(FormRuntime $formRuntime)
868  {
870  }
871 
875  protected function getTypoScriptFrontendController()
876  {
877  return $GLOBALS['TSFE'];
878  }
879 }
setRequest(\TYPO3\CMS\Extbase\Mvc\Request $request)
injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
beforeRendering(FormRuntime $formRuntime)
static getValueByPath(array $array, $path, $delimiter='/')
injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
static assertAllArrayKeysAreValid(array $arrayToTest, array $allowedArrayKeys)
static makeInstance($className,... $constructorArguments)
__construct(FormDefinition $formDefinition, Request $request, Response $response)
setHoneypotNameInSession(Page $page, string $honeypotName)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
onSubmit(FormRuntime $formRuntime, &$elementValue, array $requestArguments=[])