[BUGFIX] Submitted form data has precedence over value argument
[Packages/TYPO3.CMS.git] / typo3 / sysext / fluid / Classes / ViewHelpers / Form / AbstractFormFieldViewHelper.php
1 <?php
2
3 namespace TYPO3\CMS\Fluid\ViewHelpers\Form;
4
5 /* *
6 * This script is backported from the TYPO3 Flow package "TYPO3.Fluid". *
7 * *
8 * It is free software; you can redistribute it and/or modify it under *
9 * the terms of the GNU Lesser General Public License, either version 3 *
10 * of the License, or (at your option) any later version. *
11 * *
12 * *
13 * This script is distributed in the hope that it will be useful, but *
14 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHAN- *
15 * TABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser *
16 * General Public License for more details. *
17 * *
18 * You should have received a copy of the GNU Lesser General Public *
19 * License along with the script. *
20 * If not, see http://www.gnu.org/licenses/lgpl.html *
21 * *
22 * The TYPO3 project - inspiring people to share! *
23 * */
24
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
27
28 /**
29 * Abstract Form View Helper. Bundles functionality related to direct property access of objects in other Form ViewHelpers.
30 *
31 * If you set the "property" attribute to the name of the property to resolve from the object, this class will
32 * automatically set the name and value of a form element.
33 *
34 * @api
35 */
36 abstract class AbstractFormFieldViewHelper extends AbstractFormViewHelper {
37
38 /**
39 * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
40 * @inject
41 */
42 protected $configurationManager;
43
44 /**
45 * @var boolean
46 */
47 protected $respectSubmittedDataValue = FALSE;
48
49 /**
50 * Initialize arguments.
51 *
52 * @return void
53 * @api
54 */
55 public function initializeArguments() {
56 parent::initializeArguments();
57 $this->registerArgument('name', 'string', 'Name of input tag');
58 $this->registerArgument('value', 'mixed', 'Value of input tag');
59 $this->registerArgument(
60 'property', 'string',
61 'Name of Object Property. If used in conjunction with <f:form object="...">, "name" and "value" properties will be ignored.'
62 );
63 }
64
65 /**
66 * Getting the current configuration for respectSubmittedDataValue.
67 *
68 * @return boolean
69 */
70 public function getRespectSubmittedDataValue() {
71 return $this->respectSubmittedDataValue;
72 }
73
74 /**
75 * Define respectSubmittedDataValue to enable or disable the usage of the submitted values in the viewhelper.
76 *
77 * @param boolean $respectSubmittedDataValue
78 * @return void
79 */
80 public function setRespectSubmittedDataValue($respectSubmittedDataValue) {
81 $this->respectSubmittedDataValue = $respectSubmittedDataValue;
82 }
83
84 /**
85 * Get the name of this form element.
86 * Either returns arguments['name'], or the correct name for Object Access.
87 *
88 * In case property is something like bla.blubb (hierarchical), then [bla][blubb] is generated.
89 *
90 * @return string Name
91 */
92 protected function getName() {
93 $name = $this->getNameWithoutPrefix();
94 return $this->prefixFieldName($name);
95 }
96
97 /**
98 * Shortcut for retrieving the request from the controller context
99 *
100 * @return \TYPO3\CMS\Extbase\Mvc\Request
101 */
102 protected function getRequest() {
103 return $this->controllerContext->getRequest();
104 }
105
106 /**
107 * Get the name of this form element, without prefix.
108 *
109 * @return string name
110 */
111 protected function getNameWithoutPrefix() {
112 if ($this->isObjectAccessorMode()) {
113 $formObjectName = $this->viewHelperVariableContainer->get(
114 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObjectName'
115 );
116 if (!empty($formObjectName)) {
117 $propertySegments = explode('.', $this->arguments['property']);
118 $propertyPath = '';
119 foreach ($propertySegments as $segment) {
120 $propertyPath .= '[' . $segment . ']';
121 }
122 $name = $formObjectName . $propertyPath;
123 } else {
124 $name = $this->arguments['property'];
125 }
126 } else {
127 $name = $this->arguments['name'];
128 }
129 if ($this->hasArgument('value') && is_object($this->arguments['value'])) {
130 // @todo Use $this->persistenceManager->isNewObject() once it is implemented
131 if (NULL !== $this->persistenceManager->getIdentifierByObject($this->arguments['value'])) {
132 $name .= '[__identity]';
133 }
134 }
135 return $name;
136 }
137
138 /**
139 * Get the value of this form element.
140 * Either returns arguments['value'], or the correct value for Object Access.
141 *
142 * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8
143 * @param bool $convertObjects whether or not to convert objects to identifiers
144 * @return mixed Value
145 */
146 protected function getValue($convertObjects = TRUE) {
147 $value = NULL;
148 GeneralUtility::logDeprecatedFunction();
149
150 if ($this->hasArgument('value')) {
151 $value = $this->arguments['value'];
152 } elseif ($this->isObjectAccessorMode()) {
153 if ($this->hasMappingErrorOccurred()) {
154 $value = $this->getLastSubmittedFormData();
155 } else {
156 $value = $this->getPropertyValue();
157 }
158 $this->addAdditionalIdentityPropertiesIfNeeded();
159 }
160
161 if ($convertObjects) {
162 $value = $this->convertToPlainValue($value);
163 }
164 return $value;
165 }
166
167 /**
168 * Returns the current value of this Form ViewHelper and converts it to an identifier string in case it's an object
169 * The value is determined as follows:
170 * * If property mapping errors occurred and the form is re-displayed, the *last submitted* value is returned
171 * * Else the bound property is returned (only in objectAccessor-mode)
172 * * As fallback the "value" argument of this ViewHelper is used
173 *
174 * Note: This method should *not* be used for form elements that must not change the value attribute, e.g. (radio) buttons and checkboxes.
175 *
176 * @return mixed Value
177 */
178 protected function getValueAttribute() {
179 $value = NULL;
180
181 if ($this->respectSubmittedDataValue) {
182 $value = $this->getValueFromSubmittedFormData($value);
183 } elseif ($this->hasArgument('value')) {
184 $value = $this->arguments['value'];
185 }
186
187 if (is_object($value)) {
188 $value = $this->persistenceManager->getIdentifierByObject($value);
189 }
190 return $value;
191 }
192
193 /**
194 * If property mapping errors occurred and the form is re-displayed, the *last submitted* value is returned by this
195 * method.
196 *
197 * Note:
198 * This method should *not* be used for form elements that must not change the value attribute, e.g. (radio)
199 * buttons and checkboxes. The default behaviour is not to use this method. You need to set
200 * respectSubmittedDataValue to TRUE to enable the form data handling for the viewhelper.
201 *
202 * @param mixed $value
203 * @return mixed Value
204 */
205 protected function getValueFromSubmittedFormData($value) {
206 $submittedFormData = NULL;
207 if ($this->hasMappingErrorOccurred()) {
208 $submittedFormData = $this->getLastSubmittedFormData();
209 }
210 if ($submittedFormData !== NULL) {
211 $value = $submittedFormData;
212 } elseif ($this->hasArgument('value')) {
213 $value = $this->arguments['value'];
214 } elseif ($this->isObjectAccessorMode()) {
215 $value = $this->getPropertyValue();
216 }
217
218 return $value;
219 }
220
221 /**
222 * Converts an arbitrary value to a plain value
223 *
224 * @param mixed $value The value to convert
225 * @return mixed
226 */
227 protected function convertToPlainValue($value) {
228 if (is_object($value)) {
229 $identifier = $this->persistenceManager->getIdentifierByObject($value);
230 if ($identifier !== NULL) {
231 $value = $identifier;
232 }
233 }
234 return $value;
235 }
236
237 /**
238 * Checks if a property mapping error has occurred in the last request.
239 *
240 * @return bool TRUE if a mapping error occurred, FALSE otherwise
241 */
242 protected function hasMappingErrorOccurred() {
243 return $this->getRequest()->getOriginalRequest() !== NULL;
244 }
245
246 /**
247 * Get the form data which has last been submitted; only returns valid data in case
248 * a property mapping error has occurred. Check with hasMappingErrorOccurred() before!
249 *
250 * @return mixed
251 */
252 protected function getLastSubmittedFormData() {
253 $propertyPath = rtrim(preg_replace('/(\\]\\[|\\[|\\])/', '.', $this->getNameWithoutPrefix()), '.');
254 $value = ObjectAccess::getPropertyPath(
255 $this->controllerContext->getRequest()->getOriginalRequest()->getArguments(), $propertyPath
256 );
257 return $value;
258 }
259
260 /**
261 * Add additional identity properties in case the current property is hierarchical (of the form "bla.blubb").
262 * Then, [bla][__identity] has to be generated as well.
263 *
264 * @return void
265 */
266 protected function addAdditionalIdentityPropertiesIfNeeded() {
267 if (!$this->isObjectAccessorMode()) {
268 return;
269 }
270
271 if (!$this->viewHelperVariableContainer->exists(
272 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObject'
273 )
274 ) {
275 return;
276 }
277 $propertySegments = explode('.', $this->arguments['property']);
278 // hierarchical property. If there is no "." inside (thus $propertySegments == 1), we do not need to do anything
279 if (count($propertySegments) < 2) {
280 return;
281 }
282 $formObject = $this->viewHelperVariableContainer->get(
283 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObject'
284 );
285 $objectName = $this->viewHelperVariableContainer->get(
286 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObjectName'
287 );
288 // If count == 2 -> we need to go through the for-loop exactly once
289 for ($i = 1; $i < count($propertySegments); $i++) {
290 $object = ObjectAccess::getPropertyPath($formObject, implode('.', array_slice($propertySegments, 0, $i)));
291 $objectName .= '[' . $propertySegments[($i - 1)] . ']';
292 $hiddenIdentityField = $this->renderHiddenIdentityField($object, $objectName);
293 // Add the hidden identity field to the ViewHelperVariableContainer
294 $additionalIdentityProperties = $this->viewHelperVariableContainer->get(
295 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'additionalIdentityProperties'
296 );
297 $additionalIdentityProperties[$objectName] = $hiddenIdentityField;
298 $this->viewHelperVariableContainer->addOrUpdate(
299 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'additionalIdentityProperties',
300 $additionalIdentityProperties
301 );
302 }
303 }
304
305 /**
306 * Get the current property of the object bound to this form.
307 *
308 * @return mixed Value
309 */
310 protected function getPropertyValue() {
311 if (!$this->viewHelperVariableContainer->exists(
312 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObject'
313 )
314 ) {
315 return NULL;
316 }
317 $formObject = $this->viewHelperVariableContainer->get(
318 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObject'
319 );
320 $propertyName = $this->arguments['property'];
321 if (is_array($formObject)) {
322 return isset($formObject[$propertyName]) ? $formObject[$propertyName] : NULL;
323 }
324 return ObjectAccess::getPropertyPath($formObject, $propertyName);
325 }
326
327 /**
328 * Internal method which checks if we should evaluate a domain object or just output arguments['name'] and arguments['value']
329 *
330 * @return bool TRUE if we should evaluate the domain object, FALSE otherwise.
331 */
332 protected function isObjectAccessorMode() {
333 return $this->hasArgument('property') && $this->viewHelperVariableContainer->exists(
334 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObjectName'
335 );
336 }
337
338 /**
339 * Add an CSS class if this view helper has errors
340 *
341 * @return void
342 */
343 protected function setErrorClassAttribute() {
344 if ($this->hasArgument('class')) {
345 $cssClass = $this->arguments['class'] . ' ';
346 } else {
347 $cssClass = '';
348 }
349
350 $mappingResultsForProperty = $this->getMappingResultsForProperty();
351 if ($mappingResultsForProperty->hasErrors()) {
352 if ($this->hasArgument('errorClass')) {
353 $cssClass .= $this->arguments['errorClass'];
354 } else {
355 $cssClass .= 'error';
356 }
357 $this->tag->addAttribute('class', $cssClass);
358 }
359 }
360
361 /**
362 * Get errors for the property and form name of this view helper
363 *
364 * @return \TYPO3\CMS\Extbase\Error\Result Array of errors
365 */
366 protected function getMappingResultsForProperty() {
367 if (!$this->isObjectAccessorMode()) {
368 return new \TYPO3\CMS\Extbase\Error\Result();
369 }
370 $originalRequestMappingResults = $this->getRequest()->getOriginalRequestMappingResults();
371 $formObjectName = $this->viewHelperVariableContainer->get(
372 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formObjectName'
373 );
374 return $originalRequestMappingResults->forProperty($formObjectName)->forProperty($this->arguments['property']);
375 }
376
377 /**
378 * Renders a hidden field with the same name as the element, to make sure the empty value is submitted
379 * in case nothing is selected. This is needed for checkbox and multiple select fields
380 *
381 * @return string the hidden field.
382 */
383 protected function renderHiddenFieldForEmptyValue() {
384 $hiddenFieldNames = [];
385 if ($this->viewHelperVariableContainer->exists(
386 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'renderedHiddenFields'
387 )
388 ) {
389 $hiddenFieldNames = $this->viewHelperVariableContainer->get(
390 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'renderedHiddenFields'
391 );
392 }
393 $fieldName = $this->getName();
394 if (substr($fieldName, -2) === '[]') {
395 $fieldName = substr($fieldName, 0, -2);
396 }
397 if (!in_array($fieldName, $hiddenFieldNames)) {
398 $hiddenFieldNames[] = $fieldName;
399 $this->viewHelperVariableContainer->addOrUpdate(
400 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'renderedHiddenFields', $hiddenFieldNames
401 );
402 return '<input type="hidden" name="' . htmlspecialchars($fieldName) . '" value="" />';
403 }
404 return '';
405 }
406
407 }