[SECURITY] Validate complete referring request
[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 * @var bool
76 */
77 private $securedReferrerFieldRendered = false;
78
79 /**
80 * @param \TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService
81 */
82 public function injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
83 {
84 $this->hashService = $hashService;
85 }
86
87 /**
88 * @param \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService
89 */
90 public function injectMvcPropertyMappingConfigurationService(\TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService)
91 {
92 $this->mvcPropertyMappingConfigurationService = $mvcPropertyMappingConfigurationService;
93 }
94
95 /**
96 * @param \TYPO3\CMS\Extbase\Service\ExtensionService $extensionService
97 */
98 public function injectExtensionService(\TYPO3\CMS\Extbase\Service\ExtensionService $extensionService)
99 {
100 $this->extensionService = $extensionService;
101 }
102
103 /**
104 * Initialize arguments.
105 *
106 * @return void
107 */
108 public function initializeArguments()
109 {
110 parent::initializeArguments();
111 $this->registerTagAttribute('enctype', 'string', 'MIME type with which the form is submitted');
112 $this->registerTagAttribute('method', 'string', 'Transfer type (GET or POST)');
113 $this->registerTagAttribute('name', 'string', 'Name of form');
114 $this->registerTagAttribute('onreset', 'string', 'JavaScript: On reset of the form');
115 $this->registerTagAttribute('onsubmit', 'string', 'JavaScript: On submit of the form');
116 $this->registerUniversalTagAttributes();
117 }
118
119 /**
120 * Render the form.
121 *
122 * @param string $action Target action
123 * @param array $arguments Arguments
124 * @param string $controller Target controller
125 * @param string $extensionName Target Extension Name (without "tx_" prefix and no underscores). If NULL the current extension name is used
126 * @param string $pluginName Target plugin. If empty, the current plugin name is used
127 * @param int $pageUid Target page uid
128 * @param mixed $object Object to use for the form. Use in conjunction with the "property" attribute on the sub tags
129 * @param int $pageType Target page type
130 * @param bool $noCache set this to disable caching for the target page. You should not need this.
131 * @param bool $noCacheHash set this to suppress the cHash query parameter created by TypoLink. You should not need this.
132 * @param string $section The anchor to be added to the action URI (only active if $actionUri is not set)
133 * @param string $format The requested format (e.g. ".html") of the target page (only active if $actionUri is not set)
134 * @param array $additionalParams additional action URI query parameters that won't be prefixed like $arguments (overrule $arguments) (only active if $actionUri is not set)
135 * @param bool $absolute If set, an absolute action URI is rendered (only active if $actionUri is not set)
136 * @param bool $addQueryString If set, the current query parameters will be kept in the action URI (only active if $actionUri is not set)
137 * @param array $argumentsToBeExcludedFromQueryString arguments to be removed from the action URI. Only active if $addQueryString = TRUE and $actionUri is not set
138 * @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
139 * @param string $actionUri can be used to overwrite the "action" attribute of the form tag
140 * @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
141 * @param string $hiddenFieldClassName
142 * @return string rendered form
143 */
144 public function render($action = null, array $arguments = array(), $controller = null, $extensionName = null, $pluginName = null, $pageUid = null, $object = null, $pageType = 0, $noCache = false, $noCacheHash = false, $section = '', $format = '', array $additionalParams = array(), $absolute = false, $addQueryString = false, array $argumentsToBeExcludedFromQueryString = array(), $fieldNamePrefix = null, $actionUri = null, $objectName = null, $hiddenFieldClassName = null)
145 {
146 $this->setFormActionUri();
147 if (strtolower($this->arguments['method']) === 'get') {
148 $this->tag->addAttribute('method', 'get');
149 } else {
150 $this->tag->addAttribute('method', 'post');
151 }
152 $this->addFormObjectNameToViewHelperVariableContainer();
153 $this->addFormObjectToViewHelperVariableContainer();
154 $this->addFieldNamePrefixToViewHelperVariableContainer();
155 $this->addFormFieldNamesToViewHelperVariableContainer();
156 $formContent = $this->renderChildren();
157
158 if ($this->arguments['hiddenFieldClassName'] !== null) {
159 $content = LF . '<div class="' . htmlspecialchars($this->arguments['hiddenFieldClassName']) . '">';
160 } else {
161 $content = LF . '<div>';
162 }
163
164 $content .= $this->renderHiddenIdentityField($this->arguments['object'], $this->getFormObjectName());
165 $content .= $this->renderAdditionalIdentityFields();
166 $content .= $this->renderHiddenReferrerFields();
167 $content .= $this->renderHiddenSecuredReferrerField();
168
169 // Render the trusted list of all properties after everything else has been rendered
170 $content .= $this->renderTrustedPropertiesField();
171
172 $content .= LF . '</div>' . LF;
173 $content .= $formContent;
174 $this->tag->setContent($content);
175 $this->removeFieldNamePrefixFromViewHelperVariableContainer();
176 $this->removeFormObjectFromViewHelperVariableContainer();
177 $this->removeFormObjectNameFromViewHelperVariableContainer();
178 $this->removeFormFieldNamesFromViewHelperVariableContainer();
179 $this->removeCheckboxFieldNamesFromViewHelperVariableContainer();
180 return $this->tag->render();
181 }
182
183 /**
184 * Sets the "action" attribute of the form tag
185 *
186 * @return void
187 */
188 protected function setFormActionUri()
189 {
190 if ($this->hasArgument('actionUri')) {
191 $formActionUri = $this->arguments['actionUri'];
192 } else {
193 $uriBuilder = $this->renderingContext->getControllerContext()->getUriBuilder();
194 $formActionUri = $uriBuilder->reset()->setTargetPageUid($this->arguments['pageUid'])->setTargetPageType($this->arguments['pageType'])->setNoCache($this->arguments['noCache'])->setUseCacheHash(!$this->arguments['noCacheHash'])->setSection($this->arguments['section'])->setCreateAbsoluteUri($this->arguments['absolute'])->setArguments((array)$this->arguments['additionalParams'])->setAddQueryString($this->arguments['addQueryString'])->setArgumentsToBeExcludedFromQueryString((array)$this->arguments['argumentsToBeExcludedFromQueryString'])->setFormat($this->arguments['format'])->uriFor($this->arguments['action'], $this->arguments['arguments'], $this->arguments['controller'], $this->arguments['extensionName'], $this->arguments['pluginName']);
195 $this->formActionUriArguments = $uriBuilder->getArguments();
196 }
197 $this->tag->addAttribute('action', $formActionUri);
198 }
199
200 /**
201 * Render additional identity fields which were registered by form elements.
202 * This happens if a form field is defined like property="bla.blubb" - then we might need an identity property for the sub-object "bla".
203 *
204 * @return string HTML-string for the additional identity properties
205 */
206 protected function renderAdditionalIdentityFields()
207 {
208 if ($this->viewHelperVariableContainer->exists(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'additionalIdentityProperties')) {
209 $additionalIdentityProperties = $this->viewHelperVariableContainer->get(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'additionalIdentityProperties');
210 $output = '';
211 foreach ($additionalIdentityProperties as $identity) {
212 $output .= LF . $identity;
213 }
214 return $output;
215 }
216 return '';
217 }
218
219 /**
220 * Renders hidden form fields for referrer information about
221 * the current controller and action.
222 *
223 * @return string Hidden fields with referrer information
224 * @todo filter out referrer information that is equal to the target (e.g. same packageKey)
225 */
226 protected function renderHiddenReferrerFields()
227 {
228 $request = $this->renderingContext->getControllerContext()->getRequest();
229 $extensionName = $request->getControllerExtensionName();
230 $vendorName = $request->getControllerVendorName();
231 $controllerName = $request->getControllerName();
232 $actionName = $request->getControllerActionName();
233 $result = LF;
234 $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@extension]') . '" value="' . $extensionName . '" />' . LF;
235 if ($vendorName !== null) {
236 $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@vendor]') . '" value="' . $vendorName . '" />' . LF;
237 }
238 $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@controller]') . '" value="' . $controllerName . '" />' . LF;
239 $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@action]') . '" value="' . $actionName . '" />' . LF;
240 $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[arguments]') . '" value="' . htmlspecialchars($this->hashService->appendHmac(base64_encode(serialize($request->getArguments())))) . '" />' . LF;
241 $result .= $this->renderHiddenSecuredReferrerField();
242
243 return $result;
244 }
245
246 /**
247 * Renders hidden form field for secured referrer information about the current controller and action.
248 *
249 * This method is called twice, to deal with subclasses of this class in a most compatible way
250 *
251 * @return string Hidden field with secured referrer information
252 */
253 protected function renderHiddenSecuredReferrerField()
254 {
255 if ($this->securedReferrerFieldRendered) {
256 return '';
257 }
258 $request = $this->renderingContext->getControllerContext()->getRequest();
259 $extensionName = $request->getControllerExtensionName();
260 $vendorName = $request->getControllerVendorName();
261 $controllerName = $request->getControllerName();
262 $actionName = $request->getControllerActionName();
263 $actionRequest = [
264 '@extension' => $extensionName,
265 '@controller' => $controllerName,
266 '@action' => $actionName,
267 ];
268 if ($vendorName !== null) {
269 $actionRequest['@vendor'] = $vendorName;
270 }
271 $result = '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@request]') . '" value="' . htmlspecialchars($this->hashService->appendHmac(serialize($actionRequest))) . '" />' . LF;
272 $this->securedReferrerFieldRendered = true;
273 return $result;
274 }
275
276 /**
277 * Adds the form object name to the ViewHelperVariableContainer if "objectName" argument or "name" attribute is specified.
278 *
279 * @return void
280 */
281 protected function addFormObjectNameToViewHelperVariableContainer()
282 {
283 $formObjectName = $this->getFormObjectName();
284 if ($formObjectName !== null) {
285 $this->viewHelperVariableContainer->add(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObjectName', $formObjectName);
286 }
287 }
288
289 /**
290 * Removes the form name from the ViewHelperVariableContainer.
291 *
292 * @return void
293 */
294 protected function removeFormObjectNameFromViewHelperVariableContainer()
295 {
296 $formObjectName = $this->getFormObjectName();
297 if ($formObjectName !== null) {
298 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObjectName');
299 }
300 }
301
302 /**
303 * Returns the name of the object that is bound to this form.
304 * If the "objectName" argument has been specified, this is returned. Otherwise the name attribute of this form.
305 * If neither objectName nor name arguments have been set, NULL is returned.
306 *
307 * @return string specified Form name or NULL if neither $objectName nor $name arguments have been specified
308 */
309 protected function getFormObjectName()
310 {
311 $formObjectName = null;
312 if ($this->hasArgument('objectName')) {
313 $formObjectName = $this->arguments['objectName'];
314 } elseif ($this->hasArgument('name')) {
315 $formObjectName = $this->arguments['name'];
316 }
317 return $formObjectName;
318 }
319
320 /**
321 * Adds the object that is bound to this form to the ViewHelperVariableContainer if the formObject attribute is specified.
322 *
323 * @return void
324 */
325 protected function addFormObjectToViewHelperVariableContainer()
326 {
327 if ($this->hasArgument('object')) {
328 $this->viewHelperVariableContainer->add(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObject', $this->arguments['object']);
329 $this->viewHelperVariableContainer->add(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'additionalIdentityProperties', array());
330 }
331 }
332
333 /**
334 * Removes the form object from the ViewHelperVariableContainer.
335 *
336 * @return void
337 */
338 protected function removeFormObjectFromViewHelperVariableContainer()
339 {
340 if ($this->hasArgument('object')) {
341 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObject');
342 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'additionalIdentityProperties');
343 }
344 }
345
346 /**
347 * Adds the field name prefix to the ViewHelperVariableContainer
348 *
349 * @return void
350 */
351 protected function addFieldNamePrefixToViewHelperVariableContainer()
352 {
353 $fieldNamePrefix = $this->getFieldNamePrefix();
354 $this->viewHelperVariableContainer->add(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'fieldNamePrefix', $fieldNamePrefix);
355 }
356
357 /**
358 * Get the field name prefix
359 *
360 * @return string
361 */
362 protected function getFieldNamePrefix()
363 {
364 if ($this->hasArgument('fieldNamePrefix')) {
365 return $this->arguments['fieldNamePrefix'];
366 } else {
367 return $this->getDefaultFieldNamePrefix();
368 }
369 }
370
371 /**
372 * Removes field name prefix from the ViewHelperVariableContainer
373 *
374 * @return void
375 */
376 protected function removeFieldNamePrefixFromViewHelperVariableContainer()
377 {
378 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'fieldNamePrefix');
379 }
380
381 /**
382 * Adds a container for form field names to the ViewHelperVariableContainer
383 *
384 * @return void
385 */
386 protected function addFormFieldNamesToViewHelperVariableContainer()
387 {
388 $this->viewHelperVariableContainer->add(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formFieldNames', array());
389 }
390
391 /**
392 * Removes the container for form field names from the ViewHelperVariableContainer
393 *
394 * @return void
395 */
396 protected function removeFormFieldNamesFromViewHelperVariableContainer()
397 {
398 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formFieldNames');
399 }
400
401 /**
402 * Render the request hash field
403 *
404 * @return string the hmac field
405 */
406 protected function renderRequestHashField()
407 {
408 $formFieldNames = $this->viewHelperVariableContainer->get(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formFieldNames');
409 $this->postProcessUriArgumentsForRequestHash($this->formActionUriArguments, $formFieldNames);
410 $requestHash = $this->requestHashService->generateRequestHash($formFieldNames, $this->getFieldNamePrefix());
411 // in v4, we need to prefix __hmac as well to make it show up in the request object.
412 return '<input type="hidden" name="' . $this->prefixFieldName('__hmac') . '" value="' . htmlspecialchars($requestHash) . '" />';
413 }
414
415 /**
416 * Add the URI arguments after postprocessing to the request hash as well.
417 */
418 protected function postProcessUriArgumentsForRequestHash($arguments, &$results, $currentPrefix = '', $level = 0)
419 {
420 if (count($arguments)) {
421 foreach ($arguments as $argumentName => $argumentValue) {
422 if (is_array($argumentValue)) {
423 $prefix = $level == 0 ? $argumentName : $currentPrefix . '[' . $argumentName . ']';
424 $this->postProcessUriArgumentsForRequestHash($argumentValue, $results, $prefix, $level + 1);
425 } else {
426 $results[] = $level == 0 ? $argumentName : $currentPrefix . '[' . $argumentName . ']';
427 }
428 }
429 }
430 }
431
432 /**
433 * Retrieves the default field name prefix for this form
434 *
435 * @return string default field name prefix
436 */
437 protected function getDefaultFieldNamePrefix()
438 {
439 $request = $this->renderingContext->getControllerContext()->getRequest();
440 if ($this->hasArgument('extensionName')) {
441 $extensionName = $this->arguments['extensionName'];
442 } else {
443 $extensionName = $request->getControllerExtensionName();
444 }
445 if ($this->hasArgument('pluginName')) {
446 $pluginName = $this->arguments['pluginName'];
447 } else {
448 $pluginName = $request->getPluginName();
449 }
450 if ($extensionName !== null && $pluginName != null) {
451 return $this->extensionService->getPluginNamespace($extensionName, $pluginName);
452 } else {
453 return '';
454 }
455 }
456
457 /**
458 * Remove Checkbox field names from ViewHelper variable container, to start from scratch when a new form starts.
459 */
460 protected function removeCheckboxFieldNamesFromViewHelperVariableContainer()
461 {
462 if ($this->viewHelperVariableContainer->exists(\TYPO3\CMS\Fluid\ViewHelpers\Form\CheckboxViewHelper::class, 'checkboxFieldNames')) {
463 $this->viewHelperVariableContainer->remove(\TYPO3\CMS\Fluid\ViewHelpers\Form\CheckboxViewHelper::class, 'checkboxFieldNames');
464 }
465 }
466
467 /**
468 * Render the request hash field
469 *
470 * @return string The hmac field
471 */
472 protected function renderTrustedPropertiesField()
473 {
474 $formFieldNames = $this->viewHelperVariableContainer->get(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formFieldNames');
475 $requestHash = $this->mvcPropertyMappingConfigurationService->generateTrustedPropertiesToken($formFieldNames, $this->getFieldNamePrefix());
476 return '<input type="hidden" name="' . $this->prefixFieldName('__trustedProperties') . '" value="' . htmlspecialchars($requestHash) . '" />';
477 }
478 }