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