1<?php 2 3/* 4 * This file is part of the TYPO3 CMS project. 5 * 6 * It is free software; you can redistribute it and/or modify it under 7 * the terms of the GNU General Public License, either version 2 8 * of the License, or any later version. 9 * 10 * For the full copyright and license information, please read the 11 * LICENSE.txt file that was distributed with this source code. 12 * 13 * The TYPO3 project - inspiring people to share! 14 */ 15 16namespace TYPO3\CMS\Backend\Form\FormDataProvider; 17 18use Doctrine\DBAL\Connection; 19use TYPO3\CMS\Backend\Form\FormDataCompiler; 20use TYPO3\CMS\Backend\Form\FormDataGroup\TcaInputPlaceholderRecord; 21use TYPO3\CMS\Backend\Form\FormDataProviderInterface; 22use TYPO3\CMS\Core\Database\ConnectionPool; 23use TYPO3\CMS\Core\Localization\LanguageService; 24use TYPO3\CMS\Core\Utility\GeneralUtility; 25 26/** 27 * Resolve placeholders for fields of type input or text. The placeholder value 28 * in the processedTca section of the result will be replaced with the resolved 29 * value. 30 */ 31class TcaInputPlaceholders implements FormDataProviderInterface 32{ 33 /** 34 * Resolve placeholders for input/text fields. Placeholders that are simple 35 * strings will be returned unmodified. Placeholders beginning with __row are 36 * being resolved, possibly traversing multiple tables. 37 * 38 * @param array $result 39 * @return array 40 */ 41 public function addData(array $result) 42 { 43 foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) { 44 // Placeholders are only valid for input and text type fields 45 if ( 46 (!in_array($fieldConfig['config']['type'] ?? false, ['input', 'text'])) 47 || !isset($fieldConfig['config']['placeholder']) 48 ) { 49 continue; 50 } 51 52 // Resolve __row|field type placeholders 53 if (strpos($fieldConfig['config']['placeholder'], '__row|') === 0) { 54 // split field names into array and remove the __row indicator 55 $fieldNameArray = array_slice( 56 GeneralUtility::trimExplode('|', $fieldConfig['config']['placeholder'], true), 57 1 58 ); 59 $result['processedTca']['columns'][$fieldName]['config']['placeholder'] = $this->getPlaceholderValue($fieldNameArray, $result); 60 } 61 62 // Resolve placeholders from language files 63 if (strpos($fieldConfig['config']['placeholder'], 'LLL:') === 0) { 64 $result['processedTca']['columns'][$fieldName]['config']['placeholder'] = $this->getLanguageService()->sL($fieldConfig['config']['placeholder']); 65 } 66 67 // Remove empty placeholders 68 if (empty($result['processedTca']['columns'][$fieldName]['config']['placeholder'])) { 69 unset($result['processedTca']['columns'][$fieldName]['config']['placeholder']); 70 } 71 } 72 73 return $result; 74 } 75 76 /** 77 * Recursively resolve the placeholder value. A placeholder string with a 78 * syntax of __row|field1|field2|field3 will be recursively resolved to a 79 * final value. 80 * 81 * @param array $fieldNameArray 82 * @param array $result 83 * @param int $recursionLevel 84 * @return string 85 */ 86 protected function getPlaceholderValue($fieldNameArray, $result, $recursionLevel = 0) 87 { 88 if ($recursionLevel > 99) { 89 // This should not happen, treat as misconfiguration 90 return ''; 91 } 92 93 $fieldName = array_shift($fieldNameArray); 94 95 // Skip if a defined field was actually not present in the database row 96 // Using array_key_exists here, since NULL values are valid as well. 97 if (!array_key_exists($fieldName, $result['databaseRow'])) { 98 return ''; 99 } 100 101 $value = $result['databaseRow'][$fieldName]; 102 103 if (!isset($result['processedTca']['columns'][$fieldName]['config']) 104 || !is_array($result['processedTca']['columns'][$fieldName]['config']) 105 ) { 106 return (string)$value; 107 } 108 109 $fieldConfig = $result['processedTca']['columns'][$fieldName]['config']; 110 111 switch ($fieldConfig['type']) { 112 case 'select': 113 case 'category': 114 // The FormDataProviders already resolved the select items to an array of uids, 115 // filter out empty values that occur when no related record has been selected. 116 $possibleUids = array_filter($value); 117 $foreignTableName = $fieldConfig['foreign_table']; 118 break; 119 case 'group': 120 $possibleUids = $this->getRelatedGroupFieldUids($fieldConfig, $value); 121 $foreignTableName = $this->getAllowedTableForGroupField($fieldConfig); 122 break; 123 case 'inline': 124 $possibleUids = array_filter(GeneralUtility::trimExplode(',', $value, true)); 125 $foreignTableName = $fieldConfig['foreign_table']; 126 break; 127 default: 128 $possibleUids = []; 129 $foreignTableName = ''; 130 } 131 132 if (!empty($possibleUids) && !empty($fieldNameArray)) { 133 if (count($possibleUids) > 1 134 && !empty($GLOBALS['TCA'][$foreignTableName]['ctrl']['languageField']) 135 && isset($result['currentSysLanguage']) 136 ) { 137 $possibleUids = $this->getPossibleUidsByCurrentSysLanguage($possibleUids, $foreignTableName, $result['currentSysLanguage']); 138 } 139 $relatedFormData = $this->getRelatedFormData($foreignTableName, $possibleUids[0], $fieldNameArray[0]); 140 if (!empty($GLOBALS['TCA'][$result['tableName']]['ctrl']['languageField']) 141 && isset($result['databaseRow'][$GLOBALS['TCA'][$result['tableName']]['ctrl']['languageField']]) 142 ) { 143 $relatedFormData['currentSysLanguage'] = $result['databaseRow'][$GLOBALS['TCA'][$result['tableName']]['ctrl']['languageField'] ?? null][0] ?? ''; 144 } 145 $value = $this->getPlaceholderValue($fieldNameArray, $relatedFormData, $recursionLevel + 1); 146 } 147 148 if ($recursionLevel === 0 && is_array($value)) { 149 $value = implode(', ', $value); 150 } 151 return (string)$value; 152 } 153 154 /** 155 * Compile a formdata result set based on the tablename and record uid. 156 * 157 * @param string $tableName Name of the table for which to compile formdata 158 * @param int $uid UID of the record for which to compile the formdata 159 * @param string $columnToProcess The column that is required from the record 160 * @return array The compiled formdata 161 */ 162 protected function getRelatedFormData($tableName, $uid, $columnToProcess) 163 { 164 $fakeDataInput = [ 165 'command' => 'edit', 166 'vanillaUid' => (int)$uid, 167 'tableName' => $tableName, 168 'inlineCompileExistingChildren' => false, 169 'columnsToProcess' => [$columnToProcess], 170 ]; 171 /** @var TcaInputPlaceholderRecord $formDataGroup */ 172 $formDataGroup = GeneralUtility::makeInstance(TcaInputPlaceholderRecord::class); 173 /** @var FormDataCompiler $formDataCompiler */ 174 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup); 175 $compilerResult = $formDataCompiler->compile($fakeDataInput); 176 return $compilerResult; 177 } 178 179 /** 180 * Return uids of related records for group type fields. Uids consisting of 181 * multiple parts like [table]_[uid]|[title] will be reduced to integers and 182 * validated against the allowed table. Uids without a table prefix are 183 * accepted in any case. 184 * 185 * @param array $fieldConfig TCA "config" section for the group type field. 186 * @param string $value A comma separated list of records 187 * @return array 188 */ 189 protected function getRelatedGroupFieldUids(array $fieldConfig, $value): array 190 { 191 $relatedUids = []; 192 $allowedTable = $this->getAllowedTableForGroupField($fieldConfig); 193 194 // Skip if it's not a resolvable foreign table 195 if (!$allowedTable) { 196 return []; 197 } 198 199 // Related group values have been prepared by TcaGroup data provider, an array is expected here 200 foreach ($value as $singleValue) { 201 $relatedUids[] = $singleValue['uid']; 202 } 203 204 return $relatedUids; 205 } 206 207 /** 208 * Will read the "allowed" value from the given field configuration 209 * and returns FALSE if none or more than one has been defined. 210 * Otherwise the name of the allowed table will be returned. 211 * 212 * @param array $fieldConfig TCA "config" section for the group type field. 213 * @return bool|string 214 */ 215 protected function getAllowedTableForGroupField(array $fieldConfig) 216 { 217 $allowedTable = false; 218 219 $allowedTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed'], true); 220 if (count($allowedTables) === 1) { 221 $allowedTable = $allowedTables[0]; 222 } 223 224 return $allowedTable; 225 } 226 227 /** 228 * E.g. sys_file is not translatable, thus the uid of the translation of it's metadata has to be retrieved here. 229 * 230 * Get the uid of e.g. a file metadata entry for a given sys_language_uid and the possible translated data. 231 * If there is no translation available, return the uid of default language. 232 * If there is no value at all, return the "possible uids". 233 * 234 * @param array $possibleUids 235 * @param string $foreignTableName 236 * @param int $currentLanguage 237 * @return array 238 */ 239 protected function getPossibleUidsByCurrentSysLanguage(array $possibleUids, $foreignTableName, $currentLanguage) 240 { 241 $languageField = $GLOBALS['TCA'][$foreignTableName]['ctrl']['languageField']; 242 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTableName); 243 $possibleRecords = $queryBuilder->select('uid', $languageField) 244 ->from($foreignTableName) 245 ->where( 246 $queryBuilder->expr()->in( 247 'uid', 248 $queryBuilder->createNamedParameter($possibleUids, Connection::PARAM_INT_ARRAY) 249 ), 250 $queryBuilder->expr()->in( 251 $languageField, 252 $queryBuilder->createNamedParameter([$currentLanguage, 0], Connection::PARAM_INT_ARRAY) 253 ) 254 ) 255 ->groupBy($languageField, 'uid') 256 ->executeQuery() 257 ->fetchAllAssociative(); 258 259 if (!empty($possibleRecords)) { 260 // Either only one record or first record matches language 261 if (count($possibleRecords) === 1 262 || (int)$possibleRecords[0][$languageField] === (int)$currentLanguage 263 ) { 264 return [$possibleRecords[0]['uid']]; 265 } 266 267 // Language of second record matches language 268 return [$possibleRecords[1]['uid']]; 269 } 270 271 return $possibleUids; 272 } 273 274 /** 275 * @return LanguageService 276 */ 277 protected function getLanguageService() 278 { 279 return $GLOBALS['LANG']; 280 } 281} 282