927d173f81ae98da66eef02cc75e7612699270bd
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Domain / Runtime / FormRuntime.php
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 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
20 use Psr\Http\Message\ServerRequestInterface;
21 use TYPO3\CMS\Core\Context\Context;
22 use TYPO3\CMS\Core\ExpressionLanguage\Resolver;
23 use TYPO3\CMS\Core\Http\ServerRequest;
24 use TYPO3\CMS\Core\Site\Entity\Site;
25 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
26 use TYPO3\CMS\Core\Utility\ArrayUtility;
27 use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
28 use TYPO3\CMS\Core\Utility\GeneralUtility;
29 use TYPO3\CMS\Extbase\Error\Result;
30 use TYPO3\CMS\Extbase\Mvc\Controller\Arguments;
31 use TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext;
32 use TYPO3\CMS\Extbase\Mvc\Web\Request;
33 use TYPO3\CMS\Extbase\Mvc\Web\Response;
34 use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
35 use TYPO3\CMS\Extbase\Property\Exception as PropertyException;
36 use TYPO3\CMS\Form\Domain\Exception\RenderingException;
37 use TYPO3\CMS\Form\Domain\Finishers\FinisherContext;
38 use TYPO3\CMS\Form\Domain\Finishers\FinisherInterface;
39 use TYPO3\CMS\Form\Domain\Model\FormDefinition;
40 use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
41 use TYPO3\CMS\Form\Domain\Model\FormElements\Page;
42 use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
43 use TYPO3\CMS\Form\Domain\Model\Renderable\VariableRenderableInterface;
44 use TYPO3\CMS\Form\Domain\Renderer\RendererInterface;
45 use TYPO3\CMS\Form\Domain\Runtime\Exception\PropertyMappingException;
46 use TYPO3\CMS\Form\Exception as FormException;
47 use TYPO3\CMS\Form\Mvc\Validation\EmptyValidator;
48 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
49 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
50
51 /**
52 * This class implements the *runtime logic* of a form, i.e. deciding which
53 * page is shown currently, what the current values of the form are, trigger
54 * validation and property mapping.
55 *
56 * You generally receive an instance of this class by calling {@link \TYPO3\CMS\Form\Domain\Model\FormDefinition::bind}.
57 *
58 * Rendering a Form
59 * ================
60 *
61 * That's easy, just call render() on the FormRuntime:
62 *
63 * /---code php
64 * $form = $formDefinition->bind($request, $response);
65 * $renderedForm = $form->render();
66 * \---
67 *
68 * Accessing Form Values
69 * =====================
70 *
71 * In order to get the values the user has entered into the form, you can access
72 * this object like an array: If a form field with the identifier *firstName*
73 * exists, you can do **$form['firstName']** to retrieve its current value.
74 *
75 * You can also set values in the same way.
76 *
77 * Rendering Internals
78 * ===================
79 *
80 * The FormRuntime asks the FormDefinition about the configured Renderer
81 * which should be used ({@link \TYPO3\CMS\Form\Domain\Model\FormDefinition::getRendererClassName}),
82 * and then trigger render() on this Renderer.
83 *
84 * This makes it possible to declaratively define how a form should be rendered.
85 *
86 * Scope: frontend
87 * **This class is NOT meant to be sub classed by developers.**
88 */
89 class FormRuntime implements RootRenderableInterface, \ArrayAccess
90 {
91 const HONEYPOT_NAME_SESSION_IDENTIFIER = 'tx_form_honeypot_name_';
92
93 /**
94 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
95 */
96 protected $objectManager;
97
98 /**
99 * @var \TYPO3\CMS\Form\Domain\Model\FormDefinition
100 */
101 protected $formDefinition;
102
103 /**
104 * @var \TYPO3\CMS\Extbase\Mvc\Web\Request
105 */
106 protected $request;
107
108 /**
109 * @var \TYPO3\CMS\Extbase\Mvc\Web\Response
110 */
111 protected $response;
112
113 /**
114 * @var \TYPO3\CMS\Form\Domain\Runtime\FormState
115 */
116 protected $formState;
117
118 /**
119 * The current page is the page which will be displayed to the user
120 * during rendering.
121 *
122 * If $currentPage is NULL, the *last* page has been submitted and
123 * finishing actions need to take place. You should use $this->isAfterLastPage()
124 * instead of explicitely checking for NULL.
125 *
126 * @var \TYPO3\CMS\Form\Domain\Model\FormElements\Page
127 */
128 protected $currentPage;
129
130 /**
131 * Reference to the page which has been shown on the last request (i.e.
132 * we have to handle the submitted data from lastDisplayedPage)
133 *
134 * @var \TYPO3\CMS\Form\Domain\Model\FormElements\Page
135 */
136 protected $lastDisplayedPage;
137
138 /**
139 * @var \TYPO3\CMS\Extbase\Security\Cryptography\HashService
140 */
141 protected $hashService;
142
143 /**
144 * The current site language configuration.
145 *
146 * @var SiteLanguage
147 */
148 protected $currentSiteLanguage = null;
149
150 /**
151 * Reference to the current running finisher
152 *
153 * @var \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface
154 */
155 protected $currentFinisher = null;
156
157 /**
158 * @param \TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService
159 * @internal
160 */
161 public function injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
162 {
163 $this->hashService = $hashService;
164 }
165
166 /**
167 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
168 * @internal
169 */
170 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
171 {
172 $this->objectManager = $objectManager;
173 }
174
175 /**
176 * @param FormDefinition $formDefinition
177 * @param Request $request
178 * @param Response $response
179 */
180 public function __construct(FormDefinition $formDefinition, Request $request, Response $response)
181 {
182 $this->formDefinition = $formDefinition;
183 $arguments = $request->getArguments();
184 $this->request = clone $request;
185 $formIdentifier = $this->formDefinition->getIdentifier();
186 if (isset($arguments[$formIdentifier])) {
187 $this->request->setArguments($arguments[$formIdentifier]);
188 }
189
190 $this->response = $response;
191 }
192
193 /**
194 * @internal
195 */
196 public function initializeObject()
197 {
198 $this->initializeCurrentSiteLanguage();
199 $this->initializeFormStateFromRequest();
200 $this->processVariants();
201 $this->initializeCurrentPageFromRequest();
202 $this->initializeHoneypotFromRequest();
203
204 if (!$this->isFirstRequest() && $this->getRequest()->getMethod() === 'POST') {
205 $this->processSubmittedFormValues();
206 }
207
208 $this->renderHoneypot();
209 }
210
211 /**
212 * Initializes the current state of the form, based on the request
213 */
214 protected function initializeFormStateFromRequest()
215 {
216 $serializedFormStateWithHmac = $this->request->getInternalArgument('__state');
217 if ($serializedFormStateWithHmac === null) {
218 $this->formState = GeneralUtility::makeInstance(FormState::class);
219 } else {
220 $serializedFormState = $this->hashService->validateAndStripHmac($serializedFormStateWithHmac);
221 $this->formState = unserialize(base64_decode($serializedFormState));
222 }
223 }
224
225 /**
226 * Initializes the current page data based on the current request, also modifiable by a hook
227 */
228 protected function initializeCurrentPageFromRequest()
229 {
230 if (!$this->formState->isFormSubmitted()) {
231 $this->currentPage = $this->formDefinition->getPageByIndex(0);
232
233 if (!$this->currentPage->isEnabled()) {
234 throw new FormException('Disabling the first page is not allowed', 1527186844);
235 }
236
237 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] ?? [] as $className) {
238 $hookObj = GeneralUtility::makeInstance($className);
239 if (method_exists($hookObj, 'afterInitializeCurrentPage')) {
240 $this->currentPage = $hookObj->afterInitializeCurrentPage(
241 $this,
242 $this->currentPage,
243 null,
244 $this->request->getArguments()
245 );
246 }
247 }
248 return;
249 }
250
251 $this->lastDisplayedPage = $this->formDefinition->getPageByIndex($this->formState->getLastDisplayedPageIndex());
252 $currentPageIndex = (int)$this->request->getInternalArgument('__currentPage');
253
254 if ($this->userWentBackToPreviousStep()) {
255 if ($currentPageIndex < $this->lastDisplayedPage->getIndex()) {
256 $currentPageIndex = $this->lastDisplayedPage->getIndex();
257 }
258 } else {
259 if ($currentPageIndex > $this->lastDisplayedPage->getIndex() + 1) {
260 $currentPageIndex = $this->lastDisplayedPage->getIndex() + 1;
261 }
262 }
263
264 if ($currentPageIndex >= count($this->formDefinition->getPages())) {
265 // Last Page
266 $this->currentPage = null;
267 } else {
268 $this->currentPage = $this->formDefinition->getPageByIndex($currentPageIndex);
269
270 if (!$this->currentPage->isEnabled()) {
271 if ($currentPageIndex === 0) {
272 throw new FormException('Disabling the first page is not allowed', 1527186845);
273 }
274
275 if ($this->userWentBackToPreviousStep()) {
276 $this->currentPage = $this->getPreviousEnabledPage();
277 } else {
278 $this->currentPage = $this->getNextEnabledPage();
279 }
280 }
281 }
282
283 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] ?? [] as $className) {
284 $hookObj = GeneralUtility::makeInstance($className);
285 if (method_exists($hookObj, 'afterInitializeCurrentPage')) {
286 $this->currentPage = $hookObj->afterInitializeCurrentPage(
287 $this,
288 $this->currentPage,
289 $this->lastDisplayedPage,
290 $this->request->getArguments()
291 );
292 }
293 }
294 }
295
296 /**
297 * Checks if the honey pot is active, and adds a validator if so.
298 */
299 protected function initializeHoneypotFromRequest()
300 {
301 $renderingOptions = $this->formDefinition->getRenderingOptions();
302 if (!isset($renderingOptions['honeypot']['enable']) || $renderingOptions['honeypot']['enable'] === false || TYPO3_MODE === 'BE') {
303 return;
304 }
305
306 ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']);
307
308 if (!$this->isFirstRequest()) {
309 $elementsCount = count($this->lastDisplayedPage->getElements());
310 if ($elementsCount === 0) {
311 return;
312 }
313
314 $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage);
315 if ($honeypotNameFromSession) {
316 $honeypotElement = $this->lastDisplayedPage->createElement($honeypotNameFromSession, $renderingOptions['honeypot']['formElementToUse']);
317 $validator = $this->objectManager->get(EmptyValidator::class);
318 $honeypotElement->addValidator($validator);
319 }
320 }
321 }
322
323 /**
324 * Renders a hidden field if the honey pot is active.
325 */
326 protected function renderHoneypot()
327 {
328 $renderingOptions = $this->formDefinition->getRenderingOptions();
329 if (!isset($renderingOptions['honeypot']['enable']) || $renderingOptions['honeypot']['enable'] === false || TYPO3_MODE === 'BE') {
330 return;
331 }
332
333 ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']);
334
335 if (!$this->isAfterLastPage()) {
336 $elementsCount = count($this->currentPage->getElements());
337 if ($elementsCount === 0) {
338 return;
339 }
340
341 if (!$this->isFirstRequest()) {
342 $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage);
343 if ($honeypotNameFromSession) {
344 $honeypotElement = $this->formDefinition->getElementByIdentifier($honeypotNameFromSession);
345 if ($honeypotElement instanceof FormElementInterface) {
346 $this->lastDisplayedPage->removeElement($honeypotElement);
347 }
348 }
349 }
350
351 $elementsCount = count($this->currentPage->getElements());
352 $randomElementNumber = mt_rand(0, $elementsCount - 1);
353 $honeypotName = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, mt_rand(5, 26));
354
355 $referenceElement = $this->currentPage->getElements()[$randomElementNumber];
356 $honeypotElement = $this->currentPage->createElement($honeypotName, $renderingOptions['honeypot']['formElementToUse']);
357 $validator = $this->objectManager->get(EmptyValidator::class);
358
359 $honeypotElement->addValidator($validator);
360 if (mt_rand(0, 1) === 1) {
361 $this->currentPage->moveElementAfter($honeypotElement, $referenceElement);
362 } else {
363 $this->currentPage->moveElementBefore($honeypotElement, $referenceElement);
364 }
365 $this->setHoneypotNameInSession($this->currentPage, $honeypotName);
366 }
367 }
368
369 /**
370 * @param Page $page
371 * @return string|null
372 */
373 protected function getHoneypotNameFromSession(Page $page)
374 {
375 if ($this->isFrontendUserAuthenticated()) {
376 $honeypotNameFromSession = $this->getFrontendUser()->getKey(
377 'user',
378 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier()
379 );
380 } else {
381 $honeypotNameFromSession = $this->getFrontendUser()->getKey(
382 'ses',
383 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier()
384 );
385 }
386 return $honeypotNameFromSession;
387 }
388
389 /**
390 * @param Page $page
391 * @param string $honeypotName
392 */
393 protected function setHoneypotNameInSession(Page $page, string $honeypotName)
394 {
395 if ($this->isFrontendUserAuthenticated()) {
396 $this->getFrontendUser()->setKey(
397 'user',
398 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(),
399 $honeypotName
400 );
401 } else {
402 $this->getFrontendUser()->setKey(
403 'ses',
404 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(),
405 $honeypotName
406 );
407 }
408 }
409
410 /**
411 * Necessary to know if honeypot information should be stored in the user session info, or in the anonymous session
412 *
413 * @return bool true when a frontend user is logged, otherwise false
414 */
415 protected function isFrontendUserAuthenticated(): bool
416 {
417 return (bool)GeneralUtility::makeInstance(Context::class)
418 ->getPropertyFromAspect('frontend.user', 'isLoggedIn', false);
419 }
420
421 /**
422 */
423 protected function processVariants()
424 {
425 $conditionResolver = $this->getConditionResolver();
426
427 $renderables = array_merge([$this->formDefinition], $this->formDefinition->getRenderablesRecursively());
428 foreach ($renderables as $renderable) {
429 if ($renderable instanceof VariableRenderableInterface) {
430 $variants = $renderable->getVariants();
431 foreach ($variants as $variant) {
432 if ($variant->conditionMatches($conditionResolver)) {
433 $variant->apply();
434 }
435 }
436 }
437 }
438 }
439
440 /**
441 * Returns TRUE if the last page of the form has been submitted, otherwise FALSE
442 *
443 * @return bool
444 */
445 protected function isAfterLastPage(): bool
446 {
447 return $this->currentPage === null;
448 }
449
450 /**
451 * Returns TRUE if no previous page is stored in the FormState, otherwise FALSE
452 *
453 * @return bool
454 */
455 protected function isFirstRequest(): bool
456 {
457 return $this->lastDisplayedPage === null;
458 }
459
460 /**
461 * Runs throuh all validations
462 */
463 protected function processSubmittedFormValues()
464 {
465 $result = $this->mapAndValidatePage($this->lastDisplayedPage);
466 if ($result->hasErrors() && !$this->userWentBackToPreviousStep()) {
467 $this->currentPage = $this->lastDisplayedPage;
468 $this->request->setOriginalRequestMappingResults($result);
469 }
470 }
471
472 /**
473 * returns TRUE if the user went back to any previous step in the form.
474 *
475 * @return bool
476 */
477 protected function userWentBackToPreviousStep(): bool
478 {
479 return !$this->isAfterLastPage() && !$this->isFirstRequest() && $this->currentPage->getIndex() < $this->lastDisplayedPage->getIndex();
480 }
481
482 /**
483 * @param Page $page
484 * @return Result
485 * @throws PropertyMappingException
486 */
487 protected function mapAndValidatePage(Page $page): Result
488 {
489 $result = $this->objectManager->get(Result::class);
490 $requestArguments = $this->request->getArguments();
491
492 $propertyPathsForWhichPropertyMappingShouldHappen = [];
493 $registerPropertyPaths = function ($propertyPath) use (&$propertyPathsForWhichPropertyMappingShouldHappen) {
494 $propertyPathParts = explode('.', $propertyPath);
495 $accumulatedPropertyPathParts = [];
496 foreach ($propertyPathParts as $propertyPathPart) {
497 $accumulatedPropertyPathParts[] = $propertyPathPart;
498 $temporaryPropertyPath = implode('.', $accumulatedPropertyPathParts);
499 $propertyPathsForWhichPropertyMappingShouldHappen[$temporaryPropertyPath] = $temporaryPropertyPath;
500 }
501 };
502
503 $value = null;
504
505 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] ?? [] as $className) {
506 $hookObj = GeneralUtility::makeInstance($className);
507 if (method_exists($hookObj, 'afterSubmit')) {
508 $value = $hookObj->afterSubmit(
509 $this,
510 $page,
511 $value,
512 $requestArguments
513 );
514 }
515 }
516
517 foreach ($page->getElementsRecursively() as $element) {
518 if (!$element->isEnabled()) {
519 continue;
520 }
521
522 try {
523 $value = ArrayUtility::getValueByPath($requestArguments, $element->getIdentifier(), '.');
524 } catch (MissingArrayPathException $exception) {
525 $value = null;
526 }
527
528 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] ?? [] as $className) {
529 $hookObj = GeneralUtility::makeInstance($className);
530 if (method_exists($hookObj, 'afterSubmit')) {
531 $value = $hookObj->afterSubmit(
532 $this,
533 $element,
534 $value,
535 $requestArguments
536 );
537 }
538 }
539
540 $this->formState->setFormValue($element->getIdentifier(), $value);
541 $registerPropertyPaths($element->getIdentifier());
542 }
543
544 // The more parts the path has, the more early it is processed
545 usort($propertyPathsForWhichPropertyMappingShouldHappen, function ($a, $b) {
546 return substr_count($b, '.') - substr_count($a, '.');
547 });
548
549 $processingRules = $this->formDefinition->getProcessingRules();
550
551 foreach ($propertyPathsForWhichPropertyMappingShouldHappen as $propertyPath) {
552 if (isset($processingRules[$propertyPath])) {
553 $processingRule = $processingRules[$propertyPath];
554 $value = $this->formState->getFormValue($propertyPath);
555 try {
556 $value = $processingRule->process($value);
557 } catch (PropertyException $exception) {
558 throw new PropertyMappingException(
559 'Failed to process FormValue at "' . $propertyPath . '" from "' . gettype($value) . '" to "' . $processingRule->getDataType() . '"',
560 1480024933,
561 $exception
562 );
563 }
564 $result->forProperty($this->getIdentifier() . '.' . $propertyPath)->merge($processingRule->getProcessingMessages());
565 $this->formState->setFormValue($propertyPath, $value);
566 }
567 }
568
569 return $result;
570 }
571
572 /**
573 * Override the current page taken from the request, rendering the page with index $pageIndex instead.
574 *
575 * This is typically not needed in production code, but it is very helpful when displaying
576 * some kind of "preview" of the form (e.g. form editor).
577 *
578 * @param int $pageIndex
579 */
580 public function overrideCurrentPage(int $pageIndex)
581 {
582 $this->currentPage = $this->formDefinition->getPageByIndex($pageIndex);
583 }
584
585 /**
586 * Render this form.
587 *
588 * @return string|null rendered form
589 * @throws RenderingException
590 */
591 public function render()
592 {
593 if ($this->isAfterLastPage()) {
594 return $this->invokeFinishers();
595 }
596 $this->processVariants();
597
598 $this->formState->setLastDisplayedPageIndex($this->currentPage->getIndex());
599
600 if ($this->formDefinition->getRendererClassName() === '') {
601 throw new RenderingException(sprintf('The form definition "%s" does not have a rendererClassName set.', $this->formDefinition->getIdentifier()), 1326095912);
602 }
603 $rendererClassName = $this->formDefinition->getRendererClassName();
604 $renderer = $this->objectManager->get($rendererClassName);
605 if (!($renderer instanceof RendererInterface)) {
606 throw new RenderingException(sprintf('The renderer "%s" des not implement RendererInterface', $rendererClassName), 1326096024);
607 }
608
609 $controllerContext = $this->getControllerContext();
610
611 $renderer->setControllerContext($controllerContext);
612 $renderer->setFormRuntime($this);
613 return $renderer->render();
614 }
615
616 /**
617 * Executes all finishers of this form
618 *
619 * @return string
620 */
621 protected function invokeFinishers(): string
622 {
623 $finisherContext = $this->objectManager->get(
624 FinisherContext::class,
625 $this,
626 $this->getControllerContext()
627 );
628
629 $output = '';
630 $originalContent = $this->response->getContent();
631 $this->response->setContent(null);
632 foreach ($this->formDefinition->getFinishers() as $finisher) {
633 $this->currentFinisher = $finisher;
634 $this->processVariants();
635
636 $finisherOutput = $finisher->execute($finisherContext);
637 if (is_string($finisherOutput) && !empty($finisherOutput)) {
638 $output .= $finisherOutput;
639 } else {
640 $output .= $this->response->getContent();
641 $this->response->setContent(null);
642 }
643
644 if ($finisherContext->isCancelled()) {
645 break;
646 }
647 }
648 $this->response->setContent($originalContent);
649
650 return $output;
651 }
652
653 /**
654 * @return string The identifier of underlying form
655 */
656 public function getIdentifier(): string
657 {
658 return $this->formDefinition->getIdentifier();
659 }
660
661 /**
662 * Get the request this object is bound to.
663 *
664 * This is mostly relevant inside Finishers, where you f.e. want to redirect
665 * the user to another page.
666 *
667 * @return Request the request this object is bound to
668 */
669 public function getRequest(): Request
670 {
671 return $this->request;
672 }
673
674 /**
675 * Get the response this object is bound to.
676 *
677 * This is mostly relevant inside Finishers, where you f.e. want to set response
678 * headers or output content.
679 *
680 * @return Response the response this object is bound to
681 */
682 public function getResponse(): Response
683 {
684 return $this->response;
685 }
686
687 /**
688 * Returns the currently selected page
689 *
690 * @return Page|null
691 */
692 public function getCurrentPage(): ?Page
693 {
694 return $this->currentPage;
695 }
696
697 /**
698 * Returns the previous page of the currently selected one or NULL if there is no previous page
699 *
700 * @return Page|null
701 */
702 public function getPreviousPage(): ?Page
703 {
704 $previousPageIndex = $this->currentPage->getIndex() - 1;
705 if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) {
706 return $this->formDefinition->getPageByIndex($previousPageIndex);
707 }
708 return null;
709 }
710
711 /**
712 * Returns the next page of the currently selected one or NULL if there is no next page
713 *
714 * @return Page|null
715 */
716 public function getNextPage(): ?Page
717 {
718 $nextPageIndex = $this->currentPage->getIndex() + 1;
719 if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) {
720 return $this->formDefinition->getPageByIndex($nextPageIndex);
721 }
722 return null;
723 }
724
725 /**
726 * Returns the previous enabled page of the currently selected one
727 * or NULL if there is no previous page
728 *
729 * @return Page|null
730 */
731 public function getPreviousEnabledPage(): ?Page
732 {
733 $previousPage = null;
734 $previousPageIndex = $this->currentPage->getIndex() - 1;
735 while ($previousPageIndex >= 0) {
736 if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) {
737 $previousPage = $this->formDefinition->getPageByIndex($previousPageIndex);
738
739 if ($previousPage->isEnabled()) {
740 break;
741 }
742
743 $previousPage = null;
744 $previousPageIndex--;
745 } else {
746 $previousPage = null;
747 break;
748 }
749 }
750
751 return $previousPage;
752 }
753
754 /**
755 * Returns the next enabled page of the currently selected one or
756 * NULL if there is no next page
757 *
758 * @return Page|null
759 */
760 public function getNextEnabledPage(): ?Page
761 {
762 $nextPage = null;
763 $pageCount = count($this->formDefinition->getPages());
764 $nextPageIndex = $this->currentPage->getIndex() + 1;
765
766 while ($nextPageIndex < $pageCount) {
767 if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) {
768 $nextPage = $this->formDefinition->getPageByIndex($nextPageIndex);
769 $renderingOptions = $nextPage->getRenderingOptions();
770 if (
771 !isset($renderingOptions['enabled'])
772 || (bool)$renderingOptions['enabled']
773 ) {
774 break;
775 }
776 $nextPage = null;
777 $nextPageIndex++;
778 } else {
779 $nextPage = null;
780 break;
781 }
782 }
783
784 return $nextPage;
785 }
786
787 /**
788 * @return ControllerContext
789 */
790 protected function getControllerContext(): ControllerContext
791 {
792 $uriBuilder = $this->objectManager->get(UriBuilder::class);
793 $uriBuilder->setRequest($this->request);
794 $controllerContext = $this->objectManager->get(ControllerContext::class);
795 $controllerContext->setRequest($this->request);
796 $controllerContext->setResponse($this->response);
797 $controllerContext->setArguments($this->objectManager->get(Arguments::class, []));
798 $controllerContext->setUriBuilder($uriBuilder);
799 return $controllerContext;
800 }
801
802 /**
803 * Abstract "type" of this Renderable. Is used during the rendering process
804 * to determine the template file or the View PHP class being used to render
805 * the particular element.
806 *
807 * @return string
808 */
809 public function getType(): string
810 {
811 return $this->formDefinition->getType();
812 }
813
814 /**
815 * @param string $identifier
816 * @return bool
817 * @internal
818 */
819 public function offsetExists($identifier)
820 {
821 if ($this->getElementValue($identifier) !== null) {
822 return true;
823 }
824
825 if (is_callable([$this, 'get' . ucfirst($identifier)])) {
826 return true;
827 }
828 if (is_callable([$this, 'has' . ucfirst($identifier)])) {
829 return true;
830 }
831 if (is_callable([$this, 'is' . ucfirst($identifier)])) {
832 return true;
833 }
834 if (property_exists($this, $identifier)) {
835 $propertyReflection = new \ReflectionProperty($this, $identifier);
836 return $propertyReflection->isPublic();
837 }
838
839 return false;
840 }
841
842 /**
843 * @param string $identifier
844 * @return mixed
845 * @internal
846 */
847 public function offsetGet($identifier)
848 {
849 if ($this->getElementValue($identifier) !== null) {
850 return $this->getElementValue($identifier);
851 }
852 $getterMethodName = 'get' . ucfirst($identifier);
853 if (is_callable([$this, $getterMethodName])) {
854 return $this->{$getterMethodName}();
855 }
856 return null;
857 }
858
859 /**
860 * @param string $identifier
861 * @param mixed $value
862 * @internal
863 */
864 public function offsetSet($identifier, $value)
865 {
866 $this->formState->setFormValue($identifier, $value);
867 }
868
869 /**
870 * @param string $identifier
871 * @internal
872 */
873 public function offsetUnset($identifier)
874 {
875 $this->formState->setFormValue($identifier, null);
876 }
877
878 /**
879 * Returns the value of the specified element
880 *
881 * @param string $identifier
882 * @return mixed
883 */
884 public function getElementValue(string $identifier)
885 {
886 $formValue = $this->formState->getFormValue($identifier);
887 if ($formValue !== null) {
888 return $formValue;
889 }
890 return $this->formDefinition->getElementDefaultValueByIdentifier($identifier);
891 }
892
893 /**
894 * @return array<Page> The Form's pages in the correct order
895 */
896 public function getPages(): array
897 {
898 return $this->formDefinition->getPages();
899 }
900
901 /**
902 * @return FormState|null
903 * @internal
904 */
905 public function getFormState(): ?FormState
906 {
907 return $this->formState;
908 }
909
910 /**
911 * Get all rendering options
912 *
913 * @return array associative array of rendering options
914 */
915 public function getRenderingOptions(): array
916 {
917 return $this->formDefinition->getRenderingOptions();
918 }
919
920 /**
921 * Get the renderer class name to be used to display this renderable;
922 * must implement RendererInterface
923 *
924 * @return string the renderer class name
925 */
926 public function getRendererClassName(): string
927 {
928 return $this->formDefinition->getRendererClassName();
929 }
930
931 /**
932 * Get the label which shall be displayed next to the form element
933 *
934 * @return string
935 */
936 public function getLabel(): string
937 {
938 return $this->formDefinition->getLabel();
939 }
940
941 /**
942 * Get the template name of the renderable
943 *
944 * @return string
945 */
946 public function getTemplateName(): string
947 {
948 return $this->formDefinition->getTemplateName();
949 }
950
951 /**
952 * Get the underlying form definition from the runtime
953 *
954 * @return FormDefinition
955 */
956 public function getFormDefinition(): FormDefinition
957 {
958 return $this->formDefinition;
959 }
960
961 /**
962 * Get the current site language configuration.
963 *
964 * @return SiteLanguage
965 */
966 public function getCurrentSiteLanguage(): ?SiteLanguage
967 {
968 return $this->currentSiteLanguage;
969 }
970
971 /**
972 * Override the the current site language configuration.
973 *
974 * This is typically not needed in production code, but it is very
975 * helpful when displaying some kind of "preview" of the form (e.g. form editor).
976 *
977 * @param SiteLanguage $currentSiteLanguage
978 */
979 public function setCurrentSiteLanguage(SiteLanguage $currentSiteLanguage): void
980 {
981 $this->currentSiteLanguage = $currentSiteLanguage;
982 }
983
984 /**
985 * Initialize the SiteLanguage object.
986 * This is mainly used by the condition matcher.
987 */
988 protected function initializeCurrentSiteLanguage(): void
989 {
990 if (
991 $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface
992 && $GLOBALS['TYPO3_REQUEST']->getAttribute('language') instanceof SiteLanguage
993 ) {
994 $this->currentSiteLanguage = $GLOBALS['TYPO3_REQUEST']->getAttribute('language');
995 } else {
996 $pageId = 0;
997 $languageId = (int)GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('language', 'id', 0);
998
999 if (TYPO3_MODE === 'FE') {
1000 $pageId = $this->getTypoScriptFrontendController()->id;
1001 }
1002
1003 $fakeSiteConfiguration = [
1004 'languages' => [
1005 [
1006 'languageId' => $languageId,
1007 'title' => 'Dummy',
1008 'navigationTitle' => '',
1009 'typo3Language' => '',
1010 'flag' => '',
1011 'locale' => '',
1012 'iso-639-1' => '',
1013 'hreflang' => '',
1014 'direction' => '',
1015 ],
1016 ],
1017 ];
1018
1019 $this->currentSiteLanguage = GeneralUtility::makeInstance(Site::class, 'form-dummy', $pageId, $fakeSiteConfiguration)
1020 ->getLanguageById($languageId);
1021 }
1022 }
1023
1024 /**
1025 * Reference to the current running finisher
1026 *
1027 * @return FinisherInterface|null
1028 */
1029 public function getCurrentFinisher(): ?FinisherInterface
1030 {
1031 return $this->currentFinisher;
1032 }
1033
1034 /**
1035 * @return Resolver
1036 */
1037 protected function getConditionResolver(): Resolver
1038 {
1039 $formValues = array_replace_recursive(
1040 $this->getFormState()->getFormValues(),
1041 $this->getRequest()->getArguments()
1042 );
1043 $page = $this->getCurrentPage() ?? $this->getFormDefinition()->getPageByIndex(0);
1044
1045 $finisherIdentifier = '';
1046 if ($this->getCurrentFinisher() !== null) {
1047 $finisherIdentifier = (new \ReflectionClass($this->getCurrentFinisher()))->getShortName();
1048 $finisherIdentifier = preg_replace('/Finisher$/', '', $finisherIdentifier);
1049 }
1050
1051 return GeneralUtility::makeInstance(
1052 Resolver::class,
1053 'form',
1054 [
1055 // some shortcuts
1056 'formRuntime' => $this,
1057 'formValues' => $formValues,
1058 'stepIdentifier' => $page->getIdentifier(),
1059 'stepType' => $page->getType(),
1060 'finisherIdentifier' => $finisherIdentifier,
1061 ],
1062 $GLOBALS['TYPO3_REQUEST'] ?? GeneralUtility::makeInstance(ServerRequest::class)
1063 );
1064 }
1065
1066 /**
1067 * @return FrontendUserAuthentication
1068 */
1069 protected function getFrontendUser(): FrontendUserAuthentication
1070 {
1071 return $this->getTypoScriptFrontendController()->fe_user;
1072 }
1073
1074 /**
1075 * @return TypoScriptFrontendController
1076 */
1077 protected function getTypoScriptFrontendController(): TypoScriptFrontendController
1078 {
1079 return $GLOBALS['TSFE'];
1080 }
1081 }