[BUGFIX] FormEngine exception and warning creating new record
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Container / AbstractContainer.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\Container;
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 use TYPO3\CMS\Core\Utility\GeneralUtility;
18 use TYPO3\CMS\Backend\Form\AbstractNode;
19 use TYPO3\CMS\Backend\Form\ElementConditionMatcher;
20 use TYPO3\CMS\Backend\Utility\IconUtility;
21 use TYPO3\CMS\Backend\Utility\BackendUtility;
22 use TYPO3\CMS\Backend\Template\DocumentTemplate;
23 use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
24 use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
25 use TYPO3\CMS\Core\Utility\MathUtility;
26
27 /**
28 * Abstract container has various methods used by the container classes
29 */
30 abstract class AbstractContainer extends AbstractNode {
31
32 /**
33 * Array where records in the default language are stored. (processed by transferdata)
34 *
35 * @var array
36 */
37 protected $defaultLanguageData = array();
38
39 /**
40 * Array where records in the default language are stored (raw without any processing. used for making diff).
41 * This is the unserialized content of configured TCA ['ctrl']['transOrigDiffSourceField'] field, typically l18n_diffsource
42 *
43 * @var array
44 */
45 protected $defaultLanguageDataDiff = array();
46
47 /**
48 * Contains row data of "additional" language overlays
49 * array(
50 * $table:$uid => array(
51 * $additionalPreviewLanguageUid => $rowData
52 * )
53 * )
54 *
55 * @var array
56 */
57 protected $additionalPreviewLanguageData = array();
58
59 /**
60 * Calculate and return the current type value of a record
61 *
62 * @param string $table The table name. MUST be in $GLOBALS['TCA']
63 * @param array $row The row from the table, should contain at least the "type" field, if applicable.
64 * @return string Return the "type" value for this record, ready to pick a "types" configuration from the $GLOBALS['TCA'] array.
65 * @throws \RuntimeException
66 */
67 protected function getRecordTypeValue($table, array $row) {
68 $typeNum = 0;
69 $field = $GLOBALS['TCA'][$table]['ctrl']['type'];
70 if ($field) {
71 if (strpos($field, ':') !== FALSE) {
72 list($pointerField, $foreignTypeField) = explode(':', $field);
73 $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$pointerField]['config'];
74 $relationType = $fieldConfig['type'];
75 if ($relationType === 'select') {
76 $foreignUid = $row[$pointerField];
77 $foreignTable = $fieldConfig['foreign_table'];
78 } elseif ($relationType === 'group') {
79 $values = FormEngineUtility::extractValuesOnlyFromValueLabelList($row[$pointerField]);
80 list(, $foreignUid) = GeneralUtility::revExplode('_', $values[0], 2);
81 $allowedTables = explode(',', $fieldConfig['allowed']);
82 // Always take the first configured table.
83 $foreignTable = $allowedTables[0];
84 } else {
85 throw new \RuntimeException('TCA Foreign field pointer fields are only allowed to be used with group or select field types.', 1325861239);
86 }
87 if ($foreignUid) {
88 $foreignRow = BackendUtility::getRecord($foreignTable, $foreignUid, $foreignTypeField);
89 $this->registerDefaultLanguageData($foreignTable, $foreignRow);
90 if ($foreignRow[$foreignTypeField]) {
91 $foreignTypeFieldConfig = $GLOBALS['TCA'][$table]['columns'][$field];
92 $typeNum = $this->overrideTypeWithValueFromDefaultLanguageRecord($foreignTable, $foreignRow, $foreignTypeField, $foreignTypeFieldConfig);
93 }
94 }
95 } else {
96 $typeFieldConfig = $GLOBALS['TCA'][$table]['columns'][$field];
97 $typeNum = $this->overrideTypeWithValueFromDefaultLanguageRecord($table, $row, $field, $typeFieldConfig);
98 }
99 }
100 if (empty($typeNum)) {
101 // If that value is an empty string, set it to "0" (zero)
102 $typeNum = 0;
103 }
104 // If current typeNum doesn't exist, set it to 0 (or to 1 for historical reasons, if 0 doesn't exist)
105 if (!$GLOBALS['TCA'][$table]['types'][$typeNum]) {
106 $typeNum = $GLOBALS['TCA'][$table]['types']['0'] ? 0 : 1;
107 }
108 // Force to string. Necessary for eg '-1' to be recognized as a type value.
109 return (string)$typeNum;
110 }
111
112 /**
113 * Producing an array of field names NOT to display in the form,
114 * based on settings from subtype_value_field, bitmask_excludelist_bits etc.
115 * Notice, this list is in NO way related to the "excludeField" flag
116 *
117 * @param string $table Table name, MUST be in $GLOBALS['TCA']
118 * @param array $row A record from table.
119 * @param string $typeNum A "type" pointer value, probably the one calculated based on the record array.
120 * @return array Array with field names as values. The field names are those which should NOT be displayed "anyways
121 */
122 protected function getExcludeElements($table, $row, $typeNum) {
123 $excludeElements = array();
124 // If a subtype field is defined for the type
125 if ($GLOBALS['TCA'][$table]['types'][$typeNum]['subtype_value_field']) {
126 $subTypeField = $GLOBALS['TCA'][$table]['types'][$typeNum]['subtype_value_field'];
127 if (trim($GLOBALS['TCA'][$table]['types'][$typeNum]['subtypes_excludelist'][$row[$subTypeField]])) {
128 $excludeElements = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['types'][$typeNum]['subtypes_excludelist'][$row[$subTypeField]], TRUE);
129 }
130 }
131 // If a bitmask-value field has been configured, then find possible fields to exclude based on that:
132 if ($GLOBALS['TCA'][$table]['types'][$typeNum]['bitmask_value_field']) {
133 $subTypeField = $GLOBALS['TCA'][$table]['types'][$typeNum]['bitmask_value_field'];
134 $sTValue = MathUtility::forceIntegerInRange($row[$subTypeField], 0);
135 if (is_array($GLOBALS['TCA'][$table]['types'][$typeNum]['bitmask_excludelist_bits'])) {
136 foreach ($GLOBALS['TCA'][$table]['types'][$typeNum]['bitmask_excludelist_bits'] as $bitKey => $eList) {
137 $bit = substr($bitKey, 1);
138 if (MathUtility::canBeInterpretedAsInteger($bit)) {
139 $bit = MathUtility::forceIntegerInRange($bit, 0, 30);
140 if ($bitKey[0] === '-' && !($sTValue & pow(2, $bit)) || $bitKey[0] === '+' && $sTValue & pow(2, $bit)) {
141 $excludeElements = array_merge($excludeElements, GeneralUtility::trimExplode(',', $eList, TRUE));
142 }
143 }
144 }
145 }
146 }
147 return $excludeElements;
148 }
149
150 /**
151 * The requested field value will be overridden with the data from the default
152 * language if the field is configured accordingly.
153 *
154 * @param string $table Table name of the record being edited
155 * @param array $row Record array of the record being edited in current language
156 * @param string $field Field name represented by $item
157 * @param array $fieldConf Content of $PA['fieldConf']
158 * @return string Unprocessed field value merged with default language data if needed
159 */
160 protected function overrideTypeWithValueFromDefaultLanguageRecord($table, array $row, $field, $fieldConf) {
161 $value = $row[$field];
162 if (is_array($this->defaultLanguageData[$table . ':' . $row['uid']])) {
163 // @todo: Is this a bug? Currently the field from default lang is picked in mergeIfNotBlank mode if the
164 // @todo: default value is not empty, but imho it should only be picked if the language overlay record *is* empty?!
165 if (
166 $fieldConf['l10n_mode'] === 'exclude'
167 || $fieldConf['l10n_mode'] === 'mergeIfNotBlank' && trim($this->defaultLanguageData[$table . ':' . $row['uid']][$field]) !== ''
168 ) {
169 $value = $this->defaultLanguageData[$table . ':' . $row['uid']][$field];
170 }
171 }
172 return $value;
173 }
174
175 /**
176 * Return a list without excluded elements.
177 *
178 * @param array $fieldsArray Typically coming from types show item
179 * @param array $excludeElements Field names to be excluded
180 * @return array $fieldsArray without excluded elements
181 */
182 protected function removeExcludeElementsFromFieldArray(array $fieldsArray, array $excludeElements) {
183 $newFieldArray = array();
184 foreach ($fieldsArray as $fieldString) {
185 $fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
186 $fieldName = $fieldArray['fieldName'];
187 // It doesn't make sense to exclude palettes and tabs
188 if (!in_array($fieldName, $excludeElements, TRUE) || $fieldName === '--palette--' || $fieldName === '--div--') {
189 $newFieldArray[] = $fieldString;
190 }
191 }
192 return $newFieldArray;
193 }
194
195
196 /**
197 * A single field of TCA 'types' 'showitem' can have four semicolon separated configuration options:
198 * fieldName: Name of the field to be found in TCA 'columns' section
199 * fieldLabel: An alternative field label
200 * paletteName: Name of a palette to be found in TCA 'palettes' section that is rendered after this field
201 * extra: Special configuration options of this field
202 *
203 * @param string $field Semicolon separated field configuration
204 * @throws \RuntimeException
205 * @return array
206 */
207 protected function explodeSingleFieldShowItemConfiguration($field) {
208 $fieldArray = GeneralUtility::trimExplode(';', $field);
209 /**
210 * @todo: In general, fieldName must always be given, that is why this exception would be useful.
211 * @todo: But in older versions there was a fifth parameter and settings like ';;;;2-2-2' were used,
212 * @todo; which is obsolete now. Stuff like that could later be parsed-out on a different level of the
213 * @todo: system, the exception could then be commented in again.
214 if (empty($fieldArray[0])) {
215 throw new \RuntimeException('Field must not be empty', 1426448465);
216 }
217 */
218 return array(
219 'fieldName' => $fieldArray[0] ?: '', // This ternary could be removed if above todo is resolved
220 'fieldLabel' => $fieldArray[1] ?: NULL,
221 'paletteName' => $fieldArray[2] ?: NULL,
222 'fieldExtra' => $fieldArray[3] ?: NULL,
223 );
224 }
225
226 /**
227 * Will register data from original language records if the current record is a translation of another.
228 * The original data is shown with the edited record in the form.
229 * The information also includes possibly diff-views of what changed in the original record.
230 * Function called from outside (see alt_doc.php + quick edit) before rendering a form for a record
231 *
232 * @param string $table Table name of the record being edited
233 * @param array $rec Record array of the record being edited
234 * @return void
235 */
236 protected function registerDefaultLanguageData($table, $rec) {
237 // @todo: early return here if the arrays are already filled?
238
239 // Add default language:
240 if (
241 $GLOBALS['TCA'][$table]['ctrl']['languageField'] && $rec[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0
242 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
243 && (int)$rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0
244 ) {
245 $lookUpTable = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable']
246 ? $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable']
247 : $table;
248 // Get data formatted:
249 $this->defaultLanguageData[$table . ':' . $rec['uid']] = BackendUtility::getRecordWSOL(
250 $lookUpTable,
251 (int)$rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
252 );
253 // Get data for diff:
254 if ($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']) {
255 $this->defaultLanguageDataDiff[$table . ':' . $rec['uid']] = unserialize($rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]);
256 }
257 // If there are additional preview languages, load information for them also:
258 foreach ($this->globalOptions['additionalPreviewLanguages'] as $prL) {
259 /** @var $translationConfigurationProvider TranslationConfigurationProvider */
260 $translationConfigurationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
261 $translationInfo = $translationConfigurationProvider->translationInfo($lookUpTable, (int)$rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], $prL['uid']);
262 if (is_array($translationInfo['translations']) && is_array($translationInfo['translations'][$prL['uid']])) {
263 $this->additionalPreviewLanguageData[$table . ':' . $rec['uid']][$prL['uid']] = BackendUtility::getRecordWSOL($table, (int)$translationInfo['translations'][$prL['uid']]['uid']);
264 }
265 }
266 }
267 }
268
269 /**
270 * Evaluate condition of flex forms
271 *
272 * @param string $displayCondition The condition to evaluate
273 * @param array $flexFormData Given data the condition is based on
274 * @return bool TRUE if condition matched
275 */
276 protected function evaluateFlexFormDisplayCondition($displayCondition, $flexFormData) {
277 $elementConditionMatcher = GeneralUtility::makeInstance(ElementConditionMatcher::class);
278
279 $splitCondition = GeneralUtility::trimExplode(':', $displayCondition);
280 $skipCondition = FALSE;
281 $fakeRow = array();
282 switch ($splitCondition[0]) {
283 case 'FIELD':
284 // @todo: Not 100% sure if that is correct this way
285 list($_sheetName, $fieldName) = GeneralUtility::trimExplode('.', $splitCondition[1]);
286 $fieldValue = $flexFormData[$fieldName];
287 $splitCondition[1] = $fieldName;
288 $dataStructure['ROOT']['TCEforms']['displayCond'] = join(':', $splitCondition);
289 $fakeRow = array($fieldName => $fieldValue);
290 break;
291 case 'HIDE_FOR_NON_ADMINS':
292
293 case 'VERSION':
294
295 case 'HIDE_L10N_SIBLINGS':
296
297 case 'EXT':
298 break;
299 case 'REC':
300 $fakeRow = array('uid' => $this->globalOptions['databaseRow']['uid']);
301 break;
302 default:
303 $skipCondition = TRUE;
304 }
305 if ($skipCondition) {
306 return TRUE;
307 } else {
308 return $elementConditionMatcher->match($displayCondition, $fakeRow, 'vDEF');
309 }
310 }
311
312 /**
313 * Rendering preview output of a field value which is not shown as a form field but just outputted.
314 *
315 * @param string $value The value to output
316 * @param array $config Configuration for field.
317 * @param string $field Name of field.
318 * @return string HTML formatted output
319 */
320 protected function previewFieldValue($value, $config, $field = '') {
321 if ($config['config']['type'] === 'group' && ($config['config']['internal_type'] === 'file' || $config['config']['internal_type'] === 'file_reference')) {
322 // Ignore upload folder if internal_type is file_reference
323 if ($config['config']['internal_type'] === 'file_reference') {
324 $config['config']['uploadfolder'] = '';
325 }
326 $table = 'tt_content';
327 // Making the array of file items:
328 $itemArray = GeneralUtility::trimExplode(',', $value, TRUE);
329 // Showing thumbnails:
330 $thumbnail = '';
331 $imgs = array();
332 foreach ($itemArray as $imgRead) {
333 $imgParts = explode('|', $imgRead);
334 $imgPath = rawurldecode($imgParts[0]);
335 $rowCopy = array();
336 $rowCopy[$field] = $imgPath;
337 // Icon + click menu:
338 $absFilePath = GeneralUtility::getFileAbsFileName($config['config']['uploadfolder'] ? $config['config']['uploadfolder'] . '/' . $imgPath : $imgPath);
339 $fileInformation = pathinfo($imgPath);
340 $fileIcon = IconUtility::getSpriteIconForFile(
341 $imgPath,
342 array(
343 'title' => htmlspecialchars($fileInformation['basename'] . ($absFilePath && @is_file($absFilePath) ? ' (' . GeneralUtility::formatSize(filesize($absFilePath)) . 'bytes)' : ' - FILE NOT FOUND!'))
344 )
345 );
346 $imgs[] =
347 '<span class="text-nowrap">' .
348 BackendUtility::thumbCode(
349 $rowCopy,
350 $table,
351 $field,
352 '',
353 'thumbs.php',
354 $config['config']['uploadfolder'], 0, ' align="middle"'
355 ) .
356 ($absFilePath ? $this->getControllerDocumentTemplate()->wrapClickMenuOnIcon($fileIcon, $absFilePath, 0, 1, '', '+copy,info,edit,view') : $fileIcon) .
357 $imgPath .
358 '</span>';
359 }
360 return implode('<br />', $imgs);
361 } else {
362 return nl2br(htmlspecialchars($value));
363 }
364 }
365
366 /**
367 * @return DocumentTemplate
368 */
369 protected function getControllerDocumentTemplate() {
370 return $GLOBALS['SOBE']->doc;
371 }
372
373 }