getParsedBody(); $search = $parsedBody['value'] ?? null; $tableName = $parsedBody['tableName'] ?? null; $fieldName = $parsedBody['fieldName'] ?? null; $uid = $parsedBody['uid'] ?? null; $pid = isset($parsedBody['pid']) ? (int)$parsedBody['pid'] : 0; $dataStructureIdentifier = $parsedBody['dataStructureIdentifier'] ?? ''; $flexFormSheetName = $parsedBody['flexFormSheetName'] ?? null; $flexFormFieldName = $parsedBody['flexFormFieldName'] ?? null; $flexFormContainerName = $parsedBody['flexFormContainerName'] ?? null; $flexFormContainerFieldName = $parsedBody['flexFormContainerFieldName'] ?? null; $recordType = (string)($parsedBody['recordTypeValue'] ?? '') ?: null; $schema = $this->tcaSchemaFactory->get($tableName); // Determine TCA config of field if (empty($dataStructureIdentifier)) { // Normal columns field $fieldInformation = $schema->getField($fieldName); $fieldConfig = $fieldInformation->getConfiguration(); $fieldNameInPageTsConfig = $fieldName; // With possible columnsOverrides // @todo Validate if we can move this fallback recordType determination, should be do-able in v13?! if ($recordType === null) { $recordType = BackendUtility::getTCAtypeValue( $tableName, BackendUtility::getRecord($tableName, $uid) ?? [], true ); } if ($recordType !== null && $schema->hasSubSchema($recordType)) { $fieldConfig = $schema->getSubSchema($recordType)->getField($fieldName)->getConfiguration(); } } else { // A flex-form field $dataStructure = $this->flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier, $schema); if (empty($flexFormContainerFieldName)) { // @todo: See if a path in pageTsConfig like "TCEForm.tableName.theContainerFieldName =" is useful and works with other pageTs, too. $fieldNameInPageTsConfig = $flexFormFieldName; if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT'] ['el'][$flexFormFieldName]['config']) ) { throw new \RuntimeException( 'Specified path ' . $flexFormFieldName . ' not found in flex form data structure', 1480609491 ); } $fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT'] ['el'][$flexFormFieldName]['config']; } else { $fieldNameInPageTsConfig = $flexFormContainerFieldName; if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT'] ['el'][$flexFormFieldName] ['el'][$flexFormContainerName] ['el'][$flexFormContainerFieldName]['config']) ) { throw new \RuntimeException( 'Specified path ' . $flexFormContainerName . ' not found in flex form section container data structure', 1480611208 ); } $fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT'] ['el'][$flexFormFieldName] ['el'][$flexFormContainerName] ['el'][$flexFormContainerFieldName]['config']; } } $pageTsConfig = BackendUtility::getPagesTSconfig($pid); $wizardConfig = $fieldConfig['suggestOptions'] ?? []; $queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig); $whereClause = $this->getWhereClause($fieldConfig); $resultRows = []; // fetch the records for each query table. A query table is a table from which records are allowed to // be added to the TCEForm selector, originally fetched from the "allowed" config option in the TCA foreach ($queryTables as $queryTable) { // if the table does not exist, skip it if (!$this->tcaSchemaFactory->has($queryTable)) { continue; } $config = $this->getConfigurationForTable($queryTable, $wizardConfig, $pageTsConfig, $tableName, $fieldNameInPageTsConfig); // process addWhere if (!isset($config['addWhere']) && $whereClause) { $config['addWhere'] = $whereClause; } if (isset($config['addWhere'])) { $replacement = [ '###THIS_UID###' => (int)$uid, '###CURRENT_PID###' => (int)$pid, ]; if (isset($pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'])) { $fieldTSconfig = $pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.']; if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) { $replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID']; } if (isset($fieldTSconfig['PAGE_TSCONFIG_IDLIST'])) { $replacement['###PAGE_TSCONFIG_IDLIST###'] = implode(',', GeneralUtility::intExplode(',', (string)$fieldTSconfig['PAGE_TSCONFIG_IDLIST'])); } if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) { $connection = $this->connectionPool->getConnectionForTable($fieldConfig['foreign_table']); // nasty hack, but it's currently not possible to just quote anything "inside" the value but not escaping // the whole field as it is not known where it is used in the WHERE clause $replacement['###PAGE_TSCONFIG_STR###'] = trim($connection->quote($fieldTSconfig['PAGE_TSCONFIG_STR']), '\''); } } $config['addWhere'] = QueryHelper::quoteDatabaseIdentifiers($this->connectionPool->getConnectionForTable($queryTable), strtr(' ' . $config['addWhere'], $replacement)); } // instantiate the class that should fetch the records for this $queryTable $receiverClassName = $config['receiverClass'] ?? ''; if (!class_exists($receiverClassName)) { $receiverClassName = SuggestWizardDefaultReceiver::class; } $receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config); $params = [ 'value' => $search, 'uid' => $uid, ]; $rows = $receiverObj->queryTable($params); if (empty($rows)) { continue; } $resultRows = $rows + $resultRows; unset($rows); } // Limit the number of items in the result list $maxItems = (int)($config['maxItemsInResultList'] ?? 10); $maxItems = min(count($resultRows), $maxItems); array_splice($resultRows, $maxItems); return new JsonResponse(array_values($resultRows)); } /** * Checks if the current backend user is allowed to access the given table, based on the schema capabilities. */ protected function currentBackendUserMayAccessTable(TcaSchema $schema): bool { if ($this->getBackendUser()->isAdmin()) { return true; } // If the user is no admin, they may not access admin-only tables if ($schema->hasCapability(TcaSchemaCapability::AccessAdminOnly)) { return false; } /** @var RootLevelCapability $rootLevelCapability */ $rootLevelCapability = $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel); // allow access to root level pages if security restrictions should be bypassed return $rootLevelCapability->canAccessRecordsOnRootLevel(); } /** * Returns the configuration for the suggest wizard for the given table. This does multiple overlays from the * TSconfig. * * @param string $queryTable The table to query * @param array $wizardConfig The configuration for the wizard as configured in the data structure * @param array $TSconfig The TSconfig array of the current page * @param string $table The table where the wizard is used * @param string $field The field where the wizard is used */ protected function getConfigurationForTable(string $queryTable, array $wizardConfig, array $TSconfig, string $table, string $field): array { $config = (array)($wizardConfig['default'] ?? []); if (is_array($wizardConfig[$queryTable] ?? null)) { ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]); } $globalSuggestTsConfig = $TSconfig['TCEFORM.']['suggest.'] ?? []; $currentFieldSuggestTsConfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'] ?? []; // merge the configurations of different "levels" to get the working configuration for this table and // field (i.e., go from the most general to the most special configuration) if (is_array($globalSuggestTsConfig['default.'] ?? null)) { ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($globalSuggestTsConfig['default.'])); } if (is_array($globalSuggestTsConfig[$queryTable . '.'] ?? null)) { ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($globalSuggestTsConfig[$queryTable . '.'])); } // use $table instead of $queryTable here because we overlay a config // for the input-field here, not for the queried table if (is_array($currentFieldSuggestTsConfig['default.'] ?? null)) { ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($currentFieldSuggestTsConfig['default.'])); } if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'] ?? null)) { ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($currentFieldSuggestTsConfig[$queryTable . '.'])); } return $config; } /** * Checks the given field configuration for the tables that should be used for querying and returns them as an * array. */ protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig): array { $queryTables = []; if (isset($fieldConfig['allowed'])) { if ($fieldConfig['allowed'] !== '*') { // list of allowed tables $queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']); } else { // all tables are allowed, if the user can access them /** @var TcaSchema $schema */ foreach ($this->tcaSchemaFactory->all() as $tableName => $schema) { if ($schema->hasCapability(TcaSchemaCapability::HideInUi)) { continue; } if ($this->currentBackendUserMayAccessTable($schema)) { $queryTables[] = $tableName; } } } } elseif (isset($fieldConfig['foreign_table'])) { // use the foreign table $queryTables = [$fieldConfig['foreign_table']]; } return $queryTables; } /** * Wraps user functions in the configuration array as a `RawValue` object, * to be asserted later when actually calling `GeneralUtility::callUserFunction`. */ protected function substituteRawValues(array $config): array { if (!empty($config['renderFunc'])) { $config['renderFunc'] = new RawValue($config['renderFunc']); } return $config; } /** * Returns the SQL WHERE clause to use for querying records. This is currently only relevant if a foreign_table * is configured and should be used; it could e.g. be used to limit to a certain subset of records from the * foreign table */ protected function getWhereClause(array $fieldConfig): string { if (!isset($fieldConfig['foreign_table'], $fieldConfig['foreign_table_where'])) { return ''; } // strip ORDER BY clause return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where'])); } protected function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } }