[BUGFIX] Ensure most site related exceptions are handled
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / FormDataProvider / SiteTcaInline.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Backend\Form\FormDataProvider;
5
6 /*
7 * This file is part of the TYPO3 CMS project.
8 *
9 * It is free software; you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License, either version 2
11 * of the License, or any later version.
12 *
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
15 *
16 * The TYPO3 project - inspiring people to share!
17 */
18
19 use TYPO3\CMS\Backend\Form\FormDataCompiler;
20 use TYPO3\CMS\Backend\Form\FormDataGroup\OnTheFly;
21 use TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup;
22 use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
23 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
24 use TYPO3\CMS\Backend\Utility\BackendUtility;
25 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
26 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
27 use TYPO3\CMS\Core\Site\SiteFinder;
28 use TYPO3\CMS\Core\Utility\GeneralUtility;
29
30 /**
31 * Special data provider for the sites configuration module.
32 *
33 * Handle inline children of 'site'
34 */
35 class SiteTcaInline extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
36 {
37 /**
38 * Resolve inline fields
39 *
40 * @param array $result
41 * @return array
42 */
43 public function addData(array $result): array
44 {
45 $result = $this->addInlineFirstPid($result);
46 foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
47 if (!$this->isInlineField($fieldConfig)) {
48 continue;
49 }
50 $childTableName = $fieldConfig['config']['foreign_table'] ?? '';
51 if (!in_array($childTableName, ['site_errorhandling', 'site_language', 'site_route', 'site_base_variant'], true)) {
52 throw new \RuntimeException('Inline relation to other tables not implemented', 1522494737);
53 }
54 $result['processedTca']['columns'][$fieldName]['children'] = [];
55 $result = $this->resolveSiteRelatedChildren($result, $fieldName);
56 $result = $this->addForeignSelectorAndUniquePossibleRecords($result, $fieldName);
57 }
58
59 return $result;
60 }
61
62 /**
63 * Is column of type "inline"
64 *
65 * @param array $fieldConfig
66 * @return bool
67 */
68 protected function isInlineField(array $fieldConfig): bool
69 {
70 return !empty($fieldConfig['config']['type']) && $fieldConfig['config']['type'] === 'inline';
71 }
72
73 /**
74 * The "entry" pid for inline records. Nested inline records can potentially hang around on different
75 * pid's, but the entry pid is needed for AJAX calls, so that they would know where the action takes place on the page structure.
76 *
77 * @param array $result Incoming result
78 * @return array Modified result
79 * @todo: Find out when and if this is different from 'effectivePid'
80 */
81 protected function addInlineFirstPid(array $result): array
82 {
83 if ($result['inlineFirstPid'] === null) {
84 $table = $result['tableName'];
85 $row = $result['databaseRow'];
86 // If the parent is a page, use the uid(!) of the (new?) page as pid for the child records:
87 if ($table === 'pages') {
88 $liveVersionId = BackendUtility::getLiveVersionIdOfRecord('pages', $row['uid']);
89 $pid = $liveVersionId ?? $row['uid'];
90 } elseif ($row['pid'] < 0) {
91 $prevRec = BackendUtility::getRecord($table, abs($row['pid']));
92 $pid = $prevRec['pid'];
93 } else {
94 $pid = $row['pid'];
95 }
96 $pageRecord = BackendUtility::getRecord('pages', $pid);
97 if ((int)$pageRecord[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] > 0) {
98 $pid = (int)$pageRecord[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']];
99 }
100 $result['inlineFirstPid'] = (int)$pid;
101 }
102 return $result;
103 }
104
105 /**
106 * Substitute the value in databaseRow of this inline field with an array
107 * that contains the databaseRows of currently connected records and some meta information.
108 *
109 * @param array $result Result array
110 * @param string $fieldName Current handle field name
111 * @return array Modified item array
112 */
113 protected function resolveSiteRelatedChildren(array $result, string $fieldName): array
114 {
115 $connectedUids = [];
116 if ($result['command'] === 'edit') {
117 $siteConfigurationForPageUid = (int)$result['databaseRow']['rootPageId'][0];
118 $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
119 try {
120 $site = $siteFinder->getSiteByRootPageId($siteConfigurationForPageUid);
121 } catch (SiteNotFoundException $e) {
122 $site = null;
123 }
124 $siteConfiguration = $site ? $site->getConfiguration() : [];
125 if (is_array($siteConfiguration[$fieldName])) {
126 $connectedUids = array_keys($siteConfiguration[$fieldName]);
127 }
128 }
129
130 // If we are dealing with site_language, we *always* force a relation to sys_language "0"
131 $foreignTable = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
132 if ($foreignTable === 'site_language' && $result['command'] === 'new') {
133 // If new, just add a new default child
134 $child = $this->compileDefaultSysSiteLanguageChild($result, $fieldName);
135 $connectedUids[] = $child['databaseRow']['uid'];
136 $result['processedTca']['columns'][$fieldName]['children'][] = $child;
137 }
138
139 $result['databaseRow'][$fieldName] = implode(',', $connectedUids);
140 if ($result['inlineCompileExistingChildren']) {
141 foreach ($connectedUids as $uid) {
142 if (strpos((string)$uid, 'NEW') !== 0) {
143 $compiledChild = $this->compileChild($result, $fieldName, $uid);
144 $result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
145 }
146 }
147 }
148
149 // If we are dealing with ite_language, we *always* force a relation to sys_language "0"
150 if ($foreignTable === 'site_language' && $result['command'] === 'edit') {
151 // If edit, find out if a child using sys_language "0" exists, else add it on top
152 $defaultSysSiteLanguageChildFound = false;
153 foreach ($result['processedTca']['columns'][$fieldName]['children'] as $child) {
154 if (isset($child['databaseRow']['languageId']) && (int)$child['databaseRow']['languageId'][0] == 0) {
155 $defaultSysSiteLanguageChildFound = true;
156 }
157 }
158 if (!$defaultSysSiteLanguageChildFound) {
159 // Compile and add child as first child
160 $child = $this->compileDefaultSysSiteLanguageChild($result, $fieldName);
161 $result['databaseRow'][$fieldName] = $child['databaseRow']['uid'] . ',' . $result['databaseRow'][$fieldName];
162 array_unshift($result['processedTca']['columns'][$fieldName]['children'], $child);
163 }
164 }
165
166 return $result;
167 }
168
169 /**
170 * If there is a foreign_selector or foreign_unique configuration, fetch
171 * the list of possible records that can be connected and attach them to the
172 * inline configuration.
173 *
174 * @param array $result Result array
175 * @param string $fieldName Current handle field name
176 * @return array Modified item array
177 */
178 protected function addForeignSelectorAndUniquePossibleRecords(array $result, string $fieldName): array
179 {
180 if (!is_array($result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'])) {
181 return $result;
182 }
183
184 $selectorOrUniqueConfiguration = $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'];
185 $foreignFieldName = $selectorOrUniqueConfiguration['fieldName'];
186 $selectorOrUniquePossibleRecords = [];
187
188 if ($selectorOrUniqueConfiguration['config']['type'] === 'select') {
189 // Compile child table data for this field only
190 $selectDataInput = [
191 'tableName' => $result['processedTca']['columns'][$fieldName]['config']['foreign_table'],
192 'command' => 'new',
193 // Since there is no existing record that may have a type, it does not make sense to
194 // do extra handling of pageTsConfig merged here. Just provide "parent" pageTS as is
195 'pageTsConfig' => $result['pageTsConfig'],
196 'userTsConfig' => $result['userTsConfig'],
197 'databaseRow' => $result['databaseRow'],
198 'processedTca' => [
199 'ctrl' => [],
200 'columns' => [
201 $foreignFieldName => [
202 'config' => $selectorOrUniqueConfiguration['config'],
203 ],
204 ],
205 ],
206 'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
207 ];
208 $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
209 $formDataGroup->setProviderList([TcaSelectItems::class]);
210 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
211 $compilerResult = $formDataCompiler->compile($selectDataInput);
212 $selectorOrUniquePossibleRecords = $compilerResult['processedTca']['columns'][$foreignFieldName]['config']['items'];
213 }
214
215 $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniquePossibleRecords'] = $selectorOrUniquePossibleRecords;
216
217 return $result;
218 }
219
220 /**
221 * Compile a full child record
222 *
223 * @param array $result Result array of parent
224 * @param string $parentFieldName Name of parent field
225 * @param int $childUid Uid of child to compile
226 * @return array Full result array
227 */
228 protected function compileChild(array $result, string $parentFieldName, int $childUid): array
229 {
230 $parentConfig = $result['processedTca']['columns'][$parentFieldName]['config'];
231 $childTableName = $parentConfig['foreign_table'];
232
233 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
234 $inlineStackProcessor->initializeByGivenStructure($result['inlineStructure']);
235 $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
236
237 $formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class);
238 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
239 $formDataCompilerInput = [
240 'command' => 'edit',
241 'tableName' => $childTableName,
242 'vanillaUid' => $childUid,
243 // Give incoming returnUrl down to children so they generate a returnUrl back to
244 // the originally opening record, also see "originalReturnUrl" in inline container
245 // and FormInlineAjaxController
246 'returnUrl' => $result['returnUrl'],
247 'isInlineChild' => true,
248 'inlineStructure' => $result['inlineStructure'],
249 'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
250 'inlineFirstPid' => $result['inlineFirstPid'],
251 'inlineParentConfig' => $parentConfig,
252
253 // values of the current parent element
254 // it is always a string either an id or new...
255 'inlineParentUid' => $result['databaseRow']['uid'],
256 'inlineParentTableName' => $result['tableName'],
257 'inlineParentFieldName' => $parentFieldName,
258
259 // values of the top most parent element set on first level and not overridden on following levels
260 'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'],
261 'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'],
262 'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'],
263 ];
264
265 if ($parentConfig['foreign_selector'] && ($parentConfig['appearance']['useCombination'] ?? false)) {
266 throw new \RuntimeException('useCombination not implemented in sites module', 1522493097);
267 }
268 return $formDataCompiler->compile($formDataCompilerInput);
269 }
270
271 /**
272 * Compile default site_language child using sys_language uid "0"
273 *
274 * @param array $result
275 * @param string $parentFieldName
276 * @return array
277 */
278 protected function compileDefaultSysSiteLanguageChild(array $result, string $parentFieldName): array
279 {
280 $parentConfig = $result['processedTca']['columns'][$parentFieldName]['config'];
281 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
282 $inlineStackProcessor->initializeByGivenStructure($result['inlineStructure']);
283 $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
284 $formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class);
285 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
286 $formDataCompilerInput = [
287 'command' => 'new',
288 'tableName' => 'site_language',
289 'vanillaUid' => $result['inlineFirstPid'],
290 'returnUrl' => $result['returnUrl'],
291 'isInlineChild' => true,
292 'inlineStructure' => [],
293 'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
294 'inlineFirstPid' => $result['inlineFirstPid'],
295 'inlineParentConfig' => $parentConfig,
296 'inlineParentUid' => $result['databaseRow']['uid'],
297 'inlineParentTableName' => $result['tableName'],
298 'inlineParentFieldName' => $parentFieldName,
299 'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'],
300 'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'],
301 'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'],
302 // The sys_language uid 0
303 'inlineChildChildUid' => 0,
304 ];
305 return $formDataCompiler->compile($formDataCompilerInput);
306 }
307
308 /**
309 * @return BackendUserAuthentication
310 */
311 protected function getBackendUser(): BackendUserAuthentication
312 {
313 return $GLOBALS['BE_USER'];
314 }
315 }