TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
SuggestWizardController.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Backend\Controller\Wizard;
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 Psr\Http\Message\ResponseInterface;
18 use Psr\Http\Message\ServerRequestInterface;
26 
31 {
40  public function searchAction(ServerRequestInterface $request, ResponseInterface $response)
41  {
42  $parsedBody = $request->getParsedBody();
43 
44  if (!isset($parsedBody['value'])
45  || !isset($parsedBody['table'])
46  || !isset($parsedBody['field'])
47  || !isset($parsedBody['uid'])
48  || !isset($parsedBody['dataStructureIdentifier'])
49  || !isset($parsedBody['hmac'])
50  ) {
51  throw new \RuntimeException(
52  'Missing at least one of the required arguments "value", "table", "field", "uid"'
53  . ', "dataStructureIdentifier" or "hmac"',
54  1478607036
55  );
56  }
57 
58  $search = $parsedBody['value'];
59  $table = $parsedBody['table'];
60  $field = $parsedBody['field'];
61  $uid = $parsedBody['uid'];
62  $pid = (int)$parsedBody['pid'];
63 
64  // flex form section container identifiers are created on js side dynamically "onClick". Those are
65  // not within the generated hmac ... the js side adds "idx{dateInMilliseconds}-", so this is removed here again.
66  // example outgoing in renderSuggestSelector():
67  // flex_1|data|sSuggestCheckCombination|lDEF|settings.subelements|el|ID-356586b0d3-form|item|el|content|vDEF
68  // incoming here:
69  // flex_1|data|sSuggestCheckCombination|lDEF|settings.subelements|el|ID-356586b0d3-idx1478611729574-form|item|el|content|vDEF
70  // Note: For existing containers, these parts are numeric, so "ID-356586b0d3-idx1478611729574-form" becomes 1 or 2, etc.
71  // @todo: This could be kicked is the flex form section containers are moved to an ajax call on creation
72  $fieldForHmac = preg_replace('/idx\d{13}-/', '', $field);
73 
74  $dataStructureIdentifierString = '';
75  if (!empty($parsedBody['dataStructureIdentifier'])) {
76  $dataStructureIdentifierString = json_encode($parsedBody['dataStructureIdentifier']);
77  }
78 
79  $incomingHmac = $parsedBody['hmac'];
80  $calculatedHmac = GeneralUtility::hmac(
81  $table . $fieldForHmac . $uid . $pid . $dataStructureIdentifierString,
82  'formEngineSuggest'
83  );
84  if ($incomingHmac !== $calculatedHmac) {
85  throw new \RuntimeException(
86  'Incoming and calculated hmac do not match',
87  1478608245
88  );
89  }
90 
91  // If the $uid is numeric (existing page) and a suggest wizard in pages is handled, the effective
92  // pid is the uid of that page - important for page ts config configuration.
93  if (MathUtility::canBeInterpretedAsInteger($uid) && $table === 'pages') {
94  $pid = $uid;
95  }
96  $TSconfig = BackendUtility::getPagesTSconfig($pid);
97 
98  // Determine TCA config of field
99  if (empty($dataStructureIdentifierString)) {
100  // Normal columns field
101  $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
102  } else {
103  // A flex flex form field
104  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
105  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifierString);
106  $parts = explode('|', $field);
107  $fieldConfig = $this->getFlexFieldConfiguration($parts, $dataStructureArray);
108  // Flexform field name levels are separated with | instead of encapsulation in [];
109  // reverse this here to be compatible with regular field names.
110  $field = str_replace('|', '][', $field);
111  }
112 
113  $wizardConfig = $fieldConfig['wizards']['suggest'];
114 
115  $queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig);
116  $whereClause = $this->getWhereClause($fieldConfig);
117 
118  $resultRows = [];
119 
120  // fetch the records for each query table. A query table is a table from which records are allowed to
121  // be added to the TCEForm selector, originally fetched from the "allowed" config option in the TCA
122  foreach ($queryTables as $queryTable) {
123  // if the table does not exist, skip it
124  if (!is_array($GLOBALS['TCA'][$queryTable]) || empty($GLOBALS['TCA'][$queryTable])) {
125  continue;
126  }
127 
128  $config = $this->getConfigurationForTable($queryTable, $wizardConfig, $TSconfig, $table, $field);
129 
130  // process addWhere
131  if (!isset($config['addWhere']) && $whereClause) {
132  $config['addWhere'] = $whereClause;
133  }
134  if (isset($config['addWhere'])) {
135  $replacement = [
136  '###THIS_UID###' => (int)$uid,
137  '###CURRENT_PID###' => (int)$pid
138  ];
139  if (isset($TSconfig['TCEFORM.'][$table . '.'][$field . '.'])) {
140  $fieldTSconfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.'];
141  if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) {
142  $replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID'];
143  }
144  if (isset($fieldTSconfig['PAGE_TSCONFIG_IDLIST'])) {
145  $replacement['###PAGE_TSCONFIG_IDLIST###'] = implode(',', GeneralUtility::intExplode(',', $fieldTSconfig['PAGE_TSCONFIG_IDLIST']));
146  }
147  if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) {
148  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($fieldConfig['foreign_table']);
149  // nasty hack, but it's currently not possible to just quote anything "inside" the value but not escaping
150  // the whole field as it is not known where it is used in the WHERE clause
151  $replacement['###PAGE_TSCONFIG_STR###'] = trim($connection->quote($fieldTSconfig['PAGE_TSCONFIG_STR']), '\'');
152  }
153  }
154  $config['addWhere'] = strtr(' ' . $config['addWhere'], $replacement);
155  }
156 
157  // instantiate the class that should fetch the records for this $queryTable
158  $receiverClassName = $config['receiverClass'];
159  if (!class_exists($receiverClassName)) {
160  $receiverClassName = SuggestWizardDefaultReceiver::class;
161  }
162  $receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config);
163  $params = ['value' => $search];
164  $rows = $receiverObj->queryTable($params);
165  if (empty($rows)) {
166  continue;
167  }
168  $resultRows = $rows + $resultRows;
169  unset($rows);
170  }
171 
172  // Limit the number of items in the result list
173  $maxItems = isset($config['maxItemsInResultList']) ? $config['maxItemsInResultList'] : 10;
174  $maxItems = min(count($resultRows), $maxItems);
175 
176  array_splice($resultRows, $maxItems);
177 
178  $response->getBody()->write(json_encode(array_values($resultRows)));
179  return $response;
180  }
181 
188  protected function isTableHidden(array $tableConfig)
189  {
190  return (bool)$tableConfig['ctrl']['hideTable'];
191  }
192 
200  protected function currentBackendUserMayAccessTable(array $tableConfig)
201  {
202  if ($this->getBackendUser()->isAdmin()) {
203  return true;
204  }
205 
206  // If the user is no admin, they may not access admin-only tables
207  if ($tableConfig['ctrl']['adminOnly']) {
208  return false;
209  }
210 
211  // allow access to root level pages if security restrictions should be bypassed
212  return !$tableConfig['ctrl']['rootLevel'] || $tableConfig['ctrl']['security']['ignoreRootLevelRestriction'];
213  }
214 
222  protected function getFlexFieldConfiguration(array $parts, array $dataStructure)
223  {
224  if (count($parts) === 6) {
225  // Search a flex field, example:
226  // flex_1|data|sDb|lDEF|group_db_1|vDEF
227  if (!isset($dataStructure['sheets'][$parts[2]]['ROOT']['el'][$parts[4]]['TCEforms']['config'])) {
228  throw new \RuntimeException(
229  'Specified path ' . implode('|', $parts) . ' not found in flex form data structure',
230  1480609491
231  );
232  }
233  $fieldConfig = $dataStructure['sheets'][$parts[2]]['ROOT']['el'][$parts[4]]['TCEforms']['config'];
234  } elseif (count($parts) === 11) {
235  // Search a flex field in a section container, example:
236  // flex_1|data|sSuggestCheckCombination|lDEF|settings.subelements|el|1|item|el|content|vDEF
237  if (!isset($dataStructure['sheets'][$parts[2]]['ROOT']['el'][$parts[4]]['el'][$parts[7]]['el'][$parts[9]]['TCEforms']['config'])) {
238  throw new \RuntimeException(
239  'Specified path ' . implode('|', $parts) . ' not found in flex form section container data structure',
240  1480611208
241  );
242  }
243  $fieldConfig = $dataStructure['sheets'][$parts[2]]['ROOT']['el'][$parts[4]]['el'][$parts[7]]['el'][$parts[9]]['TCEforms']['config'];
244  } else {
245  throw new \RuntimeException(
246  'Invalid flex form path ' . implode('|', $parts),
247  1480611252
248  );
249  }
250  return $fieldConfig;
251  }
252 
264  protected function getConfigurationForTable($queryTable, array $wizardConfig, array $TSconfig, $table, $field)
265  {
266  $config = (array)$wizardConfig['default'];
267 
268  if (is_array($wizardConfig[$queryTable])) {
269  ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]);
270  }
271  $globalSuggestTsConfig = $TSconfig['TCEFORM.']['suggest.'];
272  $currentFieldSuggestTsConfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'];
273 
274  // merge the configurations of different "levels" to get the working configuration for this table and
275  // field (i.e., go from the most general to the most special configuration)
276  if (is_array($globalSuggestTsConfig['default.'])) {
277  ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig['default.']);
278  }
279 
280  if (is_array($globalSuggestTsConfig[$queryTable . '.'])) {
281  ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig[$queryTable . '.']);
282  }
283 
284  // use $table instead of $queryTable here because we overlay a config
285  // for the input-field here, not for the queried table
286  if (is_array($currentFieldSuggestTsConfig['default.'])) {
287  ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig['default.']);
288  }
289 
290  if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'])) {
291  ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig[$queryTable . '.']);
292  }
293 
294  return $config;
295  }
296 
304  protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig)
305  {
306  $queryTables = [];
307 
308  if (isset($fieldConfig['allowed'])) {
309  if ($fieldConfig['allowed'] !== '*') {
310  // list of allowed tables
311  $queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']);
312  } else {
313  // all tables are allowed, if the user can access them
314  foreach ($GLOBALS['TCA'] as $tableName => $tableConfig) {
315  if (!$this->isTableHidden($tableConfig) && $this->currentBackendUserMayAccessTable($tableConfig)) {
316  $queryTables[] = $tableName;
317  }
318  }
319  unset($tableName, $tableConfig);
320  }
321  } elseif (isset($fieldConfig['foreign_table'])) {
322  // use the foreign table
323  $queryTables = [$fieldConfig['foreign_table']];
324  }
325 
326  return $queryTables;
327  }
328 
337  protected function getWhereClause(array $fieldConfig)
338  {
339  if (!isset($fieldConfig['foreign_table'])) {
340  return '';
341  }
342 
343  // strip ORDER BY clause
344  return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where']));
345  }
346 
350  protected function getBackendUser()
351  {
352  return $GLOBALS['BE_USER'];
353  }
354 }
getConfigurationForTable($queryTable, array $wizardConfig, array $TSconfig, $table, $field)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static getPagesTSconfig($id, $rootLine=null, $returnPartArray=false)
static mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys=true, $includeEmptyValues=true, $enableUnsetFeature=true)
searchAction(ServerRequestInterface $request, ResponseInterface $response)
if(TYPO3_MODE=== 'BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static makeInstance($className,...$constructorArguments)
static hmac($input, $additionalSecret= '')
static intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)