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 ($fieldConfig['config']['type'] !== 'input' && $fieldConfig['config']['type'] !== '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 // The FormDataProviders already resolved the select items to an array of uids, 114 // filter out empty values that occur when no related record has been selected. 115 $possibleUids = array_filter($value); 116 $foreignTableName = $fieldConfig['foreign_table']; 117 break; 118 case 'group': 119 $possibleUids = $this->getRelatedGroupFieldUids($fieldConfig, $value); 120 $foreignTableName = $this->getAllowedTableForGroupField($fieldConfig); 121 break; 122 case 'inline': 123 $possibleUids = array_filter(GeneralUtility::trimExplode(',', $value, true)); 124 $foreignTableName = $fieldConfig['foreign_table']; 125 break; 126 default: 127 $possibleUids = []; 128 $foreignTableName = ''; 129 } 130 131 if (!empty($possibleUids) && !empty($fieldNameArray)) { 132 if (count($possibleUids) > 1 133 && !empty($GLOBALS['TCA'][$foreignTableName]['ctrl']['languageField']) 134 && isset($result['currentSysLanguage']) 135 ) { 136 $possibleUids = $this->getPossibleUidsByCurrentSysLanguage($possibleUids, $foreignTableName, $result['currentSysLanguage']); 137 } 138 $relatedFormData = $this->getRelatedFormData($foreignTableName, $possibleUids[0], $fieldNameArray[0]); 139 if (!empty($GLOBALS['TCA'][$result['tableName']]['ctrl']['languageField']) 140 && isset($result['databaseRow'][$GLOBALS['TCA'][$result['tableName']]['ctrl']['languageField']]) 141 ) { 142 $relatedFormData['currentSysLanguage'] = $result['databaseRow'][$GLOBALS['TCA'][$result['tableName']]['ctrl']['languageField']][0]; 143 } 144 $value = $this->getPlaceholderValue($fieldNameArray, $relatedFormData, $recursionLevel + 1); 145 } 146 147 if ($recursionLevel === 0 && is_array($value)) { 148 $value = implode(', ', $value); 149 } 150 return (string)$value; 151 } 152 153 /** 154 * Compile a formdata result set based on the tablename and record uid. 155 * 156 * @param string $tableName Name of the table for which to compile formdata 157 * @param int $uid UID of the record for which to compile the formdata 158 * @param string $columnToProcess The column that is required from the record 159 * @return array The compiled formdata 160 */ 161 protected function getRelatedFormData($tableName, $uid, $columnToProcess) 162 { 163 $fakeDataInput = [ 164 'command' => 'edit', 165 'vanillaUid' => (int)$uid, 166 'tableName' => $tableName, 167 'inlineCompileExistingChildren' => false, 168 'columnsToProcess' => [$columnToProcess], 169 ]; 170 /** @var TcaInputPlaceholderRecord $formDataGroup */ 171 $formDataGroup = GeneralUtility::makeInstance(TcaInputPlaceholderRecord::class); 172 /** @var FormDataCompiler $formDataCompiler */ 173 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup); 174 $compilerResult = $formDataCompiler->compile($fakeDataInput); 175 return $compilerResult; 176 } 177 178 /** 179 * Return uids of related records for group type fields. Uids consisting of 180 * multiple parts like [table]_[uid]|[title] will be reduced to integers and 181 * validated against the allowed table. Uids without a table prefix are 182 * accepted in any case. 183 * 184 * @param array $fieldConfig TCA "config" section for the group type field. 185 * @param string $value A comma separated list of records 186 * @return array 187 */ 188 protected function getRelatedGroupFieldUids(array $fieldConfig, $value) 189 { 190 $relatedUids = []; 191 $allowedTable = $this->getAllowedTableForGroupField($fieldConfig); 192 193 // Skip if it's not a database relation with a resolvable foreign table 194 if (($fieldConfig['internal_type'] !== 'db') || ($allowedTable === false)) { 195 return $relatedUids; 196 } 197 198 // Related group values have been prepared by TcaGroup data provider, an array is expected here 199 foreach ($value as $singleValue) { 200 $relatedUids[] = $singleValue['uid']; 201 } 202 203 return $relatedUids; 204 } 205 206 /** 207 * Will read the "allowed" value from the given field configuration 208 * and returns FALSE if none or more than one has been defined. 209 * Otherwise the name of the allowed table will be returned. 210 * 211 * @param array $fieldConfig TCA "config" section for the group type field. 212 * @return bool|string 213 */ 214 protected function getAllowedTableForGroupField(array $fieldConfig) 215 { 216 $allowedTable = false; 217 218 $allowedTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed'], true); 219 if (count($allowedTables) === 1) { 220 $allowedTable = $allowedTables[0]; 221 } 222 223 return $allowedTable; 224 } 225 226 /** 227 * E.g. sys_file is not translatable, thus the uid of the translation of it's metadata has to be retrieved here. 228 * 229 * Get the uid of e.g. a file metadata entry for a given sys_language_uid and the possible translated data. 230 * If there is no translation available, return the uid of default language. 231 * If there is no value at all, return the "possible uids". 232 * 233 * @param array $possibleUids 234 * @param string $foreignTableName 235 * @param int $currentLanguage 236 * @return array 237 */ 238 protected function getPossibleUidsByCurrentSysLanguage(array $possibleUids, $foreignTableName, $currentLanguage) 239 { 240 $languageField = $GLOBALS['TCA'][$foreignTableName]['ctrl']['languageField']; 241 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTableName); 242 $possibleRecords = $queryBuilder->select('uid', $languageField) 243 ->from($foreignTableName) 244 ->where( 245 $queryBuilder->expr()->in( 246 'uid', 247 $queryBuilder->createNamedParameter($possibleUids, Connection::PARAM_INT_ARRAY) 248 ), 249 $queryBuilder->expr()->in( 250 $languageField, 251 $queryBuilder->createNamedParameter([$currentLanguage, 0], Connection::PARAM_INT_ARRAY) 252 ) 253 ) 254 ->groupBy($languageField, 'uid') 255 ->execute() 256 ->fetchAll(); 257 258 if (!empty($possibleRecords)) { 259 // Either only one record or first record matches language 260 if (count($possibleRecords) === 1 261 || (int)$possibleRecords[0][$languageField] === (int)$currentLanguage 262 ) { 263 return [$possibleRecords[0]['uid']]; 264 } 265 266 // Language of second record matches language 267 return [$possibleRecords[1]['uid']]; 268 } 269 270 return $possibleUids; 271 } 272 273 /** 274 * @return LanguageService 275 */ 276 protected function getLanguageService() 277 { 278 return $GLOBALS['LANG']; 279 } 280} 281