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