[BUGFIX] Compatibility for finishers which set content into the response
[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 TYPO3\CMS\Core\Utility\ArrayUtility;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22 use TYPO3\CMS\Extbase\Error\Result;
23 use TYPO3\CMS\Extbase\Mvc\Controller\Arguments;
24 use TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext;
25 use TYPO3\CMS\Extbase\Mvc\Web\Request;
26 use TYPO3\CMS\Extbase\Mvc\Web\Response;
27 use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
28 use TYPO3\CMS\Extbase\Property\Exception as PropertyException;
29 use TYPO3\CMS\Extbase\Reflection\PropertyReflection;
30 use TYPO3\CMS\Form\Domain\Exception\RenderingException;
31 use TYPO3\CMS\Form\Domain\Finishers\FinisherContext;
32 use TYPO3\CMS\Form\Domain\Model\FormDefinition;
33 use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
34 use TYPO3\CMS\Form\Domain\Model\FormElements\Page;
35 use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
36 use TYPO3\CMS\Form\Domain\Renderer\RendererInterface;
37 use TYPO3\CMS\Form\Domain\Runtime\Exception\PropertyMappingException;
38 use TYPO3\CMS\Form\Mvc\Validation\EmptyValidator;
39 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
40
41 /**
42 * This class implements the *runtime logic* of a form, i.e. deciding which
43 * page is shown currently, what the current values of the form are, trigger
44 * validation and property mapping.
45 *
46 * You generally receive an instance of this class by calling {@link \TYPO3\CMS\Form\Domain\Model\FormDefinition::bind}.
47 *
48 * Rendering a Form
49 * ================
50 *
51 * That's easy, just call render() on the FormRuntime:
52 *
53 * /---code php
54 * $form = $formDefinition->bind($request, $response);
55 * $renderedForm = $form->render();
56 * \---
57 *
58 * Accessing Form Values
59 * =====================
60 *
61 * In order to get the values the user has entered into the form, you can access
62 * this object like an array: If a form field with the identifier *firstName*
63 * exists, you can do **$form['firstName']** to retrieve its current value.
64 *
65 * You can also set values in the same way.
66 *
67 * Rendering Internals
68 * ===================
69 *
70 * The FormRuntime asks the FormDefinition about the configured Renderer
71 * which should be used ({@link \TYPO3\CMS\Form\Domain\Model\FormDefinition::getRendererClassName}),
72 * and then trigger render() on this Renderer.
73 *
74 * This makes it possible to declaratively define how a form should be rendered.
75 *
76 * Scope: frontend
77 * **This class is NOT meant to be sub classed by developers.**
78 * @api
79 */
80 class FormRuntime implements RootRenderableInterface, \ArrayAccess
81 {
82 const HONEYPOT_NAME_SESSION_IDENTIFIER = 'tx_form_honeypot_name_';
83
84 /**
85 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
86 */
87 protected $objectManager;
88
89 /**
90 * @var \TYPO3\CMS\Form\Domain\Model\FormDefinition
91 */
92 protected $formDefinition;
93
94 /**
95 * @var \TYPO3\CMS\Extbase\Mvc\Web\Request
96 */
97 protected $request;
98
99 /**
100 * @var \TYPO3\CMS\Extbase\Mvc\Web\Response
101 */
102 protected $response;
103
104 /**
105 * @var \TYPO3\CMS\Form\Domain\Runtime\FormState
106 */
107 protected $formState;
108
109 /**
110 * The current page is the page which will be displayed to the user
111 * during rendering.
112 *
113 * If $currentPage is NULL, the *last* page has been submitted and
114 * finishing actions need to take place. You should use $this->isAfterLastPage()
115 * instead of explicitely checking for NULL.
116 *
117 * @var \TYPO3\CMS\Form\Domain\Model\FormElements\Page
118 */
119 protected $currentPage = null;
120
121 /**
122 * Reference to the page which has been shown on the last request (i.e.
123 * we have to handle the submitted data from lastDisplayedPage)
124 *
125 * @var \TYPO3\CMS\Form\Domain\Model\FormElements\Page
126 */
127 protected $lastDisplayedPage = null;
128
129 /**
130 * @var \TYPO3\CMS\Extbase\Security\Cryptography\HashService
131 */
132 protected $hashService;
133
134 /**
135 * @param \TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService
136 * @internal
137 */
138 public function injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
139 {
140 $this->hashService = $hashService;
141 }
142
143 /**
144 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
145 * @internal
146 */
147 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
148 {
149 $this->objectManager = $objectManager;
150 }
151
152 /**
153 * @param FormDefinition $formDefinition
154 * @param Request $request
155 * @param Response $response
156 * @api
157 */
158 public function __construct(FormDefinition $formDefinition, Request $request, Response $response)
159 {
160 $this->formDefinition = $formDefinition;
161 $arguments = $request->getArguments();
162 $this->request = clone $request;
163 $formIdentifier = $this->formDefinition->getIdentifier();
164 if (isset($arguments[$formIdentifier])) {
165 $this->request->setArguments($arguments[$formIdentifier]);
166 }
167
168 $this->response = $response;
169 }
170
171 /**
172 * @internal
173 */
174 public function initializeObject()
175 {
176 $this->initializeFormStateFromRequest();
177 $this->initializeCurrentPageFromRequest();
178 $this->initializeHoneypotFromRequest();
179
180 if (!$this->isFirstRequest() && $this->getRequest()->getMethod() === 'POST') {
181 $this->processSubmittedFormValues();
182 }
183
184 $this->renderHoneypot();
185 }
186
187 /**
188 * Initializes the current state of the form, based on the request
189 */
190 protected function initializeFormStateFromRequest()
191 {
192 $serializedFormStateWithHmac = $this->request->getInternalArgument('__state');
193 if ($serializedFormStateWithHmac === null) {
194 $this->formState = GeneralUtility::makeInstance(FormState::class);
195 } else {
196 $serializedFormState = $this->hashService->validateAndStripHmac($serializedFormStateWithHmac);
197 $this->formState = unserialize(base64_decode($serializedFormState));
198 }
199 }
200
201 /**
202 * Initializes the current page data based on the current request, also modifiable by a hook
203 */
204 protected function initializeCurrentPageFromRequest()
205 {
206 if (!$this->formState->isFormSubmitted()) {
207 $this->currentPage = $this->formDefinition->getPageByIndex(0);
208 if (
209 isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'])
210 && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'])
211 ) {
212 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] as $className) {
213 $hookObj = GeneralUtility::makeInstance($className);
214 if (method_exists($hookObj, 'afterInitializeCurrentPage')) {
215 $this->currentPage = $hookObj->afterInitializeCurrentPage(
216 $this,
217 $this->currentPage,
218 null,
219 $this->request->getArguments()
220 );
221 }
222 }
223 }
224 return;
225 }
226 $this->lastDisplayedPage = $this->formDefinition->getPageByIndex($this->formState->getLastDisplayedPageIndex());
227
228 // We know now that lastDisplayedPage is filled
229 $currentPageIndex = (int)$this->request->getInternalArgument('__currentPage');
230 if ($currentPageIndex > $this->lastDisplayedPage->getIndex() + 1) {
231 // We only allow jumps to following pages
232 $currentPageIndex = $this->lastDisplayedPage->getIndex() + 1;
233 }
234
235 // We now know that the user did not try to skip a page
236 if ($currentPageIndex === count($this->formDefinition->getPages())) {
237 // Last Page
238 $this->currentPage = null;
239 } else {
240 $this->currentPage = $this->formDefinition->getPageByIndex($currentPageIndex);
241 }
242
243 if (
244 isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'])
245 && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'])
246 ) {
247 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] as $className) {
248 $hookObj = GeneralUtility::makeInstance($className);
249 if (method_exists($hookObj, 'afterInitializeCurrentPage')) {
250 $this->currentPage = $hookObj->afterInitializeCurrentPage(
251 $this,
252 $this->currentPage,
253 $this->lastDisplayedPage,
254 $this->request->getArguments()
255 );
256 }
257 }
258 }
259 }
260
261 /**
262 * Checks if the honey pot is active, and adds a validator if so.
263 */
264 protected function initializeHoneypotFromRequest()
265 {
266 $renderingOptions = $this->formDefinition->getRenderingOptions();
267 if (!isset($renderingOptions['honeypot']['enable']) || $renderingOptions['honeypot']['enable'] === false || TYPO3_MODE === 'BE') {
268 return;
269 }
270
271 ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']);
272
273 if (!$this->isFirstRequest()) {
274 $elementsCount = count($this->lastDisplayedPage->getElements());
275 if ($elementsCount === 0) {
276 return;
277 }
278
279 $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage);
280 if ($honeypotNameFromSession) {
281 $honeypotElement = $this->lastDisplayedPage->createElement($honeypotNameFromSession, $renderingOptions['honeypot']['formElementToUse']);
282 $validator = $this->objectManager->get(EmptyValidator::class);
283 $honeypotElement->addValidator($validator);
284 }
285 }
286 }
287
288 /**
289 * Renders a hidden field if the honey pot is active.
290 */
291 protected function renderHoneypot()
292 {
293 $renderingOptions = $this->formDefinition->getRenderingOptions();
294 if (!isset($renderingOptions['honeypot']['enable']) || $renderingOptions['honeypot']['enable'] === false || TYPO3_MODE === 'BE') {
295 return;
296 }
297
298 ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']);
299
300 if (!$this->isAfterLastPage()) {
301 $elementsCount = count($this->currentPage->getElements());
302 if ($elementsCount === 0) {
303 return;
304 }
305
306 if (!$this->isFirstRequest()) {
307 $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage);
308 if ($honeypotNameFromSession) {
309 $honeypotElement = $this->formDefinition->getElementByIdentifier($honeypotNameFromSession);
310 if ($honeypotElement instanceof FormElementInterface) {
311 $this->lastDisplayedPage->removeElement($honeypotElement);
312 }
313 }
314 }
315
316 $elementsCount = count($this->currentPage->getElements());
317 $randomElementNumber = mt_rand(0, ($elementsCount - 1));
318 $honeypotName = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, mt_rand(5, 26));
319
320 $referenceElement = $this->currentPage->getElements()[$randomElementNumber];
321 $honeypotElement = $this->currentPage->createElement($honeypotName, $renderingOptions['honeypot']['formElementToUse']);
322 $validator = $this->objectManager->get(EmptyValidator::class);
323
324 $honeypotElement->addValidator($validator);
325 if (mt_rand(0, 1) === 1) {
326 $this->currentPage->moveElementAfter($honeypotElement, $referenceElement);
327 } else {
328 $this->currentPage->moveElementBefore($honeypotElement, $referenceElement);
329 }
330 $this->setHoneypotNameInSession($this->currentPage, $honeypotName);
331 }
332 }
333
334 /**
335 * @param Page $page
336 * return null|string
337 */
338 protected function getHoneypotNameFromSession(Page $page)
339 {
340 if ($this->getTypoScriptFrontendController()->loginUser) {
341 $honeypotNameFromSession = $this->getTypoScriptFrontendController()->fe_user->getKey(
342 'user',
343 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier()
344 );
345 } else {
346 $honeypotNameFromSession = $this->getTypoScriptFrontendController()->fe_user->getKey(
347 'ses',
348 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier()
349 );
350 }
351 return $honeypotNameFromSession;
352 }
353
354 /**
355 * @param Page $page
356 * @param string $honeypotName
357 */
358 protected function setHoneypotNameInSession(Page $page, string $honeypotName)
359 {
360 if ($this->getTypoScriptFrontendController()->loginUser) {
361 $this->getTypoScriptFrontendController()->fe_user->setKey(
362 'user',
363 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(),
364 $honeypotName
365 );
366 } else {
367 $this->getTypoScriptFrontendController()->fe_user->setKey(
368 'ses',
369 self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(),
370 $honeypotName
371 );
372 }
373 }
374
375 /**
376 * Returns TRUE if the last page of the form has been submitted, otherwise FALSE
377 *
378 * @return bool
379 */
380 protected function isAfterLastPage(): bool
381 {
382 return $this->currentPage === null;
383 }
384
385 /**
386 * Returns TRUE if no previous page is stored in the FormState, otherwise FALSE
387 *
388 * @return bool
389 */
390 protected function isFirstRequest(): bool
391 {
392 return $this->lastDisplayedPage === null;
393 }
394
395 /**
396 * Runs throuh all validations
397 */
398 protected function processSubmittedFormValues()
399 {
400 $result = $this->mapAndValidatePage($this->lastDisplayedPage);
401 if ($result->hasErrors() && !$this->userWentBackToPreviousStep()) {
402 $this->currentPage = $this->lastDisplayedPage;
403 $this->request->setOriginalRequestMappingResults($result);
404 }
405 }
406
407 /**
408 * returns TRUE if the user went back to any previous step in the form.
409 *
410 * @return bool
411 */
412 protected function userWentBackToPreviousStep(): bool
413 {
414 return !$this->isAfterLastPage() && !$this->isFirstRequest() && $this->currentPage->getIndex() < $this->lastDisplayedPage->getIndex();
415 }
416
417 /**
418 * @param Page $page
419 * @return Result
420 * @throws PropertyMappingException
421 */
422 protected function mapAndValidatePage(Page $page): Result
423 {
424 $result = $this->objectManager->get(Result::class);
425 $requestArguments = $this->request->getArguments();
426
427 $propertyPathsForWhichPropertyMappingShouldHappen = [];
428 $registerPropertyPaths = function ($propertyPath) use (&$propertyPathsForWhichPropertyMappingShouldHappen) {
429 $propertyPathParts = explode('.', $propertyPath);
430 $accumulatedPropertyPathParts = [];
431 foreach ($propertyPathParts as $propertyPathPart) {
432 $accumulatedPropertyPathParts[] = $propertyPathPart;
433 $temporaryPropertyPath = implode('.', $accumulatedPropertyPathParts);
434 $propertyPathsForWhichPropertyMappingShouldHappen[$temporaryPropertyPath] = $temporaryPropertyPath;
435 }
436 };
437
438 $value = null;
439
440 GeneralUtility::deprecationLog('EXT:form - calls for "onSubmit" are deprecated since TYPO3 v8 and will be removed in TYPO3 v9');
441 $page->onSubmit($this, $value, $requestArguments);
442
443 if (
444 isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'])
445 && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'])
446 ) {
447 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] as $className) {
448 $hookObj = GeneralUtility::makeInstance($className);
449 if (method_exists($hookObj, 'afterSubmit')) {
450 $value = $hookObj->afterSubmit(
451 $this,
452 $page,
453 $value,
454 $requestArguments
455 );
456 }
457 }
458 }
459
460 foreach ($page->getElementsRecursively() as $element) {
461 try {
462 $value = ArrayUtility::getValueByPath($requestArguments, $element->getIdentifier(), '.');
463 } catch (\RuntimeException $exception) {
464 $value = null;
465 }
466
467 GeneralUtility::deprecationLog('EXT:form - calls for "onSubmit" are deprecated since TYPO3 v8 and will be removed in TYPO3 v9');
468 $element->onSubmit($this, $value, $requestArguments);
469
470 if (
471 isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'])
472 && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'])
473 ) {
474 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] as $className) {
475 $hookObj = GeneralUtility::makeInstance($className);
476 if (method_exists($hookObj, 'afterSubmit')) {
477 $value = $hookObj->afterSubmit(
478 $this,
479 $element,
480 $value,
481 $requestArguments
482 );
483 }
484 }
485 }
486
487 $this->formState->setFormValue($element->getIdentifier(), $value);
488 $registerPropertyPaths($element->getIdentifier());
489 }
490
491 // The more parts the path has, the more early it is processed
492 usort($propertyPathsForWhichPropertyMappingShouldHappen, function ($a, $b) {
493 return substr_count($b, '.') - substr_count($a, '.');
494 });
495
496 $processingRules = $this->formDefinition->getProcessingRules();
497
498 foreach ($propertyPathsForWhichPropertyMappingShouldHappen as $propertyPath) {
499 if (isset($processingRules[$propertyPath])) {
500 $processingRule = $processingRules[$propertyPath];
501 $value = $this->formState->getFormValue($propertyPath);
502 try {
503 $value = $processingRule->process($value);
504 } catch (PropertyException $exception) {
505 throw new PropertyMappingException(
506 'Failed to process FormValue at "' . $propertyPath . '" from "' . gettype($value) . '" to "' . $processingRule->getDataType() . '"',
507 1480024933,
508 $exception
509 );
510 }
511 $result->forProperty($this->getIdentifier() . '.' . $propertyPath)->merge($processingRule->getProcessingMessages());
512 $this->formState->setFormValue($propertyPath, $value);
513 }
514 }
515
516 return $result;
517 }
518
519 /**
520 * Override the current page taken from the request, rendering the page with index $pageIndex instead.
521 *
522 * This is typically not needed in production code, but it is very helpful when displaying
523 * some kind of "preview" of the form.
524 *
525 * @param int $pageIndex
526 * @api
527 */
528 public function overrideCurrentPage(int $pageIndex)
529 {
530 $this->currentPage = $this->formDefinition->getPageByIndex($pageIndex);
531 }
532
533 /**
534 * Render this form.
535 *
536 * @return string|null rendered form
537 * @throws RenderingException
538 * @api
539 */
540 public function render()
541 {
542 if ($this->isAfterLastPage()) {
543 return $this->invokeFinishers();
544 }
545
546 $this->formState->setLastDisplayedPageIndex($this->currentPage->getIndex());
547
548 if ($this->formDefinition->getRendererClassName() === '') {
549 throw new RenderingException(sprintf('The form definition "%s" does not have a rendererClassName set.', $this->formDefinition->getIdentifier()), 1326095912);
550 }
551 $rendererClassName = $this->formDefinition->getRendererClassName();
552 $renderer = $this->objectManager->get($rendererClassName);
553 if (!($renderer instanceof RendererInterface)) {
554 throw new RenderingException(sprintf('The renderer "%s" des not implement RendererInterface', $rendererClassName), 1326096024);
555 }
556
557 $controllerContext = $this->getControllerContext();
558
559 $renderer->setControllerContext($controllerContext);
560 $renderer->setFormRuntime($this);
561 return $renderer->render($this);
562 }
563
564 /**
565 * Executes all finishers of this form
566 *
567 * @return string
568 */
569 protected function invokeFinishers(): string
570 {
571 $finisherContext = $this->objectManager->get(
572 FinisherContext::class,
573 $this,
574 $this->getControllerContext()
575 );
576
577 $output = '';
578 $originalContent = $this->response->getContent();
579 $this->response->setContent(null);
580 foreach ($this->formDefinition->getFinishers() as $finisher) {
581 $finisherOutput = $finisher->execute($finisherContext);
582 if (is_string($finisherOutput) && !empty($finisherOutput)) {
583 $output .= $finisherOutput;
584 } else {
585 $output .= $this->response->getContent();
586 $this->response->setContent(null);
587 }
588
589 if ($finisherContext->isCancelled()) {
590 break;
591 }
592 }
593 $this->response->setContent($originalContent);
594
595 return $output;
596 }
597
598 /**
599 * @return string The identifier of underlying form
600 * @api
601 */
602 public function getIdentifier(): string
603 {
604 return $this->formDefinition->getIdentifier();
605 }
606
607 /**
608 * Get the request this object is bound to.
609 *
610 * This is mostly relevant inside Finishers, where you f.e. want to redirect
611 * the user to another page.
612 *
613 * @return Request the request this object is bound to
614 * @api
615 */
616 public function getRequest(): Request
617 {
618 return $this->request;
619 }
620
621 /**
622 * Get the response this object is bound to.
623 *
624 * This is mostly relevant inside Finishers, where you f.e. want to set response
625 * headers or output content.
626 *
627 * @return Response the response this object is bound to
628 * @api
629 */
630 public function getResponse(): Response
631 {
632 return $this->response;
633 }
634
635 /**
636 * Returns the currently selected page
637 *
638 * @return Page
639 * @api
640 */
641 public function getCurrentPage(): Page
642 {
643 return $this->currentPage;
644 }
645
646 /**
647 * Returns the previous page of the currently selected one or NULL if there is no previous page
648 *
649 * @return Page|null
650 * @api
651 */
652 public function getPreviousPage()
653 {
654 $previousPageIndex = $this->currentPage->getIndex() - 1;
655 if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) {
656 return $this->formDefinition->getPageByIndex($previousPageIndex);
657 }
658 return null;
659 }
660
661 /**
662 * Returns the next page of the currently selected one or NULL if there is no next page
663 *
664 * @return Page|null
665 * @api
666 */
667 public function getNextPage()
668 {
669 $nextPageIndex = $this->currentPage->getIndex() + 1;
670 if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) {
671 return $this->formDefinition->getPageByIndex($nextPageIndex);
672 }
673 return null;
674 }
675
676 /**
677 * @return ControllerContext
678 */
679 protected function getControllerContext(): ControllerContext
680 {
681 $uriBuilder = $this->objectManager->get(UriBuilder::class);
682 $uriBuilder->setRequest($this->request);
683 $controllerContext = $this->objectManager->get(ControllerContext::class);
684 $controllerContext->setRequest($this->request);
685 $controllerContext->setResponse($this->response);
686 $controllerContext->setArguments($this->objectManager->get(Arguments::class, []));
687 $controllerContext->setUriBuilder($uriBuilder);
688 return $controllerContext;
689 }
690
691 /**
692 * Abstract "type" of this Renderable. Is used during the rendering process
693 * to determine the template file or the View PHP class being used to render
694 * the particular element.
695 *
696 * @return string
697 * @api
698 */
699 public function getType(): string
700 {
701 return $this->formDefinition->getType();
702 }
703
704 /**
705 * @param string $identifier
706 * @return bool
707 * @internal
708 */
709 public function offsetExists($identifier)
710 {
711 if ($this->getElementValue($identifier) !== null) {
712 return true;
713 }
714
715 if (is_callable([$this, 'get' . ucfirst($identifier)])) {
716 return true;
717 }
718 if (is_callable([$this, 'has' . ucfirst($identifier)])) {
719 return true;
720 }
721 if (is_callable([$this, 'is' . ucfirst($identifier)])) {
722 return true;
723 }
724 if (property_exists($this, $identifier)) {
725 $propertyReflection = new PropertyReflection($this, $identifier);
726 return $propertyReflection->isPublic();
727 }
728
729 return false;
730 }
731
732 /**
733 * @param string $identifier
734 * @return mixed
735 * @internal
736 */
737 public function offsetGet($identifier)
738 {
739 if ($this->getElementValue($identifier) !== null) {
740 return $this->getElementValue($identifier);
741 }
742 $getterMethodName = 'get' . ucfirst($identifier);
743 if (is_callable([$this, $getterMethodName])) {
744 return $this->{$getterMethodName}();
745 }
746 return null;
747 }
748
749 /**
750 * @param string $identifier
751 * @param mixed $value
752 * @internal
753 */
754 public function offsetSet($identifier, $value)
755 {
756 $this->formState->setFormValue($identifier, $value);
757 }
758
759 /**
760 * @param string $identifier
761 * @internal
762 */
763 public function offsetUnset($identifier)
764 {
765 $this->formState->setFormValue($identifier, null);
766 }
767
768 /**
769 * Returns the value of the specified element
770 *
771 * @param string $identifier
772 * @return mixed
773 * @api
774 */
775 public function getElementValue(string $identifier)
776 {
777 $formValue = $this->formState->getFormValue($identifier);
778 if ($formValue !== null) {
779 return $formValue;
780 }
781 return $this->formDefinition->getElementDefaultValueByIdentifier($identifier);
782 }
783
784 /**
785 * @return array<Page> The Form's pages in the correct order
786 * @api
787 */
788 public function getPages(): array
789 {
790 return $this->formDefinition->getPages();
791 }
792
793 /**
794 * @return FormState
795 * @internal
796 */
797 public function getFormState(): FormState
798 {
799 return $this->formState;
800 }
801
802 /**
803 * Get all rendering options
804 *
805 * @return array associative array of rendering options
806 * @api
807 */
808 public function getRenderingOptions(): array
809 {
810 return $this->formDefinition->getRenderingOptions();
811 }
812
813 /**
814 * Get the renderer class name to be used to display this renderable;
815 * must implement RendererInterface
816 *
817 * @return string the renderer class name
818 * @api
819 */
820 public function getRendererClassName(): string
821 {
822 return $this->formDefinition->getRendererClassName();
823 }
824
825 /**
826 * Get the label which shall be displayed next to the form element
827 *
828 * @return string
829 * @api
830 */
831 public function getLabel(): string
832 {
833 return $this->formDefinition->getLabel();
834 }
835
836 /**
837 * Get the template name of the renderable
838 *
839 * @return string
840 * @api
841 */
842 public function getTemplateName(): string
843 {
844 return $this->formDefinition->getTemplateName();
845 }
846
847 /**
848 * Get the underlying form definition from the runtime
849 *
850 * @return FormDefinition
851 * @api
852 */
853 public function getFormDefinition(): FormDefinition
854 {
855 return $this->formDefinition;
856 }
857
858 /**
859 * This is a callback that is invoked by the Renderer before the corresponding element is rendered.
860 * Use this to access previously submitted values and/or modify the $formRuntime before an element
861 * is outputted to the browser.
862 *
863 * @param FormRuntime $formRuntime
864 * @api
865 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
866 */
867 public function beforeRendering(FormRuntime $formRuntime)
868 {
869 GeneralUtility::logDeprecatedFunction();
870 }
871
872 /**
873 * @return TypoScriptFrontendController
874 */
875 protected function getTypoScriptFrontendController()
876 {
877 return $GLOBALS['TSFE'];
878 }
879 }