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