[BUGFIX] Fix several typos in php comments
[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 explicitly 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;
149
150 /**
151 * Reference to the current running finisher
152 *
153 * @var \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface
154 */
155 protected $currentFinisher;
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 protected function processVariants()
422 {
423 $conditionResolver = $this->getConditionResolver();
424
425 $renderables = array_merge([$this->formDefinition], $this->formDefinition->getRenderablesRecursively());
426 foreach ($renderables as $renderable) {
427 if ($renderable instanceof VariableRenderableInterface) {
428 $variants = $renderable->getVariants();
429 foreach ($variants as $variant) {
430 if ($variant->conditionMatches($conditionResolver)) {
431 $variant->apply();
432 }
433 }
434 }
435 }
436 }
437
438 /**
439 * Returns TRUE if the last page of the form has been submitted, otherwise FALSE
440 *
441 * @return bool
442 */
443 protected function isAfterLastPage(): bool
444 {
445 return $this->currentPage === null;
446 }
447
448 /**
449 * Returns TRUE if no previous page is stored in the FormState, otherwise FALSE
450 *
451 * @return bool
452 */
453 protected function isFirstRequest(): bool
454 {
455 return $this->lastDisplayedPage === null;
456 }
457
458 /**
459 * Runs through all validations
460 */
461 protected function processSubmittedFormValues()
462 {
463 $result = $this->mapAndValidatePage($this->lastDisplayedPage);
464 if ($result->hasErrors() && !$this->userWentBackToPreviousStep()) {
465 $this->currentPage = $this->lastDisplayedPage;
466 $this->request->setOriginalRequestMappingResults($result);
467 }
468 }
469
470 /**
471 * returns TRUE if the user went back to any previous step in the form.
472 *
473 * @return bool
474 */
475 protected function userWentBackToPreviousStep(): bool
476 {
477 return !$this->isAfterLastPage() && !$this->isFirstRequest() && $this->currentPage->getIndex() < $this->lastDisplayedPage->getIndex();
478 }
479
480 /**
481 * @param Page $page
482 * @return Result
483 * @throws PropertyMappingException
484 */
485 protected function mapAndValidatePage(Page $page): Result
486 {
487 $result = $this->objectManager->get(Result::class);
488 $requestArguments = $this->request->getArguments();
489
490 $propertyPathsForWhichPropertyMappingShouldHappen = [];
491 $registerPropertyPaths = function ($propertyPath) use (&$propertyPathsForWhichPropertyMappingShouldHappen) {
492 $propertyPathParts = explode('.', $propertyPath);
493 $accumulatedPropertyPathParts = [];
494 foreach ($propertyPathParts as $propertyPathPart) {
495 $accumulatedPropertyPathParts[] = $propertyPathPart;
496 $temporaryPropertyPath = implode('.', $accumulatedPropertyPathParts);
497 $propertyPathsForWhichPropertyMappingShouldHappen[$temporaryPropertyPath] = $temporaryPropertyPath;
498 }
499 };
500
501 $value = null;
502
503 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] ?? [] as $className) {
504 $hookObj = GeneralUtility::makeInstance($className);
505 if (method_exists($hookObj, 'afterSubmit')) {
506 $value = $hookObj->afterSubmit(
507 $this,
508 $page,
509 $value,
510 $requestArguments
511 );
512 }
513 }
514
515 foreach ($page->getElementsRecursively() as $element) {
516 if (!$element->isEnabled()) {
517 continue;
518 }
519
520 try {
521 $value = ArrayUtility::getValueByPath($requestArguments, $element->getIdentifier(), '.');
522 } catch (MissingArrayPathException $exception) {
523 $value = null;
524 }
525
526 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] ?? [] as $className) {
527 $hookObj = GeneralUtility::makeInstance($className);
528 if (method_exists($hookObj, 'afterSubmit')) {
529 $value = $hookObj->afterSubmit(
530 $this,
531 $element,
532 $value,
533 $requestArguments
534 );
535 }
536 }
537
538 $this->formState->setFormValue($element->getIdentifier(), $value);
539 $registerPropertyPaths($element->getIdentifier());
540 }
541
542 // The more parts the path has, the more early it is processed
543 usort($propertyPathsForWhichPropertyMappingShouldHappen, function ($a, $b) {
544 return substr_count($b, '.') - substr_count($a, '.');
545 });
546
547 $processingRules = $this->formDefinition->getProcessingRules();
548
549 foreach ($propertyPathsForWhichPropertyMappingShouldHappen as $propertyPath) {
550 if (isset($processingRules[$propertyPath])) {
551 $processingRule = $processingRules[$propertyPath];
552 $value = $this->formState->getFormValue($propertyPath);
553 try {
554 $value = $processingRule->process($value);
555 } catch (PropertyException $exception) {
556 throw new PropertyMappingException(
557 'Failed to process FormValue at "' . $propertyPath . '" from "' . gettype($value) . '" to "' . $processingRule->getDataType() . '"',
558 1480024933,
559 $exception
560 );
561 }
562 $result->forProperty($this->getIdentifier() . '.' . $propertyPath)->merge($processingRule->getProcessingMessages());
563 $this->formState->setFormValue($propertyPath, $value);
564 }
565 }
566
567 return $result;
568 }
569
570 /**
571 * Override the current page taken from the request, rendering the page with index $pageIndex instead.
572 *
573 * This is typically not needed in production code, but it is very helpful when displaying
574 * some kind of "preview" of the form (e.g. form editor).
575 *
576 * @param int $pageIndex
577 */
578 public function overrideCurrentPage(int $pageIndex)
579 {
580 $this->currentPage = $this->formDefinition->getPageByIndex($pageIndex);
581 }
582
583 /**
584 * Render this form.
585 *
586 * @return string|null rendered form
587 * @throws RenderingException
588 */
589 public function render()
590 {
591 if ($this->isAfterLastPage()) {
592 return $this->invokeFinishers();
593 }
594 $this->processVariants();
595
596 $this->formState->setLastDisplayedPageIndex($this->currentPage->getIndex());
597
598 if ($this->formDefinition->getRendererClassName() === '') {
599 throw new RenderingException(sprintf('The form definition "%s" does not have a rendererClassName set.', $this->formDefinition->getIdentifier()), 1326095912);
600 }
601 $rendererClassName = $this->formDefinition->getRendererClassName();
602 $renderer = $this->objectManager->get($rendererClassName);
603 if (!($renderer instanceof RendererInterface)) {
604 throw new RenderingException(sprintf('The renderer "%s" des not implement RendererInterface', $rendererClassName), 1326096024);
605 }
606
607 $controllerContext = $this->getControllerContext();
608
609 $renderer->setControllerContext($controllerContext);
610 $renderer->setFormRuntime($this);
611 return $renderer->render();
612 }
613
614 /**
615 * Executes all finishers of this form
616 *
617 * @return string
618 */
619 protected function invokeFinishers(): string
620 {
621 $finisherContext = $this->objectManager->get(
622 FinisherContext::class,
623 $this,
624 $this->getControllerContext()
625 );
626
627 $output = '';
628 $originalContent = $this->response->getContent();
629 $this->response->setContent(null);
630 foreach ($this->formDefinition->getFinishers() as $finisher) {
631 $this->currentFinisher = $finisher;
632 $this->processVariants();
633
634 $finisherOutput = $finisher->execute($finisherContext);
635 if (is_string($finisherOutput) && !empty($finisherOutput)) {
636 $output .= $finisherOutput;
637 } else {
638 $output .= $this->response->getContent();
639 $this->response->setContent(null);
640 }
641
642 if ($finisherContext->isCancelled()) {
643 break;
644 }
645 }
646 $this->response->setContent($originalContent);
647
648 return $output;
649 }
650
651 /**
652 * @return string The identifier of underlying form
653 */
654 public function getIdentifier(): string
655 {
656 return $this->formDefinition->getIdentifier();
657 }
658
659 /**
660 * Get the request this object is bound to.
661 *
662 * This is mostly relevant inside Finishers, where you f.e. want to redirect
663 * the user to another page.
664 *
665 * @return Request the request this object is bound to
666 */
667 public function getRequest(): Request
668 {
669 return $this->request;
670 }
671
672 /**
673 * Get the response this object is bound to.
674 *
675 * This is mostly relevant inside Finishers, where you f.e. want to set response
676 * headers or output content.
677 *
678 * @return Response the response this object is bound to
679 */
680 public function getResponse(): Response
681 {
682 return $this->response;
683 }
684
685 /**
686 * Returns the currently selected page
687 *
688 * @return Page|null
689 */
690 public function getCurrentPage(): ?Page
691 {
692 return $this->currentPage;
693 }
694
695 /**
696 * Returns the previous page of the currently selected one or NULL if there is no previous page
697 *
698 * @return Page|null
699 */
700 public function getPreviousPage(): ?Page
701 {
702 $previousPageIndex = $this->currentPage->getIndex() - 1;
703 if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) {
704 return $this->formDefinition->getPageByIndex($previousPageIndex);
705 }
706 return null;
707 }
708
709 /**
710 * Returns the next page of the currently selected one or NULL if there is no next page
711 *
712 * @return Page|null
713 */
714 public function getNextPage(): ?Page
715 {
716 $nextPageIndex = $this->currentPage->getIndex() + 1;
717 if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) {
718 return $this->formDefinition->getPageByIndex($nextPageIndex);
719 }
720 return null;
721 }
722
723 /**
724 * Returns the previous enabled page of the currently selected one
725 * or NULL if there is no previous page
726 *
727 * @return Page|null
728 */
729 public function getPreviousEnabledPage(): ?Page
730 {
731 $previousPage = null;
732 $previousPageIndex = $this->currentPage->getIndex() - 1;
733 while ($previousPageIndex >= 0) {
734 if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) {
735 $previousPage = $this->formDefinition->getPageByIndex($previousPageIndex);
736
737 if ($previousPage->isEnabled()) {
738 break;
739 }
740
741 $previousPage = null;
742 $previousPageIndex--;
743 } else {
744 $previousPage = null;
745 break;
746 }
747 }
748
749 return $previousPage;
750 }
751
752 /**
753 * Returns the next enabled page of the currently selected one or
754 * NULL if there is no next page
755 *
756 * @return Page|null
757 */
758 public function getNextEnabledPage(): ?Page
759 {
760 $nextPage = null;
761 $pageCount = count($this->formDefinition->getPages());
762 $nextPageIndex = $this->currentPage->getIndex() + 1;
763
764 while ($nextPageIndex < $pageCount) {
765 if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) {
766 $nextPage = $this->formDefinition->getPageByIndex($nextPageIndex);
767 $renderingOptions = $nextPage->getRenderingOptions();
768 if (
769 !isset($renderingOptions['enabled'])
770 || (bool)$renderingOptions['enabled']
771 ) {
772 break;
773 }
774 $nextPage = null;
775 $nextPageIndex++;
776 } else {
777 $nextPage = null;
778 break;
779 }
780 }
781
782 return $nextPage;
783 }
784
785 /**
786 * @return ControllerContext
787 */
788 protected function getControllerContext(): ControllerContext
789 {
790 $uriBuilder = $this->objectManager->get(UriBuilder::class);
791 $uriBuilder->setRequest($this->request);
792 $controllerContext = $this->objectManager->get(ControllerContext::class);
793 $controllerContext->setRequest($this->request);
794 $controllerContext->setResponse($this->response);
795 $controllerContext->setArguments($this->objectManager->get(Arguments::class, []));
796 $controllerContext->setUriBuilder($uriBuilder);
797 return $controllerContext;
798 }
799
800 /**
801 * Abstract "type" of this Renderable. Is used during the rendering process
802 * to determine the template file or the View PHP class being used to render
803 * the particular element.
804 *
805 * @return string
806 */
807 public function getType(): string
808 {
809 return $this->formDefinition->getType();
810 }
811
812 /**
813 * @param string $identifier
814 * @return bool
815 * @internal
816 */
817 public function offsetExists($identifier)
818 {
819 if ($this->getElementValue($identifier) !== null) {
820 return true;
821 }
822
823 if (is_callable([$this, 'get' . ucfirst($identifier)])) {
824 return true;
825 }
826 if (is_callable([$this, 'has' . ucfirst($identifier)])) {
827 return true;
828 }
829 if (is_callable([$this, 'is' . ucfirst($identifier)])) {
830 return true;
831 }
832 if (property_exists($this, $identifier)) {
833 $propertyReflection = new \ReflectionProperty($this, $identifier);
834 return $propertyReflection->isPublic();
835 }
836
837 return false;
838 }
839
840 /**
841 * @param string $identifier
842 * @return mixed
843 * @internal
844 */
845 public function offsetGet($identifier)
846 {
847 if ($this->getElementValue($identifier) !== null) {
848 return $this->getElementValue($identifier);
849 }
850 $getterMethodName = 'get' . ucfirst($identifier);
851 if (is_callable([$this, $getterMethodName])) {
852 return $this->{$getterMethodName}();
853 }
854 return null;
855 }
856
857 /**
858 * @param string $identifier
859 * @param mixed $value
860 * @internal
861 */
862 public function offsetSet($identifier, $value)
863 {
864 $this->formState->setFormValue($identifier, $value);
865 }
866
867 /**
868 * @param string $identifier
869 * @internal
870 */
871 public function offsetUnset($identifier)
872 {
873 $this->formState->setFormValue($identifier, null);
874 }
875
876 /**
877 * Returns the value of the specified element
878 *
879 * @param string $identifier
880 * @return mixed
881 */
882 public function getElementValue(string $identifier)
883 {
884 $formValue = $this->formState->getFormValue($identifier);
885 if ($formValue !== null) {
886 return $formValue;
887 }
888 return $this->formDefinition->getElementDefaultValueByIdentifier($identifier);
889 }
890
891 /**
892 * @return array|Page[] The Form's pages in the correct order
893 */
894 public function getPages(): array
895 {
896 return $this->formDefinition->getPages();
897 }
898
899 /**
900 * @return FormState|null
901 * @internal
902 */
903 public function getFormState(): ?FormState
904 {
905 return $this->formState;
906 }
907
908 /**
909 * Get all rendering options
910 *
911 * @return array associative array of rendering options
912 */
913 public function getRenderingOptions(): array
914 {
915 return $this->formDefinition->getRenderingOptions();
916 }
917
918 /**
919 * Get the renderer class name to be used to display this renderable;
920 * must implement RendererInterface
921 *
922 * @return string the renderer class name
923 */
924 public function getRendererClassName(): string
925 {
926 return $this->formDefinition->getRendererClassName();
927 }
928
929 /**
930 * Get the label which shall be displayed next to the form element
931 *
932 * @return string
933 */
934 public function getLabel(): string
935 {
936 return $this->formDefinition->getLabel();
937 }
938
939 /**
940 * Get the template name of the renderable
941 *
942 * @return string
943 */
944 public function getTemplateName(): string
945 {
946 return $this->formDefinition->getTemplateName();
947 }
948
949 /**
950 * Get the underlying form definition from the runtime
951 *
952 * @return FormDefinition
953 */
954 public function getFormDefinition(): FormDefinition
955 {
956 return $this->formDefinition;
957 }
958
959 /**
960 * Get the current site language configuration.
961 *
962 * @return SiteLanguage
963 */
964 public function getCurrentSiteLanguage(): ?SiteLanguage
965 {
966 return $this->currentSiteLanguage;
967 }
968
969 /**
970 * Override the the current site language configuration.
971 *
972 * This is typically not needed in production code, but it is very
973 * helpful when displaying some kind of "preview" of the form (e.g. form editor).
974 *
975 * @param SiteLanguage $currentSiteLanguage
976 */
977 public function setCurrentSiteLanguage(SiteLanguage $currentSiteLanguage): void
978 {
979 $this->currentSiteLanguage = $currentSiteLanguage;
980 }
981
982 /**
983 * Initialize the SiteLanguage object.
984 * This is mainly used by the condition matcher.
985 */
986 protected function initializeCurrentSiteLanguage(): void
987 {
988 if (
989 $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface
990 && $GLOBALS['TYPO3_REQUEST']->getAttribute('language') instanceof SiteLanguage
991 ) {
992 $this->currentSiteLanguage = $GLOBALS['TYPO3_REQUEST']->getAttribute('language');
993 } else {
994 $pageId = 0;
995 $languageId = (int)GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('language', 'id', 0);
996
997 if (TYPO3_MODE === 'FE') {
998 $pageId = $this->getTypoScriptFrontendController()->id;
999 }
1000
1001 $fakeSiteConfiguration = [
1002 'languages' => [
1003 [
1004 'languageId' => $languageId,
1005 'title' => 'Dummy',
1006 'navigationTitle' => '',
1007 'typo3Language' => '',
1008 'flag' => '',
1009 'locale' => '',
1010 'iso-639-1' => '',
1011 'hreflang' => '',
1012 'direction' => '',
1013 ],
1014 ],
1015 ];
1016
1017 $this->currentSiteLanguage = GeneralUtility::makeInstance(Site::class, 'form-dummy', $pageId, $fakeSiteConfiguration)
1018 ->getLanguageById($languageId);
1019 }
1020 }
1021
1022 /**
1023 * Reference to the current running finisher
1024 *
1025 * @return FinisherInterface|null
1026 */
1027 public function getCurrentFinisher(): ?FinisherInterface
1028 {
1029 return $this->currentFinisher;
1030 }
1031
1032 /**
1033 * @return Resolver
1034 */
1035 protected function getConditionResolver(): Resolver
1036 {
1037 $formValues = array_replace_recursive(
1038 $this->getFormState()->getFormValues(),
1039 $this->getRequest()->getArguments()
1040 );
1041 $page = $this->getCurrentPage() ?? $this->getFormDefinition()->getPageByIndex(0);
1042
1043 $finisherIdentifier = '';
1044 if ($this->getCurrentFinisher() !== null) {
1045 if (method_exists($this->getCurrentFinisher(), 'getFinisherIdentifier')) {
1046 $finisherIdentifier = $this->getCurrentFinisher()->getFinisherIdentifier();
1047 } else {
1048 $finisherIdentifier = (new \ReflectionClass($this->getCurrentFinisher()))->getShortName();
1049 $finisherIdentifier = preg_replace('/Finisher$/', '', $finisherIdentifier);
1050 }
1051 }
1052
1053 return GeneralUtility::makeInstance(
1054 Resolver::class,
1055 'form',
1056 [
1057 // some shortcuts
1058 'formRuntime' => $this,
1059 'formValues' => $formValues,
1060 'stepIdentifier' => $page->getIdentifier(),
1061 'stepType' => $page->getType(),
1062 'finisherIdentifier' => $finisherIdentifier,
1063 ],
1064 $GLOBALS['TYPO3_REQUEST'] ?? GeneralUtility::makeInstance(ServerRequest::class)
1065 );
1066 }
1067
1068 /**
1069 * @return FrontendUserAuthentication
1070 */
1071 protected function getFrontendUser(): FrontendUserAuthentication
1072 {
1073 return $this->getTypoScriptFrontendController()->fe_user;
1074 }
1075
1076 /**
1077 * @return TypoScriptFrontendController
1078 */
1079 protected function getTypoScriptFrontendController(): TypoScriptFrontendController
1080 {
1081 return $GLOBALS['TSFE'];
1082 }
1083 }