[BUGFIX] Fix foreign_table_where GROUP BY handling
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / FormDataProvider / TcaInputPlaceholders.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\FormDataProvider;
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\Backend\Form\FormDataCompiler;
18 use TYPO3\CMS\Backend\Form\FormDataGroup\TcaInputPlaceholderRecord;
19 use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
20 use TYPO3\CMS\Core\Localization\LanguageService;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22
23 /**
24 * Resolve placeholders for fields of type input or text. The placeholder value
25 * in the processedTca section of the result will be replaced with the resolved
26 * value.
27 */
28 class TcaInputPlaceholders implements FormDataProviderInterface
29 {
30 /**
31 * Resolve placeholders for input/text fields. Placeholders that are simple
32 * strings will be returned unmodified. Placeholders beginning with __row are
33 * being resolved, possibly traversing multiple tables.
34 *
35 * @param array $result
36 * @return array
37 */
38 public function addData(array $result)
39 {
40 foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
41 // Placeholders are only valid for input and text type fields
42 if (
43 ($fieldConfig['config']['type'] !== 'input' && $fieldConfig['config']['type'] !== 'text')
44 || !isset($fieldConfig['config']['placeholder'])
45 ) {
46 continue;
47 }
48
49 // Resolve __row|field type placeholders
50 if (strpos($fieldConfig['config']['placeholder'], '__row|') === 0) {
51 // split field names into array and remove the __row indicator
52 $fieldNameArray = array_slice(
53 GeneralUtility::trimExplode('|', $fieldConfig['config']['placeholder'], true),
54 1
55 );
56 $result['processedTca']['columns'][$fieldName]['config']['placeholder'] = $this->getPlaceholderValue($fieldNameArray, $result);
57 }
58
59 // Resolve placeholders from language files
60 if (strpos($fieldConfig['config']['placeholder'], 'LLL:') === 0) {
61 $result['processedTca']['columns'][$fieldName]['config']['placeholder'] = $this->getLanguageService()->sL($fieldConfig['config']['placeholder']);
62 }
63
64 // Remove empty placeholders
65 if (empty($result['processedTca']['columns'][$fieldName]['config']['placeholder'])) {
66 unset($result['processedTca']['columns'][$fieldName]['config']['placeholder']);
67 }
68 }
69
70 return $result;
71 }
72
73 /**
74 * Recursively resolve the placeholder value. A placeholder string with a
75 * syntax of __row|field1|field2|field3 will be recursively resolved to a
76 * final value.
77 *
78 * @param array $fieldNameArray
79 * @param array $result
80 * @param int $recursionLevel
81 * @return string
82 */
83 protected function getPlaceholderValue($fieldNameArray, $result, $recursionLevel = 0)
84 {
85 if ($recursionLevel > 99) {
86 // This should not happen, treat as misconfiguration
87 return '';
88 }
89
90 $fieldName = array_shift($fieldNameArray);
91 $fieldConfig = $result['processedTca']['columns'][$fieldName]['config'];
92
93 // Skip if a defined field was actually not present in the database row
94 // Using array_key_exists here, since NULL values are valid as well.
95 if (!array_key_exists($fieldName, $result['databaseRow'])) {
96 return '';
97 }
98
99 $value = $result['databaseRow'][$fieldName];
100
101 switch ($fieldConfig['type']) {
102 case 'select':
103 // The FormDataProviders already resolved the select items to an array of uids,
104 // filter out empty values that occur when no related record has been selected.
105 $possibleUids = array_filter($value);
106 $foreignTableName = $fieldConfig['foreign_table'];
107 break;
108 case 'group':
109 $possibleUids = $this->getRelatedGroupFieldUids($fieldConfig, $value);
110 $foreignTableName = $this->getAllowedTableForGroupField($fieldConfig);
111 break;
112 case 'inline':
113 $possibleUids = array_filter(GeneralUtility::trimExplode(',', $value, true));
114 $foreignTableName = $fieldConfig['foreign_table'];
115 break;
116 default:
117 $possibleUids = [];
118 $foreignTableName = '';
119 }
120
121 if (!empty($possibleUids) && !empty($fieldNameArray)) {
122 $relatedFormData = $this->getRelatedFormData($foreignTableName, $possibleUids[0], $fieldNameArray[0]);
123 $value = $this->getPlaceholderValue($fieldNameArray, $relatedFormData, $recursionLevel + 1);
124 }
125
126 if ($recursionLevel === 0 && is_array($value)) {
127 $value = implode(', ', $value);
128 }
129 return (string)$value;
130 }
131
132 /**
133 * Compile a formdata result set based on the tablename and record uid.
134 *
135 * @param string $tableName Name of the table for which to compile formdata
136 * @param int $uid UID of the record for which to compile the formdata
137 * @param string $columnToProcess The column that is required from the record
138 * @return array The compiled formdata
139 */
140 protected function getRelatedFormData($tableName, $uid, $columnToProcess)
141 {
142 $fakeDataInput = [
143 'command' => 'edit',
144 'vanillaUid' => (int)$uid,
145 'tableName' => $tableName,
146 'inlineCompileExistingChildren' => false,
147 'columnsToProcess' => [$columnToProcess],
148 ];
149 /** @var TcaInputPlaceholderRecord $formDataGroup */
150 $formDataGroup = GeneralUtility::makeInstance(TcaInputPlaceholderRecord::class);
151 /** @var FormDataCompiler $formDataCompiler */
152 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
153 $compilerResult = $formDataCompiler->compile($fakeDataInput);
154 return $compilerResult;
155 }
156
157 /**
158 * Return uids of related records for group type fields. Uids consisting of
159 * multiple parts like [table]_[uid]|[title] will be reduced to integers and
160 * validated against the allowed table. Uids without a table prefix are
161 * accepted in any case.
162 *
163 * @param array $fieldConfig TCA "config" section for the group type field.
164 * @param string $value A comma separated list of records
165 * @return array
166 */
167 protected function getRelatedGroupFieldUids(array $fieldConfig, $value)
168 {
169 $relatedUids = [];
170 $allowedTable = $this->getAllowedTableForGroupField($fieldConfig);
171
172 // Skip if it's not a database relation with a resolvable foreign table
173 if (($fieldConfig['internal_type'] !== 'db') || ($allowedTable === false)) {
174 return $relatedUids;
175 }
176
177 // Related group values have been prepared by TcaGroup data provider, an array is expected here
178 foreach ($value as $singleValue) {
179 $relatedUids[] = $singleValue['uid'];
180 }
181
182 return $relatedUids;
183 }
184
185 /**
186 * Will read the "allowed" value from the given field configuration
187 * and returns FALSE if none or more than one has been defined.
188 * Otherwise the name of the allowed table will be returned.
189 *
190 * @param array $fieldConfig TCA "config" section for the group type field.
191 * @return bool|string
192 */
193 protected function getAllowedTableForGroupField(array $fieldConfig)
194 {
195 $allowedTable = false;
196
197 $allowedTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed'], true);
198 if (count($allowedTables) === 1) {
199 $allowedTable = $allowedTables[0];
200 }
201
202 return $allowedTable;
203 }
204
205 /**
206 * @return LanguageService
207 */
208 protected function getLanguageService()
209 {
210 return $GLOBALS['LANG'];
211 }
212 }