[BUGFIX] Reset FormViewHelper on execution
[Packages/TYPO3.CMS.git] / typo3 / sysext / fluid / Classes / ViewHelpers / FormViewHelper.php
1 <?php
2 namespace TYPO3\CMS\Fluid\ViewHelpers;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 /**
18 * Form view helper. Generates a <form> Tag.
19 *
20 * = Basic usage =
21 *
22 * Use <f:form> to output an HTML <form> tag which is targeted at the specified action, in the current controller and package.
23 * It will submit the form data via a POST request. If you want to change this, use method="get" as an argument.
24 * <code title="Example">
25 * <f:form action="...">...</f:form>
26 * </code>
27 *
28 * = A complex form with a specified encoding type =
29 *
30 * <code title="Form with enctype set">
31 * <f:form action=".." controller="..." package="..." enctype="multipart/form-data">...</f:form>
32 * </code>
33 *
34 * = A Form which should render a domain object =
35 *
36 * <code title="Binding a domain object to a form">
37 * <f:form action="..." name="customer" object="{customer}">
38 * <f:form.hidden property="id" />
39 * <f:form.textbox property="name" />
40 * </f:form>
41 * </code>
42 * This automatically inserts the value of {customer.name} inside the textbox and adjusts the name of the textbox accordingly.
43 */
44 class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormViewHelper
45 {
46 /**
47 * @var string
48 */
49 protected $tagName = 'form';
50
51 /**
52 * @var \TYPO3\CMS\Extbase\Security\Cryptography\HashService
53 */
54 protected $hashService;
55
56 /**
57 * @var \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService
58 */
59 protected $mvcPropertyMappingConfigurationService;
60
61 /**
62 * @var \TYPO3\CMS\Extbase\Service\ExtensionService
63 */
64 protected $extensionService;
65
66 /**
67 * We need the arguments of the formActionUri on requesthash calculation
68 * therefore we will store them in here right after calling uriBuilder
69 *
70 * @var array
71 */
72 protected $formActionUriArguments;
73
74 /**
75 * @param \TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService
76 */
77 public function injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
78 {
79 $this->hashService = $hashService;
80 }
81
82 /**
83 * @param \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService
84 */
85 public function injectMvcPropertyMappingConfigurationService(\TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService)
86 {
87 $this->mvcPropertyMappingConfigurationService = $mvcPropertyMappingConfigurationService;
88 }
89
90 /**
91 * @param \TYPO3\CMS\Extbase\Service\ExtensionService $extensionService
92 */
93 public function injectExtensionService(\TYPO3\CMS\Extbase\Service\ExtensionService $extensionService)
94 {
95 $this->extensionService = $extensionService;
96 }
97
98 /**
99 * Initialize arguments.
100 */
101 public function initializeArguments()
102 {
103 parent::initializeArguments();
104 $this->registerArgument('action', 'string', 'Target action');
105 $this->registerArgument('arguments', 'array', 'Arguments', false, []);
106 $this->registerArgument('controller', 'string', 'Target controller');
107 $this->registerArgument('extensionName', 'string', 'Target Extension Name (without "tx_" prefix and no underscores). If NULL the current extension name is used');
108 $this->registerArgument('pluginName', 'string', 'Target plugin. If empty, the current plugin name is used');
109 $this->registerArgument('pageUid', 'int', 'Target page uid');
110 $this->registerArgument('object', 'mixed', 'Object to use for the form. Use in conjunction with the "property" attribute on the sub tags');
111 $this->registerArgument('pageType', 'int', 'Target page type', false, 0);
112 $this->registerArgument('noCache', 'bool', 'set this to disable caching for the target page. You should not need this.', false, false);
113 $this->registerArgument('noCacheHash', 'bool', 'set this to suppress the cHash query parameter created by TypoLink. You should not need this.', false, false);
114 $this->registerArgument('section', 'string', 'The anchor to be added to the action URI (only active if $actionUri is not set)', false, '');
115 $this->registerArgument('format', 'string', 'The requested format (e.g. ".html") of the target page (only active if $actionUri is not set)', false, '');
116 $this->registerArgument('additionalParams', 'array', 'additional action URI query parameters that won\'t be prefixed like $arguments (overrule $arguments) (only active if $actionUri is not set)', false, []);
117 $this->registerArgument('absolute', 'bool', 'If set, an absolute action URI is rendered (only active if $actionUri is not set)', false, false);
118 $this->registerArgument('addQueryString', 'bool', 'If set, the current query parameters will be kept in the action URI (only active if $actionUri is not set)', false, false);
119 $this->registerArgument('argumentsToBeExcludedFromQueryString', 'array', 'arguments to be removed from the action URI. Only active if $addQueryString = TRUE and $actionUri is not set', false, []);
120 $this->registerArgument('addQueryStringMethod', 'string', 'Method to use when keeping query parameters (GET or POST, only active if $actionUri is not set', false, 'GET');
121 $this->registerArgument('fieldNamePrefix', 'string', 'Prefix that will be added to all field names within this form. If not set the prefix will be tx_yourExtension_plugin');
122 $this->registerArgument('actionUri', 'string', 'can be used to overwrite the "action" attribute of the form tag');
123 $this->registerArgument('objectName', 'string', 'name of the object that is bound to this form. If this argument is not specified, the name attribute of this form is used to determine the FormObjectName');
124 $this->registerArgument('hiddenFieldClassName', 'string', 'hiddenFieldClassName');
125 $this->registerTagAttribute('enctype', 'string', 'MIME type with which the form is submitted');
126 $this->registerTagAttribute('method', 'string', 'Transfer type (GET or POST)');
127 $this->registerTagAttribute('name', 'string', 'Name of form');
128 $this->registerTagAttribute('onreset', 'string', 'JavaScript: On reset of the form');
129 $this->registerTagAttribute('onsubmit', 'string', 'JavaScript: On submit of the form');
130 $this->registerUniversalTagAttributes();
131 }
132
133 /**
134 * Render the form.
135 *
136 * @return string rendered form
137 */
138 public function render()
139 {
140 $this->setFormActionUri();
141 if (strtolower($this->arguments['method']) === 'get') {
142 $this->tag->addAttribute('method', 'get');
143 } else {
144 $this->tag->addAttribute('method', 'post');
145 }
146 $this->addFormObjectNameToViewHelperVariableContainer();
147 $this->addFormObjectToViewHelperVariableContainer();
148 $this->addFieldNamePrefixToViewHelperVariableContainer();
149 $this->addFormFieldNamesToViewHelperVariableContainer();
150 $formContent = $this->renderChildren();
151
152 if ($this->arguments['hiddenFieldClassName'] !== null) {
153 $content = LF . '<div class="' . htmlspecialchars($this->arguments['hiddenFieldClassName']) . '">';
154 } else {
155 $content = LF . '<div>';
156 }
157
158 $content .= $this->renderHiddenIdentityField($this->arguments['object'], $this->getFormObjectName());
159 $content .= $this->renderAdditionalIdentityFields();
160 $content .= $this->renderHiddenReferrerFields();
161 $content .= $this->renderHiddenSecuredReferrerField();
162
163 // Render the trusted list of all properties after everything else has been rendered
164 $content .= $this->renderTrustedPropertiesField();
165
166 $content .= LF . '</div>' . LF;
167 $content .= $formContent;
168 $this->tag->setContent($content);
169 $this->removeFieldNamePrefixFromViewHelperVariableContainer();
170 $this->removeFormObjectFromViewHelperVariableContainer();
171 $this->removeFormObjectNameFromViewHelperVariableContainer();
172 $this->removeFormFieldNamesFromViewHelperVariableContainer();
173 $this->removeCheckboxFieldNamesFromViewHelperVariableContainer();
174 $this->removeSecuredHiddenFieldsRenderedFromViewHelperVariableContainer();
175 return $this->tag->render();
176 }
177
178 /**
179 * Sets the "action" attribute of the form tag
180 */
181 protected function setFormActionUri()
182 {
183 if ($this->hasArgument('actionUri')) {
184 $formActionUri = $this->arguments['actionUri'];
185 } else {
186 $pageUid = (int)$this->arguments['pageUid'] > 0 ? (int)$this->arguments['pageUid'] : null;
187 $uriBuilder = $this->renderingContext->getControllerContext()->getUriBuilder();
188 $formActionUri = $uriBuilder
189 ->reset()
190 ->setTargetPageUid($pageUid)
191 ->setTargetPageType($this->arguments['pageType'])
192 ->setNoCache($this->arguments['noCache'])
193 ->setUseCacheHash(!$this->arguments['noCacheHash'])
194 ->setSection($this->arguments['section'])
195 ->setCreateAbsoluteUri($this->arguments['absolute'])
196 ->setArguments((array)$this->arguments['additionalParams'])
197 ->setAddQueryString($this->arguments['addQueryString'])
198 ->setAddQueryStringMethod($this->arguments['addQueryStringMethod'])
199 ->setArgumentsToBeExcludedFromQueryString((array)$this->arguments['argumentsToBeExcludedFromQueryString'])
200 ->setFormat($this->arguments['format'])
201 ->uriFor(
202 $this->arguments['action'],
203 $this->arguments['arguments'],
204 $this->arguments['controller'],
205 $this->arguments['extensionName'],
206 $this->arguments['pluginName']
207 );
208 $this->formActionUriArguments = $uriBuilder->getArguments();
209 }
210 $this->tag->addAttribute('action', $formActionUri);
211 }
212
213 /**
214 * Render additional identity fields which were registered by form elements.
215 * This happens if a form field is defined like property="bla.blubb" - then we might need an identity property for the sub-object "bla".
216 *
217 * @return string HTML-string for the additional identity properties
218 */
219 protected function renderAdditionalIdentityFields()
220 {
221 if ($this->viewHelperVariableContainer->exists(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'additionalIdentityProperties')) {
222 $additionalIdentityProperties = $this->viewHelperVariableContainer->get(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'additionalIdentityProperties');
223 $output = '';
224 foreach ($additionalIdentityProperties as $identity) {
225 $output .= LF . $identity;
226 }
227 return $output;
228 }
229 return '';
230 }
231
232 /**
233 * Renders hidden form fields for referrer information about
234 * the current controller and action.
235 *
236 * @return string Hidden fields with referrer information
237 * @todo filter out referrer information that is equal to the target (e.g. same packageKey)
238 */
239 protected function renderHiddenReferrerFields()
240 {
241 $request = $this->renderingContext->getControllerContext()->getRequest();
242 $extensionName = $request->getControllerExtensionName();
243 $vendorName = $request->getControllerVendorName();
244 $controllerName = $request->getControllerName();
245 $actionName = $request->getControllerActionName();
246 $result = LF;
247 $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@extension]') . '" value="' . $extensionName . '" />' . LF;
248 if ($vendorName !== null) {
249 $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@vendor]') . '" value="' . $vendorName . '" />' . LF;
250 }
251 $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@controller]') . '" value="' . $controllerName . '" />' . LF;
252 $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@action]') . '" value="' . $actionName . '" />' . LF;
253 $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[arguments]') . '" value="' . htmlspecialchars($this->hashService->appendHmac(base64_encode(serialize($request->getArguments())))) . '" />' . LF;
254 $result .= $this->renderHiddenSecuredReferrerField();
255
256 return $result;
257 }
258
259 /**
260 * Renders hidden form field for secured referrer information about the current controller and action.
261 *
262 * This method is called twice, to deal with subclasses of this class in a most compatible way
263 *
264 * @return string Hidden field with secured referrer information
265 */
266 protected function renderHiddenSecuredReferrerField()
267 {
268 if ($this->hasSecuredHiddenFieldsRendered()) {
269 return '';
270 }
271 $request = $this->renderingContext->getControllerContext()->getRequest();
272 $extensionName = $request->getControllerExtensionName();
273 $vendorName = $request->getControllerVendorName();
274 $controllerName = $request->getControllerName();
275 $actionName = $request->getControllerActionName();
276 $actionRequest = [
277 '@extension' => $extensionName,
278 '@controller' => $controllerName,
279 '@action' => $actionName,
280 ];
281 if ($vendorName !== null) {
282 $actionRequest['@vendor'] = $vendorName;
283 }
284 $result = '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@request]') . '" value="' . htmlspecialchars($this->hashService->appendHmac(serialize($actionRequest))) . '" />' . LF;
285 $this->addSecuredHiddenFieldsRenderedToViewHelperVariableContainer();
286 return $result;
287 }
288
289 /**
290 * Adds the form object name to the ViewHelperVariableContainer if "objectName" argument or "name" attribute is specified.
291 */
292 protected function addFormObjectNameToViewHelperVariableContainer()
293 {
294 $formObjectName = $this->getFormObjectName();
295 if ($formObjectName !== null) {
296 $this->viewHelperVariableContainer->add(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObjectName', $formObjectName);
297 }
298 }
299
300 /**
301 * Removes the form name from the ViewHelperVariableContainer.
302 */
303 protected function removeFormObjectNameFromViewHelperVariableContainer()
304 {
305 $formObjectName = $this->getFormObjectName();
306 if ($formObjectName !== null) {
307 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObjectName');
308 }
309 }
310
311 /**
312 * Returns the name of the object that is bound to this form.
313 * If the "objectName" argument has been specified, this is returned. Otherwise the name attribute of this form.
314 * If neither objectName nor name arguments have been set, NULL is returned.
315 *
316 * @return string specified Form name or NULL if neither $objectName nor $name arguments have been specified
317 */
318 protected function getFormObjectName()
319 {
320 $formObjectName = null;
321 if ($this->hasArgument('objectName')) {
322 $formObjectName = $this->arguments['objectName'];
323 } elseif ($this->hasArgument('name')) {
324 $formObjectName = $this->arguments['name'];
325 }
326 return $formObjectName;
327 }
328
329 /**
330 * Adds the object that is bound to this form to the ViewHelperVariableContainer if the formObject attribute is specified.
331 */
332 protected function addFormObjectToViewHelperVariableContainer()
333 {
334 if ($this->hasArgument('object')) {
335 $this->viewHelperVariableContainer->add(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObject', $this->arguments['object']);
336 $this->viewHelperVariableContainer->add(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'additionalIdentityProperties', []);
337 }
338 }
339
340 /**
341 * Removes the form object from the ViewHelperVariableContainer.
342 */
343 protected function removeFormObjectFromViewHelperVariableContainer()
344 {
345 if ($this->hasArgument('object')) {
346 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObject');
347 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'additionalIdentityProperties');
348 }
349 }
350
351 /**
352 * Adds the field name prefix to the ViewHelperVariableContainer
353 */
354 protected function addFieldNamePrefixToViewHelperVariableContainer()
355 {
356 $fieldNamePrefix = $this->getFieldNamePrefix();
357 $this->viewHelperVariableContainer->add(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'fieldNamePrefix', $fieldNamePrefix);
358 }
359
360 /**
361 * Get the field name prefix
362 *
363 * @return string
364 */
365 protected function getFieldNamePrefix()
366 {
367 if ($this->hasArgument('fieldNamePrefix')) {
368 return $this->arguments['fieldNamePrefix'];
369 } else {
370 return $this->getDefaultFieldNamePrefix();
371 }
372 }
373
374 /**
375 * Removes field name prefix from the ViewHelperVariableContainer
376 */
377 protected function removeFieldNamePrefixFromViewHelperVariableContainer()
378 {
379 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'fieldNamePrefix');
380 }
381
382 /**
383 * Adds a container for form field names to the ViewHelperVariableContainer
384 */
385 protected function addFormFieldNamesToViewHelperVariableContainer()
386 {
387 $this->viewHelperVariableContainer->add(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formFieldNames', []);
388 }
389
390 /**
391 * Removes the container for form field names from the ViewHelperVariableContainer
392 */
393 protected function removeFormFieldNamesFromViewHelperVariableContainer()
394 {
395 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formFieldNames');
396 }
397
398 /**
399 * Adds flag to indicate the secured hidden fields have been rendered to the ViewHelperVariableContainer
400 */
401 protected function addSecuredHiddenFieldsRenderedToViewHelperVariableContainer()
402 {
403 $this->viewHelperVariableContainer->add(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'securedHiddenFieldsRendered', true);
404 }
405
406 /**
407 * Checks whether the secured hidden fields have been rendered
408 *
409 * @return bool
410 */
411 protected function hasSecuredHiddenFieldsRendered()
412 {
413 return $this->viewHelperVariableContainer->exists(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'securedHiddenFieldsRendered');
414 }
415
416 /**
417 * Removes flag to indicate the secured hidden fields have been rendered from the ViewHelperVariableContainer
418 */
419 protected function removeSecuredHiddenFieldsRenderedFromViewHelperVariableContainer()
420 {
421 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'securedHiddenFieldsRendered');
422 }
423
424 /**
425 * Render the request hash field
426 *
427 * @return string the hmac field
428 */
429 protected function renderRequestHashField()
430 {
431 $formFieldNames = $this->viewHelperVariableContainer->get(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formFieldNames');
432 $this->postProcessUriArgumentsForRequestHash($this->formActionUriArguments, $formFieldNames);
433 $requestHash = $this->requestHashService->generateRequestHash($formFieldNames, $this->getFieldNamePrefix());
434 // in v4, we need to prefix __hmac as well to make it show up in the request object.
435 return '<input type="hidden" name="' . $this->prefixFieldName('__hmac') . '" value="' . htmlspecialchars($requestHash) . '" />';
436 }
437
438 /**
439 * Add the URI arguments after postprocessing to the request hash as well.
440 */
441 protected function postProcessUriArgumentsForRequestHash($arguments, &$results, $currentPrefix = '', $level = 0)
442 {
443 if (count($arguments)) {
444 foreach ($arguments as $argumentName => $argumentValue) {
445 if (is_array($argumentValue)) {
446 $prefix = $level == 0 ? $argumentName : $currentPrefix . '[' . $argumentName . ']';
447 $this->postProcessUriArgumentsForRequestHash($argumentValue, $results, $prefix, $level + 1);
448 } else {
449 $results[] = $level == 0 ? $argumentName : $currentPrefix . '[' . $argumentName . ']';
450 }
451 }
452 }
453 }
454
455 /**
456 * Retrieves the default field name prefix for this form
457 *
458 * @return string default field name prefix
459 */
460 protected function getDefaultFieldNamePrefix()
461 {
462 $request = $this->renderingContext->getControllerContext()->getRequest();
463 if ($this->hasArgument('extensionName')) {
464 $extensionName = $this->arguments['extensionName'];
465 } else {
466 $extensionName = $request->getControllerExtensionName();
467 }
468 if ($this->hasArgument('pluginName')) {
469 $pluginName = $this->arguments['pluginName'];
470 } else {
471 $pluginName = $request->getPluginName();
472 }
473 if ($extensionName !== null && $pluginName != null) {
474 return $this->extensionService->getPluginNamespace($extensionName, $pluginName);
475 } else {
476 return '';
477 }
478 }
479
480 /**
481 * Remove Checkbox field names from ViewHelper variable container, to start from scratch when a new form starts.
482 */
483 protected function removeCheckboxFieldNamesFromViewHelperVariableContainer()
484 {
485 if ($this->viewHelperVariableContainer->exists(\TYPO3\CMS\Fluid\ViewHelpers\Form\CheckboxViewHelper::class, 'checkboxFieldNames')) {
486 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\Form\CheckboxViewHelper::class, 'checkboxFieldNames');
487 }
488 }
489
490 /**
491 * Render the request hash field
492 *
493 * @return string The hmac field
494 */
495 protected function renderTrustedPropertiesField()
496 {
497 $formFieldNames = $this->viewHelperVariableContainer->get(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formFieldNames');
498 $requestHash = $this->mvcPropertyMappingConfigurationService->generateTrustedPropertiesToken($formFieldNames, $this->getFieldNamePrefix());
499 return '<input type="hidden" name="' . $this->prefixFieldName('__trustedProperties') . '" value="' . htmlspecialchars($requestHash) . '" />';
500 }
501 }