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