[!!!][TASK] House of forms
[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 if (empty($fieldArray[0])) {
210 throw new \RuntimeException('Field must not be empty', 1426448465);
211 }
212 return array(
213 'fieldName' => $fieldArray[0],
214 'fieldLabel' => $fieldArray[1] ?: NULL,
215 'paletteName' => $fieldArray[2] ?: NULL,
216 'fieldExtra' => $fieldArray[3] ?: NULL,
217 );
218 }
219
220 /**
221 * Will register data from original language records if the current record is a translation of another.
222 * The original data is shown with the edited record in the form.
223 * The information also includes possibly diff-views of what changed in the original record.
224 * Function called from outside (see alt_doc.php + quick edit) before rendering a form for a record
225 *
226 * @param string $table Table name of the record being edited
227 * @param array $rec Record array of the record being edited
228 * @return void
229 */
230 protected function registerDefaultLanguageData($table, $rec) {
231 // @todo: early return here if the arrays are already filled?
232
233 // Add default language:
234 if (
235 $GLOBALS['TCA'][$table]['ctrl']['languageField'] && $rec[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0
236 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
237 && (int)$rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0
238 ) {
239 $lookUpTable = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable']
240 ? $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable']
241 : $table;
242 // Get data formatted:
243 $this->defaultLanguageData[$table . ':' . $rec['uid']] = BackendUtility::getRecordWSOL(
244 $lookUpTable,
245 (int)$rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
246 );
247 // Get data for diff:
248 if ($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']) {
249 $this->defaultLanguageDataDiff[$table . ':' . $rec['uid']] = unserialize($rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]);
250 }
251 // If there are additional preview languages, load information for them also:
252 foreach ($this->globalOptions['additionalPreviewLanguages'] as $prL) {
253 /** @var $translationConfigurationProvider TranslationConfigurationProvider */
254 $translationConfigurationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
255 $translationInfo = $translationConfigurationProvider->translationInfo($lookUpTable, (int)$rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], $prL['uid']);
256 if (is_array($translationInfo['translations']) && is_array($translationInfo['translations'][$prL['uid']])) {
257 $this->additionalPreviewLanguageData[$table . ':' . $rec['uid']][$prL['uid']] = BackendUtility::getRecordWSOL($table, (int)$translationInfo['translations'][$prL['uid']]['uid']);
258 }
259 }
260 }
261 }
262
263 /**
264 * Evaluate condition of flex forms
265 *
266 * @param string $displayCondition The condition to evaluate
267 * @param array $flexFormData Given data the condition is based on
268 * @return bool TRUE if condition matched
269 */
270 protected function evaluateFlexFormDisplayCondition($displayCondition, $flexFormData) {
271 $elementConditionMatcher = GeneralUtility::makeInstance(ElementConditionMatcher::class);
272
273 $splitCondition = GeneralUtility::trimExplode(':', $displayCondition);
274 $skipCondition = FALSE;
275 $fakeRow = array();
276 switch ($splitCondition[0]) {
277 case 'FIELD':
278 // @todo: Not 100% sure if that is correct this way
279 list($_sheetName, $fieldName) = GeneralUtility::trimExplode('.', $splitCondition[1]);
280 $fieldValue = $flexFormData[$fieldName];
281 $splitCondition[1] = $fieldName;
282 $dataStructure['ROOT']['TCEforms']['displayCond'] = join(':', $splitCondition);
283 $fakeRow = array($fieldName => $fieldValue);
284 break;
285 case 'HIDE_FOR_NON_ADMINS':
286
287 case 'VERSION':
288
289 case 'HIDE_L10N_SIBLINGS':
290
291 case 'EXT':
292 break;
293 case 'REC':
294 $fakeRow = array('uid' => $this->globalOptions['databaseRow']['uid']);
295 break;
296 default:
297 $skipCondition = TRUE;
298 }
299 if ($skipCondition) {
300 return TRUE;
301 } else {
302 return $elementConditionMatcher->match($displayCondition, $fakeRow, 'vDEF');
303 }
304 }
305
306 /**
307 * Rendering preview output of a field value which is not shown as a form field but just outputted.
308 *
309 * @param string $value The value to output
310 * @param array $config Configuration for field.
311 * @param string $field Name of field.
312 * @return string HTML formatted output
313 */
314 protected function previewFieldValue($value, $config, $field = '') {
315 if ($config['config']['type'] === 'group' && ($config['config']['internal_type'] === 'file' || $config['config']['internal_type'] === 'file_reference')) {
316 // Ignore upload folder if internal_type is file_reference
317 if ($config['config']['internal_type'] === 'file_reference') {
318 $config['config']['uploadfolder'] = '';
319 }
320 $table = 'tt_content';
321 // Making the array of file items:
322 $itemArray = GeneralUtility::trimExplode(',', $value, TRUE);
323 // Showing thumbnails:
324 $thumbnail = '';
325 $imgs = array();
326 foreach ($itemArray as $imgRead) {
327 $imgParts = explode('|', $imgRead);
328 $imgPath = rawurldecode($imgParts[0]);
329 $rowCopy = array();
330 $rowCopy[$field] = $imgPath;
331 // Icon + click menu:
332 $absFilePath = GeneralUtility::getFileAbsFileName($config['config']['uploadfolder'] ? $config['config']['uploadfolder'] . '/' . $imgPath : $imgPath);
333 $fileInformation = pathinfo($imgPath);
334 $fileIcon = IconUtility::getSpriteIconForFile(
335 $imgPath,
336 array(
337 'title' => htmlspecialchars($fileInformation['basename'] . ($absFilePath && @is_file($absFilePath) ? ' (' . GeneralUtility::formatSize(filesize($absFilePath)) . 'bytes)' : ' - FILE NOT FOUND!'))
338 )
339 );
340 $imgs[] =
341 '<span class="text-nowrap">' .
342 BackendUtility::thumbCode(
343 $rowCopy,
344 $table,
345 $field,
346 '',
347 'thumbs.php',
348 $config['config']['uploadfolder'], 0, ' align="middle"'
349 ) .
350 ($absFilePath ? $this->getControllerDocumentTemplate()->wrapClickMenuOnIcon($fileIcon, $absFilePath, 0, 1, '', '+copy,info,edit,view') : $fileIcon) .
351 $imgPath .
352 '</span>';
353 }
354 return implode('<br />', $imgs);
355 } else {
356 return nl2br(htmlspecialchars($value));
357 }
358 }
359
360 /**
361 * @return DocumentTemplate
362 */
363 protected function getControllerDocumentTemplate() {
364 return $GLOBALS['SOBE']->doc;
365 }
366
367 }