‪TYPO3CMS  ‪main
SiteConfigurationController.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
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 
19 
20 use Psr\Http\Message\ResponseInterface;
21 use Psr\Http\Message\ServerRequestInterface;
33 use TYPO3\CMS\Backend\Utility\BackendUtility;
44 use TYPO3\CMS\Core\Imaging\IconSize;
51 use ‪TYPO3\CMS\Core\SysLog\Action\Site as SiteAction;
52 use ‪TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
58 
65 #[AsController]
67 {
68  public function ‪__construct(
69  protected readonly ‪SiteFinder $siteFinder,
70  protected readonly ‪IconFactory $iconFactory,
71  protected readonly ‪UriBuilder $uriBuilder,
72  protected readonly ‪ModuleTemplateFactory $moduleTemplateFactory,
73  private readonly ‪FormDataCompiler $formDataCompiler,
74  private readonly ‪PageRenderer $pageRenderer,
75  private readonly ‪SiteConfiguration $siteConfiguration,
76  private readonly ‪SiteWriter $siteWriter,
77  ) {}
78 
83  public function ‪overviewAction(ServerRequestInterface $request): ResponseInterface
84  {
85  // forcing uncached sites will re-initialize `SiteFinder`
86  // which is used later by FormEngine (implicit behavior)
87  $allSites = $this->siteFinder->getAllSites(false);
88  $pages = $this->‪getAllSitePages();
89  $unassignedSites = [];
90  $duplicatedRootPages = [];
91  foreach ($allSites as ‪$identifier => $site) {
92  $rootPageId = $site->getRootPageId();
93  if (isset($pages[$rootPageId]['siteConfiguration'])) {
94  // rootPage is already used in a site configuration
95  $duplicatedRootPages[$rootPageId][] = $pages[$rootPageId]['siteConfiguration']->getIdentifier();
96  $duplicatedRootPages[$rootPageId][] = $site->getIdentifier();
97  $duplicatedRootPages[$rootPageId] = array_unique($duplicatedRootPages[$rootPageId]);
98  }
99  if (isset($pages[$rootPageId])) {
100  $pages[$rootPageId]['siteIdentifier'] = ‪$identifier;
101  $pages[$rootPageId]['siteConfiguration'] = $site;
102  } else {
103  $unassignedSites[] = $site;
104  }
105  }
106 
107  $view = $this->moduleTemplateFactory->create($request);
108  $this->‪configureOverViewDocHeader($view, $request->getAttribute('normalizedParams')->getRequestUri());
109  $view->setTitle(
110  $this->‪getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf:mlang_tabs_tab')
111  );
112  $view->assignMultiple([
113  'pages' => $pages,
114  'unassignedSites' => $unassignedSites,
115  'duplicatedRootPages' => $duplicatedRootPages,
116  'duplicatedEntryPoints' => $this->‪getDuplicatedEntryPoints($allSites, $pages),
117  ]);
118  return $view->renderResponse('SiteConfiguration/Overview');
119  }
120 
126  public function ‪editAction(ServerRequestInterface $request): ResponseInterface
127  {
128  // forcing uncached sites will re-initialize `SiteFinder`
129  // which is used later by FormEngine (implicit behavior)
130  $allSites = $this->siteFinder->getAllSites(false);
131 
132  // Put site and friends TCA into global TCA
133  // @todo: We might be able to get rid of that later
134  ‪$GLOBALS['TCA'] = array_merge(‪$GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca());
135 
136  ‪$siteIdentifier = $request->getQueryParams()['site'] ?? null;
137  $pageUid = (int)($request->getQueryParams()['pageUid'] ?? 0);
138 
139  if (empty(‪$siteIdentifier) && empty($pageUid)) {
140  throw new \RuntimeException('Either site identifier to edit a config or page uid to add new config must be set', 1521561148);
141  }
142  $isNewConfig = empty(‪$siteIdentifier);
143 
144  $defaultValues = [];
145  if ($isNewConfig) {
146  $defaultValues['site']['rootPageId'] = $pageUid;
147  }
148 
149  if (!$isNewConfig && !isset($allSites[‪$siteIdentifier])) {
150  throw new \RuntimeException('Existing config for site ' . ‪$siteIdentifier . ' not found', 1521561226);
151  }
152 
153  $returnUrl = $this->uriBuilder->buildUriFromRoute('site_configuration');
154 
155  $formDataCompilerInput = [
156  'request' => $request,
157  'tableName' => 'site',
158  'vanillaUid' => $isNewConfig ? $pageUid : $allSites[‪$siteIdentifier]->getRootPageId(),
159  'command' => $isNewConfig ? 'new' : 'edit',
160  'returnUrl' => (string)$returnUrl,
161  'customData' => [
162  'siteIdentifier' => $isNewConfig ? '' : ‪$siteIdentifier,
163  ],
164  'defaultValues' => $defaultValues,
165  ];
166  $formData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(SiteConfigurationDataGroup::class));
167  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
168  $formData['renderType'] = 'outerWrapContainer';
169  $formResult = $nodeFactory->create($formData)->render();
170  // Needed to be set for 'onChange="reload"' and reload on type change to work
171  $formResult['doSaveFieldName'] = 'doSave';
172  $formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
173  $formResultCompiler->mergeResult($formResult);
174  $formResultCompiler->addCssFiles();
175 
176  $view = $this->moduleTemplateFactory->create($request);
177  $view->assignMultiple([
178  // Always add rootPageId as additional field to have a reference for new records
179  'rootPageId' => $isNewConfig ? $pageUid : $allSites[‪$siteIdentifier]->getRootPageId(),
180  'returnUrl' => $returnUrl,
181  'formEngineHtml' => $formResult['html'],
182  'formEngineFooter' => $formResultCompiler->printNeededJSFunctions(),
183  ]);
184 
185  $this->pageRenderer->getJavaScriptRenderer()->includeTaggedImports('backend.form');
186  $this->‪configureEditViewDocHeader($view);
187  $view->setTitle(
188  $this->‪getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf:mlang_tabs_tab'),
189  ‪$siteIdentifier ?? ''
190  );
191  return $view->renderResponse('SiteConfiguration/Edit');
192  }
193 
199  public function ‪saveAction(ServerRequestInterface $request): ResponseInterface
200  {
201  // loading uncached site configurations without settings.yaml
203  $mappingRootPageToSite = [];
204  $allSites = $this->siteConfiguration->resolveAllExistingSitesRaw();
205  foreach ($allSites as $site) {
206  $mappingRootPageToSite[$site->getRootPageId()] = $site;
207  }
208 
209  // Put site and friends TCA into global TCA
210  // @todo We might be able to get rid of that later
211  ‪$GLOBALS['TCA'] = array_merge(‪$GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca());
212 
213  $siteTca = GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca();
214 
215  $overviewRoute = $this->uriBuilder->buildUriFromRoute('site_configuration');
216  $parsedBody = $request->getParsedBody();
217  if (isset($parsedBody['closeDoc']) && (int)$parsedBody['closeDoc'] === 1) {
218  // Closing means no save, just redirect to overview
219  return new ‪RedirectResponse($overviewRoute);
220  }
221  $isSave = $parsedBody['_savedok'] ?? $parsedBody['doSave'] ?? false;
222  $isSaveClose = $parsedBody['_saveandclosedok'] ?? false;
223  if (!$isSave && !$isSaveClose) {
224  throw new \RuntimeException('Either save or save and close', 1520370364);
225  }
226 
227  if (!isset($parsedBody['data']['site']) || !is_array($parsedBody['data']['site'])) {
228  throw new \RuntimeException('No site data or site identifier given', 1521030950);
229  }
230 
231  $data = $parsedBody['data'];
232  // This can be NEW123 for new records
233  $pageId = (int)key($data['site']);
234  $sysSiteRow = current($data['site']);
235  ‪$siteIdentifier = $sysSiteRow['identifier'] ?? '';
236 
237  $isNewConfiguration = false;
238  $currentIdentifier = '';
239  if (isset($mappingRootPageToSite[$pageId])) {
240  $currentSite = $mappingRootPageToSite[$pageId];
241  $currentSiteConfiguration = $currentSite->getConfiguration();
242  $currentIdentifier = $currentSite->getIdentifier();
243  } else {
244  $currentSiteConfiguration = [];
245  $isNewConfiguration = true;
246  $pageId = (int)$parsedBody['rootPageId'];
247  if ($pageId <= 0) {
248  // Early validation of rootPageId - it must always be given and greater than 0
249  throw new \RuntimeException('No root page id found', 1521719709);
250  }
251  }
252 
253  // Validate site identifier and do not store or further process it
254  ‪$siteIdentifier = $this->‪validateAndProcessIdentifier($isNewConfiguration, ‪$siteIdentifier, $pageId, $allSites, $mappingRootPageToSite);
255  unset($sysSiteRow['identifier']);
256 
257  try {
258  $newSysSiteData = [];
259  // Hard set rootPageId: This is TCA readOnly and not transmitted by FormEngine, but is also the "uid" of the site record
260  $newSysSiteData['rootPageId'] = $pageId;
261  foreach ($sysSiteRow as $fieldName => $fieldValue) {
262  $type = $siteTca['site']['columns'][$fieldName]['config']['type'];
263  $renderType = $siteTca['site']['columns'][$fieldName]['config']['renderType'] ?? '';
264  switch ($type) {
265  case 'input':
266  case 'number':
267  case 'email':
268  case 'link':
269  case 'datetime':
270  case 'color':
271  case 'text':
272  $fieldValue = $this->‪validateAndProcessValue('site', $fieldName, $fieldValue);
273  $newSysSiteData[$fieldName] = $fieldValue;
274  break;
275 
276  case 'inline':
277  $newSysSiteData[$fieldName] = [];
278  $childRowIds = ‪GeneralUtility::trimExplode(',', $fieldValue, true);
279  if (!isset($siteTca['site']['columns'][$fieldName]['config']['foreign_table'])) {
280  throw new \RuntimeException('No foreign_table found for inline type', 1521555037);
281  }
282  $foreignTable = $siteTca['site']['columns'][$fieldName]['config']['foreign_table'];
283  foreach ($childRowIds as $childRowId) {
284  $childRowData = [];
285  if (!isset($data[$foreignTable][$childRowId])) {
286  if (!empty($currentSiteConfiguration[$fieldName][$childRowId])) {
287  // A collapsed inline record: Fetch data from existing config
288  $newSysSiteData[$fieldName][] = $currentSiteConfiguration[$fieldName][$childRowId];
289  continue;
290  }
291  throw new \RuntimeException('No data found for table ' . $foreignTable . ' with id ' . $childRowId, 1521555177);
292  }
293  $childRow = $data[$foreignTable][$childRowId];
294  foreach ($childRow as $childFieldName => $childFieldValue) {
295  if ($childFieldName === 'pid') {
296  // pid is added by inline by default, but not relevant for yml storage
297  continue;
298  }
299  $type = $siteTca[$foreignTable]['columns'][$childFieldName]['config']['type'];
300  switch ($type) {
301  case 'input':
302  case 'number':
303  case 'email':
304  case 'link':
305  case 'datetime':
306  case 'color':
307  case 'select':
308  case 'text':
309  $childRowData[$childFieldName] = $childFieldValue;
310  break;
311  case 'check':
312  $childRowData[$childFieldName] = (bool)$childFieldValue;
313  break;
314  default:
315  throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1521555340);
316  }
317  }
318  $newSysSiteData[$fieldName][] = $childRowData;
319  }
320  break;
321 
322  case 'siteLanguage':
323  if (!isset($siteTca['site_language'])) {
324  throw new \RuntimeException('Required foreign table site_language does not exist', 1624286811);
325  }
326  if (!isset($siteTca['site_language']['columns']['languageId'])
327  || ($siteTca['site_language']['columns']['languageId']['config']['type'] ?? '') !== 'select'
328  ) {
329  throw new \RuntimeException(
330  'Required foreign field languageId does not exist or is not of type select',
331  1624286812
332  );
333  }
334  $newSysSiteData[$fieldName] = [];
335  $lastLanguageId = $this->‪getLastLanguageId();
336  foreach (‪GeneralUtility::trimExplode(',', $fieldValue, true) as $childRowId) {
337  if (!isset($data['site_language'][$childRowId])) {
338  if (!empty($currentSiteConfiguration[$fieldName][$childRowId])) {
339  $newSysSiteData[$fieldName][] = $currentSiteConfiguration[$fieldName][$childRowId];
340  continue;
341  }
342  throw new \RuntimeException('No data found for table site_language with id ' . $childRowId, 1624286813);
343  }
344  $childRowData = [];
345  foreach ($data['site_language'][$childRowId] ?? [] as $childFieldName => $childFieldValue) {
346  if ($childFieldName === 'pid') {
347  // pid is added by default, but not relevant for yml storage
348  continue;
349  }
350  if ($childFieldName === 'languageId'
351  && (int)$childFieldValue === PHP_INT_MAX
352  && str_starts_with($childRowId, 'NEW')
353  ) {
354  // In case we deal with a new site language, whose "languageID" field is
355  // set to the PHP_INT_MAX placeholder, the next available language ID has
356  // to be used (auto-increment).
357  $childRowData[$childFieldName] = ++$lastLanguageId;
358  continue;
359  }
360  $type = $siteTca['site_language']['columns'][$childFieldName]['config']['type'];
361  switch ($type) {
362  case 'input':
363  case 'number':
364  case 'email':
365  case 'link':
366  case 'datetime':
367  case 'color':
368  case 'select':
369  case 'text':
370  $childRowData[$childFieldName] = $childFieldValue;
371  break;
372  case 'check':
373  $childRowData[$childFieldName] = (bool)$childFieldValue;
374  break;
375  default:
376  throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1624286814);
377  }
378  }
379  $newSysSiteData[$fieldName][] = $childRowData;
380  }
381  break;
382 
383  case 'select':
384  if ($renderType === 'selectMultipleSideBySide') {
385  $fieldValues = is_array($fieldValue) ? $fieldValue : ‪GeneralUtility::trimExplode(',', $fieldValue, true);
386  $newSysSiteData[$fieldName] = $fieldValues;
387  } else {
389  $fieldValue = (int)$fieldValue;
390  } elseif (is_array($fieldValue)) {
391  $fieldValue = implode(',', $fieldValue);
392  }
393  $newSysSiteData[$fieldName] = $fieldValue;
394  }
395 
396  break;
397 
398  case 'check':
399  $newSysSiteData[$fieldName] = (bool)$fieldValue;
400  break;
401 
402  default:
403  throw new \RuntimeException('TCA type "' . $type . '" is not implemented in site handling', 1521032781);
404  }
405  }
406 
407  $newSiteConfiguration = $this->‪validateFullStructure(
408  $this->‪getMergeSiteData($currentSiteConfiguration, $newSysSiteData),
409  $isNewConfiguration
410  );
411 
412  // Persist the configuration
413  try {
414  if (!$isNewConfiguration && $currentIdentifier !== ‪$siteIdentifier) {
415  $this->siteWriter->rename($currentIdentifier, ‪$siteIdentifier);
416  $this->‪getBackendUser()->writelog(‪Type::SITE, SiteAction::RENAME, SystemLogErrorClassification::MESSAGE, 0, 'Site configuration \'%s\' was renamed to \'%s\'.', [$currentIdentifier, ‪$siteIdentifier], 'site');
417  }
418  $this->siteWriter->write(‪$siteIdentifier, $newSiteConfiguration, true);
419  if ($isNewConfiguration) {
420  $this->‪getBackendUser()->writelog(‪Type::SITE, SiteAction::CREATE, SystemLogErrorClassification::MESSAGE, 0, 'Site configuration \'%s\' was created.', [‪$siteIdentifier], 'site');
421  } else {
422  $this->‪getBackendUser()->writelog(‪Type::SITE, SiteAction::UPDATE, SystemLogErrorClassification::MESSAGE, 0, 'Site configuration \'%s\' was updated.', [‪$siteIdentifier], 'site');
423  }
425  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $e->getMessage(), '', ContextualFeedbackSeverity::WARNING, true);
426  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
427  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
428  $defaultFlashMessageQueue->enqueue($flashMessage);
429  }
430  } catch (‪SiteValidationErrorException $e) {
431  // Do not store new config if a validation error is thrown, but redirect only to show a generated flash message
432  }
433 
434  $saveRoute = $this->uriBuilder->buildUriFromRoute('site_configuration.edit', ['site' => ‪$siteIdentifier]);
435  if ($isSaveClose) {
436  return new ‪RedirectResponse($overviewRoute);
437  }
438  return new ‪RedirectResponse($saveRoute);
439  }
440 
451  protected function ‪validateAndProcessIdentifier(bool $isNew, string ‪$identifier, int $rootPageId, array $allSites, array $mappingRootPageToSite)
452  {
453  $languageService = $this->‪getLanguageService();
454  // Normal "eval" processing of field first
455  $identifier = $this->‪validateAndProcessValue('site', 'identifier', $identifier);
456  if ($isNew) {
457  // Verify no other site with this identifier exists. If so, find a new unique name as
458  // identifier and show a flash message the identifier has been adapted
459  if (($allSites[‪$identifier] ?? null) instanceof ‪Site) {
460  // Force this identifier to be unique
461  $originalIdentifier = ‪$identifier;
463  $message = sprintf(
464  $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.message'),
465  $originalIdentifier,
467  );
468  $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.title');
469  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, ContextualFeedbackSeverity::WARNING, true);
470  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
471  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
472  $defaultFlashMessageQueue->enqueue($flashMessage);
473  }
474  } else {
475  // If this is an existing config, the site for this identifier must have the same rootPageId, otherwise
476  // a user tried to rename a site identifier to a different site that already exists. If so, we do not rename
477  // the site and show a flash message
478  $site = ($allSites[‪$identifier] ?? null);
479  if ($site instanceof ‪Site
480  && $site->‪getRootPageId() !== $rootPageId
481  && ($mappingRootPageToSite[$rootPageId] ?? null) instanceof ‪Site
482  ) {
483  // Find original value and keep this
484  $origSite = $mappingRootPageToSite[$rootPageId];
485  $originalIdentifier = ‪$identifier;
486  ‪$identifier = $origSite->getIdentifier();
487  $message = sprintf(
488  $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.message'),
489  $originalIdentifier,
491  );
492  $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.title');
493  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, ContextualFeedbackSeverity::WARNING, true);
494  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
495  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
496  $defaultFlashMessageQueue->enqueue($flashMessage);
497  }
498  }
499  return ‪$identifier;
500  }
501 
514  protected function ‪validateAndProcessValue(string $tableName, string $fieldName, $fieldValue)
515  {
516  $languageService = $this->‪getLanguageService();
517  $fieldConfig = ‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
518  $handledEvals = [];
519 
520  if (!$this->‪validateValueForRequired($fieldConfig, $fieldValue)) {
521  // Validation throws - these should be handled client side already,
522  // eg. 'required' being set and receiving empty, shouldn't happen server side
523  $message = sprintf(
524  $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.message'),
525  $fieldName
526  );
527  $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.title');
528  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, ContextualFeedbackSeverity::WARNING, true);
529  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
530  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
531  $defaultFlashMessageQueue->enqueue($flashMessage);
533  'Field ' . $fieldName . ' is set to required, but received empty.',
534  1521726421
535  );
536  }
537 
538  if (!empty($fieldConfig['eval'])) {
539  $evalArray = ‪GeneralUtility::trimExplode(',', $fieldConfig['eval'], true);
540  // Processing
541  if (in_array('alphanum_x', $evalArray, true)) {
542  $handledEvals[] = 'alphanum_x';
543  $fieldValue = preg_replace('/[^a-zA-Z0-9_-]/', '', $fieldValue);
544  }
545  if (in_array('lower', $evalArray, true)) {
546  $handledEvals[] = 'lower';
547  $fieldValue = mb_strtolower($fieldValue, 'utf-8');
548  }
549  if (in_array('trim', $evalArray, true)) {
550  $handledEvals[] = 'trim';
551  $fieldValue = trim($fieldValue);
552  }
553  if (in_array('int', $evalArray, true)) {
554  $handledEvals[] = 'int';
555  $fieldValue = (int)$fieldValue;
556  }
557  if (!empty(array_diff($evalArray, $handledEvals))) {
558  throw new \RuntimeException('At least one not implemented \'eval\' in list ' . $fieldConfig['eval'], 1522491734);
559  }
560  }
561  if (isset($fieldConfig['range']['lower'])) {
562  $fieldValue = (int)$fieldValue < (int)$fieldConfig['range']['lower'] ? (int)$fieldConfig['range']['lower'] : (int)$fieldValue;
563  }
564  if (isset($fieldConfig['range']['upper'])) {
565  $fieldValue = (int)$fieldValue > (int)$fieldConfig['range']['upper'] ? (int)$fieldConfig['range']['upper'] : (int)$fieldValue;
566  }
567  return $fieldValue;
568  }
569 
579  protected function ‪validateFullStructure(array $newSysSiteData, bool $isNewConfiguration): array
580  {
581  $languageService = $this->‪getLanguageService();
582  // Verify there are not two error handlers with the same error code
583  if (isset($newSysSiteData['errorHandling']) && is_array($newSysSiteData['errorHandling'])) {
584  $uniqueCriteria = [];
585  $validChildren = [];
586  foreach ($newSysSiteData['errorHandling'] as $child) {
587  if (!isset($child['errorCode'])) {
588  throw new \RuntimeException('No errorCode found', 1521788518);
589  }
590  if (!in_array((int)$child['errorCode'], $uniqueCriteria, true)) {
591  $uniqueCriteria[] = (int)$child['errorCode'];
592  $child['errorCode'] = (int)$child['errorCode'];
593  $validChildren[] = $child;
594  } else {
595  $message = sprintf(
596  $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.message'),
597  $child['errorCode']
598  );
599  $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.title');
600  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, ContextualFeedbackSeverity::WARNING, true);
601  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
602  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
603  $defaultFlashMessageQueue->enqueue($flashMessage);
604  }
605  }
606  $newSysSiteData['errorHandling'] = $validChildren;
607  }
608 
609  // Verify there is at least one site_language element configured.
610  if (!isset($newSysSiteData['languages']) || !is_array($newSysSiteData['languages']) || count($newSysSiteData['languages']) < 1) {
611  throw new \RuntimeException(
612  'No default language definition found. The interface does not allow this. Aborting',
613  1521789306
614  );
615  }
616  $uniqueCriteria = [];
617  $validChildren = [];
618  foreach ($newSysSiteData['languages'] as $child) {
619  if (!isset($child['languageId'])) {
620  throw new \RuntimeException('languageId not found', 1521789455);
621  }
622  if (!in_array((int)$child['languageId'], $uniqueCriteria, true)) {
623  $uniqueCriteria[] = (int)$child['languageId'];
624  $child['languageId'] = (int)$child['languageId'];
625  $validChildren[] = $child;
626  } else {
627  $message = sprintf(
628  $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.title'),
629  $child['languageId']
630  );
631  $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.title');
632  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, ContextualFeedbackSeverity::WARNING, true);
633  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
634  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
635  $defaultFlashMessageQueue->enqueue($flashMessage);
636  }
637  }
638  // On new site configurations, ensure that the only existing language has the languageId set to 0
639  // @todo: this shouldn't be done here, but rather properly handled in saveAction() where 'siteLanguage' is handled
640  if ($isNewConfiguration && count($validChildren) === 1) {
641  $validChildren[0]['languageId'] = 0;
642  }
643  $newSysSiteData['languages'] = $validChildren;
644 
645  // cleanup configuration
646  foreach ($newSysSiteData as ‪$identifier => $value) {
647  if (is_array($value) && empty($value)) {
648  unset($newSysSiteData[‪$identifier]);
649  }
650  }
651 
652  return $newSysSiteData;
653  }
654 
658  public function ‪deleteAction(ServerRequestInterface $request): ResponseInterface
659  {
660  ‪$siteIdentifier = $request->getParsedBody()['site'] ?? '';
661  if (empty(‪$siteIdentifier)) {
662  throw new \RuntimeException('Not site identifier given', 1521565182);
663  }
664  try {
665  // Verify site does exist, method throws if not
666  $this->siteWriter->delete(‪$siteIdentifier);
667  $this->‪getBackendUser()->writelog(‪Type::SITE, SiteAction::DELETE, SystemLogErrorClassification::MESSAGE, 0, 'Site configuration \'%s\' was deleted.', [‪$siteIdentifier], 'site');
669  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $e->getMessage(), '', ContextualFeedbackSeverity::WARNING, true);
670  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
671  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
672  $defaultFlashMessageQueue->enqueue($flashMessage);
673  }
674  $overviewRoute = $this->uriBuilder->buildUriFromRoute('site_configuration');
675  return new ‪RedirectResponse($overviewRoute);
676  }
677 
681  protected function ‪configureEditViewDocHeader(‪ModuleTemplate $view): void
682  {
683  $buttonBar = $view->‪getDocHeaderComponent()->getButtonBar();
684  $lang = $this->‪getLanguageService();
685  $closeButton = $buttonBar->makeLinkButton()
686  ->setHref('#')
687  ->setClasses('t3js-editform-close')
688  ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
689  ->setShowLabelText(true)
690  ->setIcon($this->iconFactory->getIcon('actions-close', IconSize::SMALL));
691  $saveButton = $buttonBar->makeInputButton()
692  ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'))
693  ->setName('_savedok')
694  ->setValue('1')
695  ->setShowLabelText(true)
696  ->setForm('siteConfigurationController')
697  ->setIcon($this->iconFactory->getIcon('actions-document-save', IconSize::SMALL));
698  $buttonBar->addButton($closeButton);
699  $buttonBar->addButton($saveButton, ‪ButtonBar::BUTTON_POSITION_LEFT, 2);
700  }
701 
705  protected function ‪configureOverViewDocHeader(‪ModuleTemplate $view, string $requestUri): void
706  {
707  $buttonBar = $view->‪getDocHeaderComponent()->getButtonBar();
708  $reloadButton = $buttonBar->makeLinkButton()
709  ->setHref($requestUri)
710  ->setTitle($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
711  ->setIcon($this->iconFactory->getIcon('actions-refresh', IconSize::SMALL));
712  $buttonBar->addButton($reloadButton, ‪ButtonBar::BUTTON_POSITION_RIGHT);
713  $shortcutButton = $buttonBar->makeShortcutButton()
714  ->setRouteIdentifier('site_configuration')
715  ->setDisplayName($this->‪getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf:mlang_labels_tablabel'));
716  $buttonBar->addButton($shortcutButton, ‪ButtonBar::BUTTON_POSITION_RIGHT);
717  }
718 
723  protected function ‪getAllSitePages(): array
724  {
725  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
726  $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
727  $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, 0));
728  $statement = $queryBuilder
729  ->select('*')
730  ->from('pages')
731  ->where(
732  $queryBuilder->expr()->eq('sys_language_uid', 0),
733  $queryBuilder->expr()->or(
734  $queryBuilder->expr()->and(
735  $queryBuilder->expr()->eq('pid', 0),
736  $queryBuilder->expr()->notIn('doktype', [
740  ])
741  ),
742  $queryBuilder->expr()->eq('is_siteroot', 1)
743  )
744  )
745  ->orderBy('pid')
746  ->addOrderBy('sorting')
747  ->executeQuery();
748 
749  $pages = [];
750  while ($row = $statement->fetchAssociative()) {
751  $row['rootline'] = BackendUtility::BEgetRootLine((int)$row['uid']);
752  array_pop($row['rootline']);
753  $row['rootline'] = array_reverse($row['rootline']);
754  $pages[(int)$row['uid']] = $row;
755  }
756  return $pages;
757  }
758 
764  protected function ‪getDuplicatedEntryPoints(array $allSites, array $pages): array
765  {
766  $duplicatedEntryPoints = [];
767 
768  foreach ($allSites as $site) {
769  if (!isset($pages[$site->getRootPageId()])) {
770  continue;
771  }
772  foreach ($site->getAllLanguages() as $language) {
773  $base = $language->getBase();
774  $entryPoint = rtrim((string)$language->getBase(), '/');
775  $scheme = $base->getScheme() ? $base->getScheme() . '://' : '//';
776  $entryPointWithoutScheme = str_replace($scheme, '', $entryPoint);
777  if (!isset($duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint])) {
778  $duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint] = 1;
779  } else {
780  $duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint]++;
781  }
782  }
783  }
784  return array_filter($duplicatedEntryPoints, static function (array $variants): bool {
785  return count($variants) > 1 || reset($variants) > 1;
786  }, ARRAY_FILTER_USE_BOTH);
787  }
788 
792  protected function ‪getLastLanguageId(): int
793  {
794  $lastLanguageId = 0;
795  foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) {
796  foreach ($site->getAllLanguages() as $language) {
797  if ($language->getLanguageId() > $lastLanguageId) {
798  $lastLanguageId = $language->getLanguageId();
799  }
800  }
801  }
802  return $lastLanguageId;
803  }
804 
810  protected function ‪validateValueForRequired(array $tcaFieldConfig, mixed $value): bool
811  {
812  if (!($tcaFieldConfig['required'] ?? false)) {
813  return true;
814  }
815 
816  return !empty($value) || $value === '0';
817  }
818 
828  protected function ‪getMergeSiteData(array $currentSiteConfiguration, array $newSysSiteData): array
829  {
830  $newSysSiteData = array_merge($currentSiteConfiguration, $newSysSiteData);
831 
832  // @todo: this should go away, once base variants for languages are managable via the GUI.
833  $existingLanguageConfigurationsWithBaseVariants = [];
834  $existingLanguagesWithLegacyProperties = [];
835  foreach ($currentSiteConfiguration['languages'] ?? [] as $languageConfiguration) {
836  if (isset($languageConfiguration['baseVariants'])) {
837  $existingLanguageConfigurationsWithBaseVariants[$languageConfiguration['languageId']] = $languageConfiguration['baseVariants'];
838  }
839  if (isset($languageConfiguration['typo3Language'])) {
840  $existingLanguagesWithLegacyProperties[$languageConfiguration['languageId']]['typo3Language'] = $languageConfiguration['typo3Language'];
841  }
842  if (isset($languageConfiguration['iso-639-1'])) {
843  $existingLanguagesWithLegacyProperties[$languageConfiguration['languageId']]['iso-639-1'] = $languageConfiguration['iso-639-1'];
844  }
845  if (isset($languageConfiguration['direction'])) {
846  $existingLanguagesWithLegacyProperties[$languageConfiguration['languageId']]['direction'] = $languageConfiguration['direction'];
847  }
848  }
849  foreach ($newSysSiteData['languages'] ?? [] as $key => $languageConfiguration) {
850  $languageId = $languageConfiguration['languageId'];
851  if (isset($existingLanguageConfigurationsWithBaseVariants[$languageId])) {
852  $newSysSiteData['languages'][$key]['baseVariants'] = $existingLanguageConfigurationsWithBaseVariants[$languageId];
853  }
854  foreach ($existingLanguagesWithLegacyProperties[$languageId] ?? [] as $propertyName => $propertyValue) {
855  $newSysSiteData['languages'][$key][$propertyName] = $propertyValue;
856  }
857  }
858 
859  return $newSysSiteData;
860  }
861 
863  {
864  return ‪$GLOBALS['LANG'];
865  }
866 
868  {
869  return ‪$GLOBALS['BE_USER'];
870  }
871 }
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\configureEditViewDocHeader
‪configureEditViewDocHeader(ModuleTemplate $view)
Definition: SiteConfigurationController.php:681
‪TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
Definition: HiddenRestriction.php:27
‪TYPO3\CMS\Backend\Configuration\SiteTcaConfiguration
Definition: SiteTcaConfiguration.php:34
‪TYPO3\CMS\Core\SysLog\Action\Site
Definition: Site.php:24
‪TYPO3\CMS\Backend\Form\FormResultCompiler
Definition: FormResultCompiler.php:34
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\deleteAction
‪deleteAction(ServerRequestInterface $request)
Definition: SiteConfigurationController.php:658
‪TYPO3\CMS\Backend\Template\Components\ButtonBar\BUTTON_POSITION_LEFT
‪const BUTTON_POSITION_LEFT
Definition: ButtonBar.php:37
‪TYPO3\CMS\Core\Configuration\Exception\SiteConfigurationWriteException
Definition: SiteConfigurationWriteException.php:27
‪TYPO3\CMS\Backend\Template\Components\ButtonBar
Definition: ButtonBar.php:33
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\validateAndProcessValue
‪mixed validateAndProcessValue(string $tableName, string $fieldName, $fieldValue)
Definition: SiteConfigurationController.php:514
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\getBackendUser
‪getBackendUser()
Definition: SiteConfigurationController.php:867
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\configureOverViewDocHeader
‪configureOverViewDocHeader(ModuleTemplate $view, string $requestUri)
Definition: SiteConfigurationController.php:705
‪TYPO3\CMS\Backend\Template\ModuleTemplateFactory
Definition: ModuleTemplateFactory.php:35
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\validateValueForRequired
‪validateValueForRequired(array $tcaFieldConfig, mixed $value)
Definition: SiteConfigurationController.php:810
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\saveAction
‪saveAction(ServerRequestInterface $request)
Definition: SiteConfigurationController.php:199
‪TYPO3\CMS\Core\SysLog\Type\SITE
‪const SITE
Definition: Type.php:34
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_LINK
‪const DOKTYPE_LINK
Definition: PageRepository.php:99
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\getLastLanguageId
‪getLastLanguageId()
Definition: SiteConfigurationController.php:792
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Core\Imaging\IconFactory
Definition: IconFactory.php:35
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\overviewAction
‪overviewAction(ServerRequestInterface $request)
Definition: SiteConfigurationController.php:83
‪TYPO3\CMS\Core\Configuration\SiteConfiguration
Definition: SiteConfiguration.php:47
‪TYPO3\CMS\Backend\Template\ModuleTemplate
Definition: ModuleTemplate.php:46
‪TYPO3\CMS\Core\Site\Entity\Site
Definition: Site.php:42
‪TYPO3\CMS\Core\Type\ContextualFeedbackSeverity
‪ContextualFeedbackSeverity
Definition: ContextualFeedbackSeverity.php:25
‪TYPO3\CMS\Core\Site\Entity\Site\getRootPageId
‪getRootPageId()
Definition: Site.php:201
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:74
‪TYPO3\CMS\Core\Page\PageRenderer
Definition: PageRenderer.php:45
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\validateFullStructure
‪array validateFullStructure(array $newSysSiteData, bool $isNewConfiguration)
Definition: SiteConfigurationController.php:579
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController
Definition: SiteConfigurationController.php:67
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:44
‪TYPO3\CMS\Backend\Exception\SiteValidationErrorException
Definition: SiteValidationErrorException.php:25
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\getLanguageService
‪getLanguageService()
Definition: SiteConfigurationController.php:862
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_SPACER
‪const DOKTYPE_SPACER
Definition: PageRepository.php:103
‪TYPO3\CMS\Core\SysLog\Error
Definition: Error.php:24
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_SYSFOLDER
‪const DOKTYPE_SYSFOLDER
Definition: PageRepository.php:104
‪TYPO3\CMS\Core\Configuration\SiteWriter
Definition: SiteWriter.php:39
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\validateAndProcessIdentifier
‪mixed validateAndProcessIdentifier(bool $isNew, string $identifier, int $rootPageId, array $allSites, array $mappingRootPageToSite)
Definition: SiteConfigurationController.php:451
‪TYPO3\CMS\Core\Http\RedirectResponse
Definition: RedirectResponse.php:30
‪TYPO3\CMS\Backend\Form\NodeFactory
Definition: NodeFactory.php:39
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\__construct
‪__construct(protected readonly SiteFinder $siteFinder, protected readonly IconFactory $iconFactory, protected readonly UriBuilder $uriBuilder, protected readonly ModuleTemplateFactory $moduleTemplateFactory, private readonly FormDataCompiler $formDataCompiler, private readonly PageRenderer $pageRenderer, private readonly SiteConfiguration $siteConfiguration, private readonly SiteWriter $siteWriter,)
Definition: SiteConfigurationController.php:68
‪TYPO3\CMS\Backend\Template\ModuleTemplate\getDocHeaderComponent
‪getDocHeaderComponent()
Definition: ModuleTemplate.php:181
‪TYPO3\CMS\Core\Messaging\FlashMessage
Definition: FlashMessage.php:27
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\editAction
‪editAction(ServerRequestInterface $request)
Definition: SiteConfigurationController.php:126
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Backend\Attribute\AsController
Definition: AsController.php:25
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Webhooks\Message\$siteIdentifier
‪identifier readonly int readonly array readonly string readonly string $siteIdentifier
Definition: PageModificationMessage.php:38
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:69
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\getDuplicatedEntryPoints
‪getDuplicatedEntryPoints(array $allSites, array $pages)
Definition: SiteConfigurationController.php:764
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Backend\Form\FormDataCompiler
Definition: FormDataCompiler.php:26
‪TYPO3\CMS\Core\Utility\StringUtility
Definition: StringUtility.php:24
‪TYPO3\CMS\Backend\Template\Components\ButtonBar\BUTTON_POSITION_RIGHT
‪const BUTTON_POSITION_RIGHT
Definition: ButtonBar.php:42
‪TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup
Definition: SiteConfigurationDataGroup.php:33
‪TYPO3\CMS\Backend\Controller
Definition: AboutController.php:18
‪TYPO3\CMS\Core\Messaging\FlashMessageService
Definition: FlashMessageService.php:27
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\Utility\StringUtility\getUniqueId
‪static getUniqueId(string $prefix='')
Definition: StringUtility.php:57
‪TYPO3\CMS\Core\SysLog\Type
Definition: Type.php:28
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\getMergeSiteData
‪getMergeSiteData(array $currentSiteConfiguration, array $newSysSiteData)
Definition: SiteConfigurationController.php:828
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\getAllSitePages
‪getAllSitePages()
Definition: SiteConfigurationController.php:723
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:39