Total Complexity | 149 |
Total Lines | 1082 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like FormRuntime often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use FormRuntime, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
108 | class FormRuntime implements RootRenderableInterface, \ArrayAccess |
||
109 | { |
||
110 | const HONEYPOT_NAME_SESSION_IDENTIFIER = 'tx_form_honeypot_name_'; |
||
111 | |||
112 | protected ContainerInterface $container; |
||
113 | protected ObjectManagerInterface $objectManager; |
||
114 | protected ?FormDefinition $formDefinition = null; |
||
115 | protected ?Request $request = null; |
||
116 | protected ResponseInterface $response; |
||
117 | protected HashService $hashService; |
||
118 | protected ConfigurationManagerInterface $configurationManager; |
||
119 | |||
120 | /** |
||
121 | * @var FormState |
||
122 | */ |
||
123 | protected $formState; |
||
124 | |||
125 | /** |
||
126 | * Individual unique random form session identifier valid |
||
127 | * for current user session. This value is not persisted server-side. |
||
128 | * |
||
129 | * @var FormSession|null |
||
130 | */ |
||
131 | protected $formSession; |
||
132 | |||
133 | /** |
||
134 | * The current page is the page which will be displayed to the user |
||
135 | * during rendering. |
||
136 | * |
||
137 | * If $currentPage is NULL, the *last* page has been submitted and |
||
138 | * finishing actions need to take place. You should use $this->isAfterLastPage() |
||
139 | * instead of explicitly checking for NULL. |
||
140 | * |
||
141 | * @var Page|null |
||
142 | */ |
||
143 | protected $currentPage; |
||
144 | |||
145 | /** |
||
146 | * Reference to the page which has been shown on the last request (i.e. |
||
147 | * we have to handle the submitted data from lastDisplayedPage) |
||
148 | * |
||
149 | * @var Page |
||
150 | */ |
||
151 | protected $lastDisplayedPage; |
||
152 | |||
153 | /** |
||
154 | * The current site language configuration. |
||
155 | * |
||
156 | * @var SiteLanguage |
||
157 | */ |
||
158 | protected $currentSiteLanguage; |
||
159 | |||
160 | /** |
||
161 | * Reference to the current running finisher |
||
162 | * |
||
163 | * @var FinisherInterface |
||
164 | */ |
||
165 | protected $currentFinisher; |
||
166 | |||
167 | public function __construct( |
||
168 | ContainerInterface $container, |
||
169 | ObjectManagerInterface $objectManager, |
||
170 | ConfigurationManagerInterface $configurationManager, |
||
171 | HashService $hashService |
||
172 | ) { |
||
173 | $this->container = $container; |
||
174 | // @deprecated since v11, will be removed in v12 |
||
175 | $this->objectManager = $objectManager; |
||
176 | $this->configurationManager = $configurationManager; |
||
177 | $this->hashService = $hashService; |
||
178 | $this->response = new Response(); |
||
179 | } |
||
180 | |||
181 | public function setFormDefinition(FormDefinition $formDefinition) |
||
182 | { |
||
183 | $this->formDefinition = $formDefinition; |
||
184 | } |
||
185 | |||
186 | public function setRequest(Request $request) |
||
187 | { |
||
188 | $this->request = clone $request; |
||
189 | } |
||
190 | |||
191 | public function initialize() |
||
192 | { |
||
193 | $arguments = $this->request->getArguments(); |
||
|
|||
194 | $formIdentifier = $this->formDefinition->getIdentifier(); |
||
195 | if (isset($arguments[$formIdentifier])) { |
||
196 | $this->request->setArguments($arguments[$formIdentifier]); |
||
197 | } |
||
198 | |||
199 | $this->initializeCurrentSiteLanguage(); |
||
200 | $this->initializeFormSessionFromRequest(); |
||
201 | $this->initializeFormStateFromRequest(); |
||
202 | $this->triggerAfterFormStateInitialized(); |
||
203 | $this->processVariants(); |
||
204 | $this->initializeCurrentPageFromRequest(); |
||
205 | $this->initializeHoneypotFromRequest(); |
||
206 | |||
207 | // Only validate and set form values within the form state |
||
208 | // if the current request is not the very first request |
||
209 | // and the current request can be processed (POST request and uncached). |
||
210 | if (!$this->isFirstRequest() && $this->canProcessFormSubmission()) { |
||
211 | $this->processSubmittedFormValues(); |
||
212 | } |
||
213 | |||
214 | $this->renderHoneypot(); |
||
215 | } |
||
216 | |||
217 | /** |
||
218 | * @todo `FormRuntime::$formSession` is still vulnerable to session fixation unless a real cookie-based process is used |
||
219 | */ |
||
220 | protected function initializeFormSessionFromRequest(): void |
||
221 | { |
||
222 | // Initialize the form session only if the current request can be processed |
||
223 | // (POST request and uncached) to ensure unique sessions for each form submitter. |
||
224 | if (!$this->canProcessFormSubmission()) { |
||
225 | return; |
||
226 | } |
||
227 | |||
228 | $sessionIdentifierFromRequest = $this->request->getInternalArgument('__session'); |
||
229 | $this->formSession = GeneralUtility::makeInstance(FormSession::class, $sessionIdentifierFromRequest); |
||
230 | } |
||
231 | |||
232 | /** |
||
233 | * Initializes the current state of the form, based on the request |
||
234 | * @throws BadRequestException |
||
235 | */ |
||
236 | protected function initializeFormStateFromRequest() |
||
237 | { |
||
238 | // Only try to reconstitute the form state if the current request |
||
239 | // is not the very first request and if the current request can |
||
240 | // be processed (POST request and uncached). |
||
241 | $serializedFormStateWithHmac = $this->request->getInternalArgument('__state'); |
||
242 | if ($serializedFormStateWithHmac === null || !$this->canProcessFormSubmission()) { |
||
243 | $this->formState = GeneralUtility::makeInstance(FormState::class); |
||
244 | } else { |
||
245 | try { |
||
246 | $serializedFormState = $this->hashService->validateAndStripHmac($serializedFormStateWithHmac); |
||
247 | } catch (InvalidHashException | InvalidArgumentForHashGenerationException $e) { |
||
248 | throw new BadRequestException('The HMAC of the form state could not be validated.', 1581862823); |
||
249 | } |
||
250 | $this->formState = unserialize(base64_decode($serializedFormState)); |
||
251 | } |
||
252 | } |
||
253 | |||
254 | protected function triggerAfterFormStateInitialized(): void |
||
255 | { |
||
256 | foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterFormStateInitialized'] ?? [] as $className) { |
||
257 | $hookObj = GeneralUtility::makeInstance($className); |
||
258 | if ($hookObj instanceof AfterFormStateInitializedInterface) { |
||
259 | $hookObj->afterFormStateInitialized($this); |
||
260 | } |
||
261 | } |
||
262 | } |
||
263 | |||
264 | /** |
||
265 | * Initializes the current page data based on the current request, also modifiable by a hook |
||
266 | */ |
||
267 | protected function initializeCurrentPageFromRequest() |
||
268 | { |
||
269 | // If there was no previous form submissions or if the current request |
||
270 | // can't be processed (no POST request and/or cached) then display the first |
||
271 | // form step |
||
272 | if (!$this->formState->isFormSubmitted() || !$this->canProcessFormSubmission()) { |
||
273 | $this->currentPage = $this->formDefinition->getPageByIndex(0); |
||
274 | |||
275 | if (!$this->currentPage->isEnabled()) { |
||
276 | throw new FormException('Disabling the first page is not allowed', 1527186844); |
||
277 | } |
||
278 | |||
279 | foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] ?? [] as $className) { |
||
280 | $hookObj = GeneralUtility::makeInstance($className); |
||
281 | if (method_exists($hookObj, 'afterInitializeCurrentPage')) { |
||
282 | $this->currentPage = $hookObj->afterInitializeCurrentPage( |
||
283 | $this, |
||
284 | $this->currentPage, |
||
285 | null, |
||
286 | $this->request->getArguments() |
||
287 | ); |
||
288 | } |
||
289 | } |
||
290 | return; |
||
291 | } |
||
292 | |||
293 | $this->lastDisplayedPage = $this->formDefinition->getPageByIndex($this->formState->getLastDisplayedPageIndex()); |
||
294 | $currentPageIndex = (int)$this->request->getInternalArgument('__currentPage'); |
||
295 | |||
296 | if ($this->userWentBackToPreviousStep()) { |
||
297 | if ($currentPageIndex < $this->lastDisplayedPage->getIndex()) { |
||
298 | $currentPageIndex = $this->lastDisplayedPage->getIndex(); |
||
299 | } |
||
300 | } else { |
||
301 | if ($currentPageIndex > $this->lastDisplayedPage->getIndex() + 1) { |
||
302 | $currentPageIndex = $this->lastDisplayedPage->getIndex() + 1; |
||
303 | } |
||
304 | } |
||
305 | |||
306 | if ($currentPageIndex >= count($this->formDefinition->getPages())) { |
||
307 | // Last Page |
||
308 | $this->currentPage = null; |
||
309 | } else { |
||
310 | $this->currentPage = $this->formDefinition->getPageByIndex($currentPageIndex); |
||
311 | |||
312 | if (!$this->currentPage->isEnabled()) { |
||
313 | if ($currentPageIndex === 0) { |
||
314 | throw new FormException('Disabling the first page is not allowed', 1527186845); |
||
315 | } |
||
316 | |||
317 | if ($this->userWentBackToPreviousStep()) { |
||
318 | $this->currentPage = $this->getPreviousEnabledPage(); |
||
319 | } else { |
||
320 | $this->currentPage = $this->getNextEnabledPage(); |
||
321 | } |
||
322 | } |
||
323 | } |
||
324 | |||
325 | foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] ?? [] as $className) { |
||
326 | $hookObj = GeneralUtility::makeInstance($className); |
||
327 | if (method_exists($hookObj, 'afterInitializeCurrentPage')) { |
||
328 | $this->currentPage = $hookObj->afterInitializeCurrentPage( |
||
329 | $this, |
||
330 | $this->currentPage, |
||
331 | $this->lastDisplayedPage, |
||
332 | $this->request->getArguments() |
||
333 | ); |
||
334 | } |
||
335 | } |
||
336 | } |
||
337 | |||
338 | /** |
||
339 | * Checks if the honey pot is active, and adds a validator if so. |
||
340 | */ |
||
341 | protected function initializeHoneypotFromRequest() |
||
342 | { |
||
343 | $renderingOptions = $this->formDefinition->getRenderingOptions(); |
||
344 | if (!isset($renderingOptions['honeypot']['enable']) |
||
345 | || $renderingOptions['honeypot']['enable'] === false |
||
346 | || (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface |
||
347 | && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()) |
||
348 | ) { |
||
349 | return; |
||
350 | } |
||
351 | |||
352 | ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']); |
||
353 | |||
354 | if (!$this->isFirstRequest()) { |
||
355 | $elementsCount = count($this->lastDisplayedPage->getElements()); |
||
356 | if ($elementsCount === 0) { |
||
357 | return; |
||
358 | } |
||
359 | |||
360 | $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage); |
||
361 | if ($honeypotNameFromSession) { |
||
362 | $honeypotElement = $this->lastDisplayedPage->createElement($honeypotNameFromSession, $renderingOptions['honeypot']['formElementToUse']); |
||
363 | $validator = GeneralUtility::makeInstance(EmptyValidator::class); |
||
364 | $honeypotElement->addValidator($validator); |
||
365 | } |
||
366 | } |
||
367 | } |
||
368 | |||
369 | /** |
||
370 | * Renders a hidden field if the honey pot is active. |
||
371 | */ |
||
372 | protected function renderHoneypot() |
||
373 | { |
||
374 | $renderingOptions = $this->formDefinition->getRenderingOptions(); |
||
375 | if (!isset($renderingOptions['honeypot']['enable']) |
||
376 | || $this->currentPage === null |
||
377 | || $renderingOptions['honeypot']['enable'] === false |
||
378 | || (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface |
||
379 | && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()) |
||
380 | ) { |
||
381 | return; |
||
382 | } |
||
383 | |||
384 | ArrayUtility::assertAllArrayKeysAreValid($renderingOptions['honeypot'], ['enable', 'formElementToUse']); |
||
385 | |||
386 | if (!$this->isAfterLastPage()) { |
||
387 | $elementsCount = count($this->currentPage->getElements()); |
||
388 | if ($elementsCount === 0) { |
||
389 | return; |
||
390 | } |
||
391 | |||
392 | if (!$this->isFirstRequest()) { |
||
393 | $honeypotNameFromSession = $this->getHoneypotNameFromSession($this->lastDisplayedPage); |
||
394 | if ($honeypotNameFromSession) { |
||
395 | $honeypotElement = $this->formDefinition->getElementByIdentifier($honeypotNameFromSession); |
||
396 | if ($honeypotElement instanceof FormElementInterface) { |
||
397 | $this->lastDisplayedPage->removeElement($honeypotElement); |
||
398 | } |
||
399 | } |
||
400 | } |
||
401 | |||
402 | $elementsCount = count($this->currentPage->getElements()); |
||
403 | $randomElementNumber = random_int(0, $elementsCount - 1); |
||
404 | $honeypotName = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, random_int(5, 26)); |
||
405 | |||
406 | $referenceElement = $this->currentPage->getElements()[$randomElementNumber]; |
||
407 | $honeypotElement = $this->currentPage->createElement($honeypotName, $renderingOptions['honeypot']['formElementToUse']); |
||
408 | $validator = GeneralUtility::makeInstance(EmptyValidator::class); |
||
409 | |||
410 | $honeypotElement->addValidator($validator); |
||
411 | if (random_int(0, 1) === 1) { |
||
412 | $this->currentPage->moveElementAfter($honeypotElement, $referenceElement); |
||
413 | } else { |
||
414 | $this->currentPage->moveElementBefore($honeypotElement, $referenceElement); |
||
415 | } |
||
416 | $this->setHoneypotNameInSession($this->currentPage, $honeypotName); |
||
417 | } |
||
418 | } |
||
419 | |||
420 | /** |
||
421 | * @param Page $page |
||
422 | * @return string|null |
||
423 | */ |
||
424 | protected function getHoneypotNameFromSession(Page $page) |
||
425 | { |
||
426 | if ($this->isFrontendUserAuthenticated()) { |
||
427 | $honeypotNameFromSession = $this->getFrontendUser()->getKey( |
||
428 | 'user', |
||
429 | self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier() |
||
430 | ); |
||
431 | } else { |
||
432 | $honeypotNameFromSession = $this->getFrontendUser()->getKey( |
||
433 | 'ses', |
||
434 | self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier() |
||
435 | ); |
||
436 | } |
||
437 | return $honeypotNameFromSession; |
||
438 | } |
||
439 | |||
440 | /** |
||
441 | * @param Page $page |
||
442 | * @param string $honeypotName |
||
443 | */ |
||
444 | protected function setHoneypotNameInSession(Page $page, string $honeypotName) |
||
445 | { |
||
446 | if ($this->isFrontendUserAuthenticated()) { |
||
447 | $this->getFrontendUser()->setKey( |
||
448 | 'user', |
||
449 | self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(), |
||
450 | $honeypotName |
||
451 | ); |
||
452 | } else { |
||
453 | $this->getFrontendUser()->setKey( |
||
454 | 'ses', |
||
455 | self::HONEYPOT_NAME_SESSION_IDENTIFIER . $this->getIdentifier() . $page->getIdentifier(), |
||
456 | $honeypotName |
||
457 | ); |
||
458 | } |
||
459 | } |
||
460 | |||
461 | /** |
||
462 | * Necessary to know if honeypot information should be stored in the user session info, or in the anonymous session |
||
463 | * |
||
464 | * @return bool true when a frontend user is logged, otherwise false |
||
465 | */ |
||
466 | protected function isFrontendUserAuthenticated(): bool |
||
467 | { |
||
468 | return (bool)GeneralUtility::makeInstance(Context::class) |
||
469 | ->getPropertyFromAspect('frontend.user', 'isLoggedIn', false); |
||
470 | } |
||
471 | |||
472 | protected function processVariants() |
||
473 | { |
||
474 | $conditionResolver = $this->getConditionResolver(); |
||
475 | |||
476 | $renderables = array_merge([$this->formDefinition], $this->formDefinition->getRenderablesRecursively()); |
||
477 | foreach ($renderables as $renderable) { |
||
478 | if ($renderable instanceof VariableRenderableInterface) { |
||
479 | $variants = $renderable->getVariants(); |
||
480 | foreach ($variants as $variant) { |
||
481 | if ($variant->conditionMatches($conditionResolver)) { |
||
482 | $variant->apply(); |
||
483 | } |
||
484 | } |
||
485 | } |
||
486 | } |
||
487 | } |
||
488 | |||
489 | /** |
||
490 | * Returns TRUE if the last page of the form has been submitted, otherwise FALSE |
||
491 | * |
||
492 | * @return bool |
||
493 | */ |
||
494 | protected function isAfterLastPage(): bool |
||
497 | } |
||
498 | |||
499 | /** |
||
500 | * Returns TRUE if no previous page is stored in the FormState, otherwise FALSE |
||
501 | * |
||
502 | * @return bool |
||
503 | */ |
||
504 | protected function isFirstRequest(): bool |
||
505 | { |
||
506 | return $this->lastDisplayedPage === null; |
||
507 | } |
||
508 | |||
509 | /** |
||
510 | * @return bool |
||
511 | */ |
||
512 | protected function isPostRequest(): bool |
||
513 | { |
||
514 | return $this->getRequest()->getMethod() === 'POST'; |
||
515 | } |
||
516 | |||
517 | /** |
||
518 | * Determine whether the surrounding content object is cached. |
||
519 | * If no surrounding content object can be found (which would be strange) |
||
520 | * we assume a cached request for safety which means that an empty form |
||
521 | * will be rendered. |
||
522 | * |
||
523 | * @todo: this should be checked against https://forge.typo3.org/issues/91625 as this was fixed differently for UriBuilder |
||
524 | * @return bool |
||
525 | */ |
||
526 | protected function isRenderedCached(): bool |
||
527 | { |
||
528 | $contentObject = $this->configurationManager->getContentObject(); |
||
529 | return $contentObject === null |
||
530 | ? true |
||
531 | // @todo this does not work when rendering a cached `FLUIDTEMPLATE` (not nested in `COA_INT`) |
||
532 | : $contentObject->getUserObjectType() === ContentObjectRenderer::OBJECTTYPE_USER; |
||
533 | } |
||
534 | |||
535 | /** |
||
536 | * Runs through all validations |
||
537 | */ |
||
538 | protected function processSubmittedFormValues() |
||
539 | { |
||
540 | $result = $this->mapAndValidatePage($this->lastDisplayedPage); |
||
541 | if ($result->hasErrors() && !$this->userWentBackToPreviousStep()) { |
||
542 | $this->currentPage = $this->lastDisplayedPage; |
||
543 | $this->request->setOriginalRequestMappingResults($result); |
||
544 | } |
||
545 | } |
||
546 | |||
547 | /** |
||
548 | * returns TRUE if the user went back to any previous step in the form. |
||
549 | * |
||
550 | * @return bool |
||
551 | */ |
||
552 | protected function userWentBackToPreviousStep(): bool |
||
553 | { |
||
554 | return !$this->isAfterLastPage() && !$this->isFirstRequest() && $this->currentPage->getIndex() < $this->lastDisplayedPage->getIndex(); |
||
555 | } |
||
556 | |||
557 | /** |
||
558 | * @param Page $page |
||
559 | * @return Result |
||
560 | * @throws PropertyMappingException |
||
561 | */ |
||
562 | protected function mapAndValidatePage(Page $page): Result |
||
563 | { |
||
564 | $result = GeneralUtility::makeInstance(Result::class); |
||
565 | $requestArguments = $this->request->getArguments(); |
||
566 | |||
567 | $propertyPathsForWhichPropertyMappingShouldHappen = []; |
||
568 | $registerPropertyPaths = function ($propertyPath) use (&$propertyPathsForWhichPropertyMappingShouldHappen) { |
||
569 | $propertyPathParts = explode('.', $propertyPath); |
||
570 | $accumulatedPropertyPathParts = []; |
||
571 | foreach ($propertyPathParts as $propertyPathPart) { |
||
572 | $accumulatedPropertyPathParts[] = $propertyPathPart; |
||
573 | $temporaryPropertyPath = implode('.', $accumulatedPropertyPathParts); |
||
574 | $propertyPathsForWhichPropertyMappingShouldHappen[$temporaryPropertyPath] = $temporaryPropertyPath; |
||
575 | } |
||
576 | }; |
||
577 | |||
578 | $value = null; |
||
579 | |||
580 | foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] ?? [] as $className) { |
||
581 | $hookObj = GeneralUtility::makeInstance($className); |
||
582 | if (method_exists($hookObj, 'afterSubmit')) { |
||
583 | $value = $hookObj->afterSubmit( |
||
584 | $this, |
||
585 | $page, |
||
586 | $value, |
||
587 | $requestArguments |
||
588 | ); |
||
589 | } |
||
590 | } |
||
591 | |||
592 | foreach ($page->getElementsRecursively() as $element) { |
||
593 | if (!$element->isEnabled()) { |
||
594 | continue; |
||
595 | } |
||
596 | |||
597 | try { |
||
598 | $value = ArrayUtility::getValueByPath($requestArguments, $element->getIdentifier(), '.'); |
||
599 | } catch (MissingArrayPathException $exception) { |
||
600 | $value = null; |
||
601 | } |
||
602 | |||
603 | foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] ?? [] as $className) { |
||
604 | $hookObj = GeneralUtility::makeInstance($className); |
||
605 | if (method_exists($hookObj, 'afterSubmit')) { |
||
606 | $value = $hookObj->afterSubmit( |
||
607 | $this, |
||
608 | $element, |
||
609 | $value, |
||
610 | $requestArguments |
||
611 | ); |
||
612 | } |
||
613 | } |
||
614 | |||
615 | $this->formState->setFormValue($element->getIdentifier(), $value); |
||
616 | $registerPropertyPaths($element->getIdentifier()); |
||
617 | } |
||
618 | |||
619 | // The more parts the path has, the more early it is processed |
||
620 | usort($propertyPathsForWhichPropertyMappingShouldHappen, function ($a, $b) { |
||
621 | return substr_count($b, '.') - substr_count($a, '.'); |
||
622 | }); |
||
623 | |||
624 | $processingRules = $this->formDefinition->getProcessingRules(); |
||
625 | |||
626 | foreach ($propertyPathsForWhichPropertyMappingShouldHappen as $propertyPath) { |
||
627 | if (isset($processingRules[$propertyPath])) { |
||
628 | $processingRule = $processingRules[$propertyPath]; |
||
629 | $value = $this->formState->getFormValue($propertyPath); |
||
630 | try { |
||
631 | $value = $processingRule->process($value); |
||
632 | } catch (PropertyException $exception) { |
||
633 | throw new PropertyMappingException( |
||
634 | 'Failed to process FormValue at "' . $propertyPath . '" from "' . gettype($value) . '" to "' . $processingRule->getDataType() . '"', |
||
635 | 1480024933, |
||
636 | $exception |
||
637 | ); |
||
638 | } |
||
639 | $result->forProperty($this->getIdentifier() . '.' . $propertyPath)->merge($processingRule->getProcessingMessages()); |
||
640 | $this->formState->setFormValue($propertyPath, $value); |
||
641 | } |
||
642 | } |
||
643 | |||
644 | return $result; |
||
645 | } |
||
646 | |||
647 | /** |
||
648 | * Override the current page taken from the request, rendering the page with index $pageIndex instead. |
||
649 | * |
||
650 | * This is typically not needed in production code, but it is very helpful when displaying |
||
651 | * some kind of "preview" of the form (e.g. form editor). |
||
652 | * |
||
653 | * @param int $pageIndex |
||
654 | */ |
||
655 | public function overrideCurrentPage(int $pageIndex) |
||
656 | { |
||
657 | $this->currentPage = $this->formDefinition->getPageByIndex($pageIndex); |
||
658 | } |
||
659 | |||
660 | /** |
||
661 | * Render this form. |
||
662 | * |
||
663 | * @return string|null rendered form |
||
664 | * @throws RenderingException |
||
665 | */ |
||
666 | public function render() |
||
667 | { |
||
668 | if ($this->isAfterLastPage()) { |
||
669 | return $this->invokeFinishers(); |
||
670 | } |
||
671 | $this->processVariants(); |
||
672 | |||
673 | $this->formState->setLastDisplayedPageIndex($this->currentPage->getIndex()); |
||
674 | |||
675 | if ($this->formDefinition->getRendererClassName() === '') { |
||
676 | throw new RenderingException(sprintf('The form definition "%s" does not have a rendererClassName set.', $this->formDefinition->getIdentifier()), 1326095912); |
||
677 | } |
||
678 | $rendererClassName = $this->formDefinition->getRendererClassName(); |
||
679 | if ($this->container->has($rendererClassName)) { |
||
680 | $renderer = $this->container->get($rendererClassName); |
||
681 | } else { |
||
682 | // @deprecated since v11, will be removed in v12. |
||
683 | $renderer = $this->objectManager->get($rendererClassName); |
||
684 | } |
||
685 | if (!($renderer instanceof RendererInterface)) { |
||
686 | throw new RenderingException(sprintf('The renderer "%s" des not implement RendererInterface', $rendererClassName), 1326096024); |
||
687 | } |
||
688 | |||
689 | $controllerContext = $this->getControllerContext(); |
||
690 | |||
691 | $renderer->setControllerContext($controllerContext); |
||
692 | $renderer->setFormRuntime($this); |
||
693 | return $renderer->render(); |
||
694 | } |
||
695 | |||
696 | /** |
||
697 | * Executes all finishers of this form |
||
698 | * |
||
699 | * @return string |
||
700 | */ |
||
701 | protected function invokeFinishers(): string |
||
702 | { |
||
703 | $finisherContext = GeneralUtility::makeInstance( |
||
704 | FinisherContext::class, |
||
705 | $this, |
||
706 | $this->getControllerContext(), |
||
707 | $this->request |
||
708 | ); |
||
709 | |||
710 | $output = ''; |
||
711 | $this->response->getBody()->rewind(); |
||
712 | $originalContent = $this->response->getBody()->getContents(); |
||
713 | $this->response->getBody()->write(''); |
||
714 | foreach ($this->formDefinition->getFinishers() as $finisher) { |
||
715 | $this->currentFinisher = $finisher; |
||
716 | $this->processVariants(); |
||
717 | |||
718 | $finisherOutput = $finisher->execute($finisherContext); |
||
719 | if (is_string($finisherOutput) && !empty($finisherOutput)) { |
||
720 | $output .= $finisherOutput; |
||
721 | } else { |
||
722 | $this->response->getBody()->rewind(); |
||
723 | $output .= $this->response->getBody()->getContents(); |
||
724 | $this->response->getBody()->write(''); |
||
725 | } |
||
726 | |||
727 | if ($finisherContext->isCancelled()) { |
||
728 | break; |
||
729 | } |
||
730 | } |
||
731 | $this->response->getBody()->rewind(); |
||
732 | $this->response->getBody()->write($originalContent); |
||
733 | |||
734 | return $output; |
||
735 | } |
||
736 | |||
737 | /** |
||
738 | * @return string The identifier of underlying form |
||
739 | */ |
||
740 | public function getIdentifier(): string |
||
741 | { |
||
742 | return $this->formDefinition->getIdentifier(); |
||
743 | } |
||
744 | |||
745 | /** |
||
746 | * Get the request this object is bound to. |
||
747 | * |
||
748 | * This is mostly relevant inside Finishers, where you f.e. want to redirect |
||
749 | * the user to another page. |
||
750 | * |
||
751 | * @return Request the request this object is bound to |
||
752 | */ |
||
753 | public function getRequest(): Request |
||
754 | { |
||
755 | return $this->request; |
||
756 | } |
||
757 | |||
758 | /** |
||
759 | * Get the response this object is bound to. |
||
760 | * |
||
761 | * This is mostly relevant inside Finishers, where you f.e. want to set response |
||
762 | * headers or output content. |
||
763 | * |
||
764 | * @return ResponseInterface the response this object is bound to |
||
765 | */ |
||
766 | public function getResponse(): ResponseInterface |
||
767 | { |
||
768 | return $this->response; |
||
769 | } |
||
770 | |||
771 | /** |
||
772 | * Only process values if there is a post request and if the |
||
773 | * surrounding content object is uncached. |
||
774 | * Is this not the case, all possible submitted values will be discarded |
||
775 | * and the first form step will be shown with an empty form state. |
||
776 | * |
||
777 | * @return bool |
||
778 | * @internal |
||
779 | */ |
||
780 | public function canProcessFormSubmission(): bool |
||
781 | { |
||
782 | return $this->isPostRequest() && !$this->isRenderedCached(); |
||
783 | } |
||
784 | |||
785 | /** |
||
786 | * @return FormSession|null |
||
787 | * @internal |
||
788 | */ |
||
789 | public function getFormSession(): ?FormSession |
||
790 | { |
||
791 | return $this->formSession; |
||
792 | } |
||
793 | |||
794 | /** |
||
795 | * Returns the currently selected page |
||
796 | * |
||
797 | * @return Page|null |
||
798 | */ |
||
799 | public function getCurrentPage(): ?Page |
||
800 | { |
||
801 | return $this->currentPage; |
||
802 | } |
||
803 | |||
804 | /** |
||
805 | * Returns the previous page of the currently selected one or NULL if there is no previous page |
||
806 | * |
||
807 | * @return Page|null |
||
808 | */ |
||
809 | public function getPreviousPage(): ?Page |
||
810 | { |
||
811 | $previousPageIndex = $this->currentPage->getIndex() - 1; |
||
812 | if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) { |
||
813 | return $this->formDefinition->getPageByIndex($previousPageIndex); |
||
814 | } |
||
815 | return null; |
||
816 | } |
||
817 | |||
818 | /** |
||
819 | * Returns the next page of the currently selected one or NULL if there is no next page |
||
820 | * |
||
821 | * @return Page|null |
||
822 | */ |
||
823 | public function getNextPage(): ?Page |
||
824 | { |
||
825 | $nextPageIndex = $this->currentPage->getIndex() + 1; |
||
826 | if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) { |
||
827 | return $this->formDefinition->getPageByIndex($nextPageIndex); |
||
828 | } |
||
829 | return null; |
||
830 | } |
||
831 | |||
832 | /** |
||
833 | * Returns the previous enabled page of the currently selected one |
||
834 | * or NULL if there is no previous page |
||
835 | * |
||
836 | * @return Page|null |
||
837 | */ |
||
838 | public function getPreviousEnabledPage(): ?Page |
||
839 | { |
||
840 | $previousPage = null; |
||
841 | $previousPageIndex = $this->currentPage->getIndex() - 1; |
||
842 | while ($previousPageIndex >= 0) { |
||
843 | if ($this->formDefinition->hasPageWithIndex($previousPageIndex)) { |
||
844 | $previousPage = $this->formDefinition->getPageByIndex($previousPageIndex); |
||
845 | |||
846 | if ($previousPage->isEnabled()) { |
||
847 | break; |
||
848 | } |
||
849 | |||
850 | $previousPage = null; |
||
851 | $previousPageIndex--; |
||
852 | } else { |
||
853 | $previousPage = null; |
||
854 | break; |
||
855 | } |
||
856 | } |
||
857 | |||
858 | return $previousPage; |
||
859 | } |
||
860 | |||
861 | /** |
||
862 | * Returns the next enabled page of the currently selected one or |
||
863 | * NULL if there is no next page |
||
864 | * |
||
865 | * @return Page|null |
||
866 | */ |
||
867 | public function getNextEnabledPage(): ?Page |
||
868 | { |
||
869 | $nextPage = null; |
||
870 | $pageCount = count($this->formDefinition->getPages()); |
||
871 | $nextPageIndex = $this->currentPage->getIndex() + 1; |
||
872 | |||
873 | while ($nextPageIndex < $pageCount) { |
||
874 | if ($this->formDefinition->hasPageWithIndex($nextPageIndex)) { |
||
875 | $nextPage = $this->formDefinition->getPageByIndex($nextPageIndex); |
||
876 | $renderingOptions = $nextPage->getRenderingOptions(); |
||
877 | if ( |
||
878 | !isset($renderingOptions['enabled']) |
||
879 | || (bool)$renderingOptions['enabled'] |
||
880 | ) { |
||
881 | break; |
||
882 | } |
||
883 | $nextPage = null; |
||
884 | $nextPageIndex++; |
||
885 | } else { |
||
886 | $nextPage = null; |
||
887 | break; |
||
888 | } |
||
889 | } |
||
890 | |||
891 | return $nextPage; |
||
892 | } |
||
893 | |||
894 | /** |
||
895 | * @return ControllerContext |
||
896 | */ |
||
897 | protected function getControllerContext(): ControllerContext |
||
898 | { |
||
899 | $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); |
||
900 | $uriBuilder->setRequest($this->request); |
||
901 | $controllerContext = GeneralUtility::makeInstance(ControllerContext::class); |
||
902 | $controllerContext->setRequest($this->request); |
||
903 | $controllerContext->setArguments(GeneralUtility::makeInstance(Arguments::class)); |
||
904 | $controllerContext->setUriBuilder($uriBuilder); |
||
905 | return $controllerContext; |
||
906 | } |
||
907 | |||
908 | /** |
||
909 | * Abstract "type" of this Renderable. Is used during the rendering process |
||
910 | * to determine the template file or the View PHP class being used to render |
||
911 | * the particular element. |
||
912 | * |
||
913 | * @return string |
||
914 | */ |
||
915 | public function getType(): string |
||
916 | { |
||
917 | return $this->formDefinition->getType(); |
||
918 | } |
||
919 | |||
920 | /** |
||
921 | * @param string $identifier |
||
922 | * @return bool |
||
923 | * @internal |
||
924 | */ |
||
925 | public function offsetExists($identifier) |
||
926 | { |
||
927 | if ($this->getElementValue($identifier) !== null) { |
||
928 | return true; |
||
929 | } |
||
930 | |||
931 | if (is_callable([$this, 'get' . ucfirst($identifier)])) { |
||
932 | return true; |
||
933 | } |
||
934 | if (is_callable([$this, 'has' . ucfirst($identifier)])) { |
||
935 | return true; |
||
936 | } |
||
937 | if (is_callable([$this, 'is' . ucfirst($identifier)])) { |
||
938 | return true; |
||
939 | } |
||
940 | if (property_exists($this, $identifier)) { |
||
941 | $propertyReflection = new \ReflectionProperty($this, $identifier); |
||
942 | return $propertyReflection->isPublic(); |
||
943 | } |
||
944 | |||
945 | return false; |
||
946 | } |
||
947 | |||
948 | /** |
||
949 | * @param string $identifier |
||
950 | * @return mixed |
||
951 | * @internal |
||
952 | */ |
||
953 | public function offsetGet($identifier) |
||
954 | { |
||
955 | if ($this->getElementValue($identifier) !== null) { |
||
956 | return $this->getElementValue($identifier); |
||
957 | } |
||
958 | $getterMethodName = 'get' . ucfirst($identifier); |
||
959 | if (is_callable([$this, $getterMethodName])) { |
||
960 | return $this->{$getterMethodName}(); |
||
961 | } |
||
962 | return null; |
||
963 | } |
||
964 | |||
965 | /** |
||
966 | * @param string $identifier |
||
967 | * @param mixed $value |
||
968 | * @internal |
||
969 | */ |
||
970 | public function offsetSet($identifier, $value) |
||
971 | { |
||
972 | $this->formState->setFormValue($identifier, $value); |
||
973 | } |
||
974 | |||
975 | /** |
||
976 | * @param string $identifier |
||
977 | * @internal |
||
978 | */ |
||
979 | public function offsetUnset($identifier) |
||
980 | { |
||
981 | $this->formState->setFormValue($identifier, null); |
||
982 | } |
||
983 | |||
984 | /** |
||
985 | * Returns the value of the specified element |
||
986 | * |
||
987 | * @param string $identifier |
||
988 | * @return mixed |
||
989 | */ |
||
990 | public function getElementValue(string $identifier) |
||
991 | { |
||
992 | $formValue = $this->formState->getFormValue($identifier); |
||
993 | if ($formValue !== null) { |
||
994 | return $formValue; |
||
995 | } |
||
996 | return $this->formDefinition->getElementDefaultValueByIdentifier($identifier); |
||
997 | } |
||
998 | |||
999 | /** |
||
1000 | * @return array|Page[] The Form's pages in the correct order |
||
1001 | */ |
||
1002 | public function getPages(): array |
||
1003 | { |
||
1004 | return $this->formDefinition->getPages(); |
||
1005 | } |
||
1006 | |||
1007 | /** |
||
1008 | * @return FormState|null |
||
1009 | * @internal |
||
1010 | */ |
||
1011 | public function getFormState(): ?FormState |
||
1014 | } |
||
1015 | |||
1016 | /** |
||
1017 | * Get all rendering options |
||
1018 | * |
||
1019 | * @return array associative array of rendering options |
||
1020 | */ |
||
1021 | public function getRenderingOptions(): array |
||
1022 | { |
||
1023 | return $this->formDefinition->getRenderingOptions(); |
||
1024 | } |
||
1025 | |||
1026 | /** |
||
1027 | * Get the renderer class name to be used to display this renderable; |
||
1028 | * must implement RendererInterface |
||
1029 | * |
||
1030 | * @return string the renderer class name |
||
1031 | */ |
||
1032 | public function getRendererClassName(): string |
||
1033 | { |
||
1034 | return $this->formDefinition->getRendererClassName(); |
||
1035 | } |
||
1036 | |||
1037 | /** |
||
1038 | * Get the label which shall be displayed next to the form element |
||
1039 | * |
||
1040 | * @return string |
||
1041 | */ |
||
1042 | public function getLabel(): string |
||
1043 | { |
||
1044 | return $this->formDefinition->getLabel(); |
||
1045 | } |
||
1046 | |||
1047 | /** |
||
1048 | * Get the template name of the renderable |
||
1049 | * |
||
1050 | * @return string |
||
1051 | */ |
||
1052 | public function getTemplateName(): string |
||
1053 | { |
||
1054 | return $this->formDefinition->getTemplateName(); |
||
1055 | } |
||
1056 | |||
1057 | /** |
||
1058 | * Get the underlying form definition from the runtime |
||
1059 | * |
||
1060 | * @return FormDefinition |
||
1061 | */ |
||
1062 | public function getFormDefinition(): FormDefinition |
||
1063 | { |
||
1064 | return $this->formDefinition; |
||
1065 | } |
||
1066 | |||
1067 | /** |
||
1068 | * Get the current site language configuration. |
||
1069 | * |
||
1070 | * @return SiteLanguage |
||
1071 | */ |
||
1072 | public function getCurrentSiteLanguage(): ?SiteLanguage |
||
1073 | { |
||
1074 | return $this->currentSiteLanguage; |
||
1075 | } |
||
1076 | |||
1077 | /** |
||
1078 | * Override the the current site language configuration. |
||
1079 | * |
||
1080 | * This is typically not needed in production code, but it is very |
||
1081 | * helpful when displaying some kind of "preview" of the form (e.g. form editor). |
||
1082 | * |
||
1083 | * @param SiteLanguage $currentSiteLanguage |
||
1084 | */ |
||
1085 | public function setCurrentSiteLanguage(SiteLanguage $currentSiteLanguage): void |
||
1086 | { |
||
1087 | $this->currentSiteLanguage = $currentSiteLanguage; |
||
1088 | } |
||
1089 | |||
1090 | /** |
||
1091 | * Initialize the SiteLanguage object. |
||
1092 | * This is mainly used by the condition matcher. |
||
1093 | */ |
||
1094 | protected function initializeCurrentSiteLanguage(): void |
||
1127 | } |
||
1128 | } |
||
1129 | |||
1130 | /** |
||
1131 | * Reference to the current running finisher |
||
1132 | * |
||
1133 | * @return FinisherInterface|null |
||
1134 | */ |
||
1135 | public function getCurrentFinisher(): ?FinisherInterface |
||
1136 | { |
||
1137 | return $this->currentFinisher; |
||
1138 | } |
||
1139 | |||
1140 | /** |
||
1141 | * @return Resolver |
||
1142 | */ |
||
1143 | protected function getConditionResolver(): Resolver |
||
1144 | { |
||
1145 | $formValues = array_replace_recursive( |
||
1146 | $this->getFormState()->getFormValues(), |
||
1147 | $this->getRequest()->getArguments() |
||
1148 | ); |
||
1149 | $page = $this->getCurrentPage() ?? $this->getFormDefinition()->getPageByIndex(0); |
||
1150 | |||
1151 | $finisherIdentifier = ''; |
||
1152 | if ($this->getCurrentFinisher() !== null) { |
||
1153 | if (method_exists($this->getCurrentFinisher(), 'getFinisherIdentifier')) { |
||
1154 | $finisherIdentifier = $this->getCurrentFinisher()->getFinisherIdentifier(); |
||
1155 | } else { |
||
1156 | $finisherIdentifier = (new \ReflectionClass($this->getCurrentFinisher()))->getShortName(); |
||
1157 | $finisherIdentifier = preg_replace('/Finisher$/', '', $finisherIdentifier); |
||
1158 | } |
||
1159 | } |
||
1160 | |||
1161 | return GeneralUtility::makeInstance( |
||
1162 | Resolver::class, |
||
1163 | 'form', |
||
1164 | [ |
||
1165 | // some shortcuts |
||
1166 | 'formRuntime' => $this, |
||
1167 | 'formValues' => $formValues, |
||
1168 | 'stepIdentifier' => $page->getIdentifier(), |
||
1169 | 'stepType' => $page->getType(), |
||
1170 | 'finisherIdentifier' => $finisherIdentifier, |
||
1171 | ], |
||
1172 | $GLOBALS['TYPO3_REQUEST'] ?? GeneralUtility::makeInstance(ServerRequest::class) |
||
1173 | ); |
||
1174 | } |
||
1175 | |||
1176 | /** |
||
1177 | * @return FrontendUserAuthentication |
||
1178 | */ |
||
1179 | protected function getFrontendUser(): FrontendUserAuthentication |
||
1182 | } |
||
1183 | |||
1184 | /** |
||
1185 | * @return TypoScriptFrontendController|null |
||
1186 | */ |
||
1187 | protected function getTypoScriptFrontendController(): ?TypoScriptFrontendController |
||
1188 | { |
||
1189 | return $GLOBALS['TSFE'] ?? null; |
||
1190 | } |
||
1191 | } |
||
1192 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.