‪TYPO3CMS  11.5
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;
32 use TYPO3\CMS\Backend\Utility\BackendUtility;
48 use TYPO3\CMS\Core\Page\PageRenderer;
51 use ‪TYPO3\CMS\Core\SysLog\Action\Site as SiteAction;
52 use ‪TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
58 use TYPO3Fluid\Fluid\View\ViewInterface;
59 
68 {
69  protected const ‪ALLOWED_ACTIONS = ['overview', 'edit', 'save', 'delete'];
70 
74  protected ‪$moduleTemplate;
75 
79  protected ‪$view;
80 
83  protected PageRenderer ‪$pageRenderer;
84  protected ‪UriBuilder ‪$uriBuilder;
86 
87  public function ‪__construct(
90  PageRenderer ‪$pageRenderer,
93  ) {
94  $this->siteFinder = ‪$siteFinder;
95  $this->iconFactory = ‪$iconFactory;
96  $this->pageRenderer = ‪$pageRenderer;
97  $this->uriBuilder = ‪$uriBuilder;
98  $this->moduleTemplateFactory = ‪$moduleTemplateFactory;
99  }
100 
107  public function ‪handleRequest(ServerRequestInterface $request): ResponseInterface
108  {
109  $this->moduleTemplate = $this->moduleTemplateFactory->create($request);
110  // forcing uncached sites will re-initialize `SiteFinder`
111  // which is used later by FormEngine (implicit behavior)
112  $this->siteFinder->getAllSites(false);
113  $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
114  $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
115  $action = $request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'overview';
116 
117  if (!in_array($action, self::ALLOWED_ACTIONS, true)) {
118  return new ‪HtmlResponse('Action not allowed', 400);
119  }
120 
121  $this->‪initializeView($action);
122 
123  $result = $this->{$action . 'Action'}($request);
124  if ($result instanceof ResponseInterface) {
125  return $result;
126  }
127  $this->moduleTemplate->setContent($this->view->render());
128  return new HtmlResponse($this->moduleTemplate->renderContent());
129  }
130 
137  protected function ‪overviewAction(ServerRequestInterface $request): void
138  {
139  $this->‪configureOverViewDocHeader($request->getAttribute('normalizedParams')->getRequestUri());
140  $allSites = $this->siteFinder->getAllSites();
141  $pages = $this->‪getAllSitePages();
142  $unassignedSites = [];
143  foreach ($allSites as $identifier => $site) {
144  $rootPageId = $site->getRootPageId();
145  if (isset($pages[$rootPageId])) {
146  $pages[$rootPageId]['siteIdentifier'] = $identifier;
147  $pages[$rootPageId]['siteConfiguration'] = $site;
148  } else {
149  $unassignedSites[] = $site;
150  }
151  }
152 
153  $this->moduleTemplate->setTitle(
154  $this->‪getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf:mlang_tabs_tab')
155  );
156  $this->view->assignMultiple([
157  'pages' => $pages,
158  'unassignedSites' => $unassignedSites,
159  'duplicatedEntryPoints' => $this->‪getDuplicatedEntryPoints($allSites, $pages),
160  ]);
161  }
162 
169  protected function ‪editAction(ServerRequestInterface $request): void
170  {
172 
173  // Put site and friends TCA into global TCA
174  // @todo: We might be able to get rid of that later
175  ‪$GLOBALS['TCA'] = array_merge(‪$GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca());
176 
177  $siteIdentifier = $request->getQueryParams()['site'] ?? null;
178  $pageUid = (int)($request->getQueryParams()['pageUid'] ?? 0);
179 
180  if (empty($siteIdentifier) && empty($pageUid)) {
181  throw new \RuntimeException('Either site identifier to edit a config or page uid to add new config must be set', 1521561148);
182  }
183  $isNewConfig = empty($siteIdentifier);
184 
185  $defaultValues = [];
186  if ($isNewConfig) {
187  $defaultValues['site']['rootPageId'] = $pageUid;
188  }
189 
190  $allSites = $this->siteFinder->getAllSites();
191  if (!$isNewConfig && !isset($allSites[$siteIdentifier])) {
192  throw new \RuntimeException('Existing config for site ' . $siteIdentifier . ' not found', 1521561226);
193  }
194 
195  $returnUrl = $this->uriBuilder->buildUriFromRoute('site_configuration');
196 
197  $formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class);
198  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
199  $formDataCompilerInput = [
200  'tableName' => 'site',
201  'vanillaUid' => $isNewConfig ? $pageUid : $allSites[$siteIdentifier]->getRootPageId(),
202  'command' => $isNewConfig ? 'new' : 'edit',
203  'returnUrl' => (string)$returnUrl,
204  'customData' => [
205  'siteIdentifier' => $isNewConfig ? '' : $siteIdentifier,
206  ],
207  'defaultValues' => $defaultValues,
208  ];
209  $formData = $formDataCompiler->compile($formDataCompilerInput);
210  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
211  $formData['renderType'] = 'outerWrapContainer';
212  $formResult = $nodeFactory->create($formData)->render();
213  // Needed to be set for 'onChange="reload"' and reload on type change to work
214  $formResult['doSaveFieldName'] = 'doSave';
215  $formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
216  $formResultCompiler->mergeResult($formResult);
217  $formResultCompiler->addCssFiles();
218  // Always add rootPageId as additional field to have a reference for new records
219  $this->view->assign('rootPageId', $isNewConfig ? $pageUid : $allSites[$siteIdentifier]->getRootPageId());
220  $this->view->assign('returnUrl', $returnUrl);
221  $this->view->assign('formEngineHtml', $formResult['html']);
222  $this->view->assign('formEngineFooter', $formResultCompiler->printNeededJSFunctions());
223 
224  $this->moduleTemplate->setTitle(
225  $this->‪getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf:mlang_tabs_tab'),
226  $siteIdentifier ?? ''
227  );
228  }
229 
237  protected function ‪saveAction(ServerRequestInterface $request): ResponseInterface
238  {
239  // Put site and friends TCA into global TCA
240  // @todo: We might be able to get rid of that later
241  ‪$GLOBALS['TCA'] = array_merge(‪$GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca());
242 
243  $siteTca = GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca();
244 
245  $overviewRoute = $this->uriBuilder->buildUriFromRoute('site_configuration', ['action' => 'overview']);
246  $parsedBody = $request->getParsedBody();
247  if (isset($parsedBody['closeDoc']) && (int)$parsedBody['closeDoc'] === 1) {
248  // Closing means no save, just redirect to overview
249  return new RedirectResponse($overviewRoute);
250  }
251  $isSave = $parsedBody['_savedok'] ?? $parsedBody['doSave'] ?? false;
252  $isSaveClose = $parsedBody['_saveandclosedok'] ?? false;
253  if (!$isSave && !$isSaveClose) {
254  throw new \RuntimeException('Either save or save and close', 1520370364);
255  }
256 
257  if (!isset($parsedBody['data']['site']) || !is_array($parsedBody['data']['site'])) {
258  throw new \RuntimeException('No site data or site identifier given', 1521030950);
259  }
260 
261  $data = $parsedBody['data'];
262  // This can be NEW123 for new records
263  $pageId = (int)key($data['site']);
264  $sysSiteRow = current($data['site']);
265  $siteIdentifier = $sysSiteRow['identifier'] ?? '';
266 
267  $isNewConfiguration = false;
268  $currentIdentifier = '';
269  try {
270  $currentSite = $this->siteFinder->getSiteByRootPageId($pageId);
271  $currentSiteConfiguration = $currentSite->getConfiguration();
272  $currentIdentifier = $currentSite->getIdentifier();
273  } catch (SiteNotFoundException $e) {
274  $currentSiteConfiguration = [];
275  $isNewConfiguration = true;
276  $pageId = (int)$parsedBody['rootPageId'];
277  if ($pageId <= 0) {
278  // Early validation of rootPageId - it must always be given and greater than 0
279  throw new \RuntimeException('No root page id found', 1521719709);
280  }
281  }
282 
283  // Validate site identifier and do not store or further process it
284  $siteIdentifier = $this->‪validateAndProcessIdentifier($isNewConfiguration, $siteIdentifier, $pageId);
285  unset($sysSiteRow['identifier']);
286 
287  try {
288  $newSysSiteData = [];
289  // Hard set rootPageId: This is TCA readOnly and not transmitted by FormEngine, but is also the "uid" of the site record
290  $newSysSiteData['rootPageId'] = $pageId;
291  foreach ($sysSiteRow as $fieldName => $fieldValue) {
292  $type = $siteTca['site']['columns'][$fieldName]['config']['type'];
293  switch ($type) {
294  case 'input':
295  case 'text':
296  $fieldValue = $this->‪validateAndProcessValue('site', $fieldName, $fieldValue);
297  $newSysSiteData[$fieldName] = $fieldValue;
298  break;
299 
300  case 'inline':
301  $newSysSiteData[$fieldName] = [];
302  $childRowIds = ‪GeneralUtility::trimExplode(',', $fieldValue, true);
303  if (!isset($siteTca['site']['columns'][$fieldName]['config']['foreign_table'])) {
304  throw new \RuntimeException('No foreign_table found for inline type', 1521555037);
305  }
306  $foreignTable = $siteTca['site']['columns'][$fieldName]['config']['foreign_table'];
307  foreach ($childRowIds as $childRowId) {
308  $childRowData = [];
309  if (!isset($data[$foreignTable][$childRowId])) {
310  if (!empty($currentSiteConfiguration[$fieldName][$childRowId])) {
311  // A collapsed inline record: Fetch data from existing config
312  $newSysSiteData[$fieldName][] = $currentSiteConfiguration[$fieldName][$childRowId];
313  continue;
314  }
315  throw new \RuntimeException('No data found for table ' . $foreignTable . ' with id ' . $childRowId, 1521555177);
316  }
317  $childRow = $data[$foreignTable][$childRowId];
318  foreach ($childRow as $childFieldName => $childFieldValue) {
319  if ($childFieldName === 'pid') {
320  // pid is added by inline by default, but not relevant for yml storage
321  continue;
322  }
323  $type = $siteTca[$foreignTable]['columns'][$childFieldName]['config']['type'];
324  switch ($type) {
325  case 'input':
326  case 'select':
327  case 'text':
328  $childRowData[$childFieldName] = $childFieldValue;
329  break;
330  case 'check':
331  $childRowData[$childFieldName] = (bool)$childFieldValue;
332  break;
333  default:
334  throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1521555340);
335  }
336  }
337  $newSysSiteData[$fieldName][] = $childRowData;
338  }
339  break;
340 
341  case 'siteLanguage':
342  if (!isset($siteTca['site_language'])) {
343  throw new \RuntimeException('Required foreign table site_language does not exist', 1624286811);
344  }
345  if (!isset($siteTca['site_language']['columns']['languageId'])
346  || ($siteTca['site_language']['columns']['languageId']['config']['type'] ?? '') !== 'select'
347  ) {
348  throw new \RuntimeException(
349  'Required foreign field languageId does not exist or is not of type select',
350  1624286812
351  );
352  }
353  $newSysSiteData[$fieldName] = [];
354  $lastLanguageId = $this->‪getLastLanguageId();
355  foreach (‪GeneralUtility::trimExplode(',', $fieldValue, true) as $childRowId) {
356  if (!isset($data['site_language'][$childRowId])) {
357  if (!empty($currentSiteConfiguration[$fieldName][$childRowId])) {
358  $newSysSiteData[$fieldName][] = $currentSiteConfiguration[$fieldName][$childRowId];
359  continue;
360  }
361  throw new \RuntimeException('No data found for table site_language with id ' . $childRowId, 1624286813);
362  }
363  $childRowData = [];
364  foreach ($data['site_language'][$childRowId] ?? [] as $childFieldName => $childFieldValue) {
365  if ($childFieldName === 'pid') {
366  // pid is added by default, but not relevant for yml storage
367  continue;
368  }
369  if ($childFieldName === 'languageId'
370  && (int)$childFieldValue === PHP_INT_MAX
371  && str_starts_with($childRowId, 'NEW')
372  ) {
373  // In case we deal with a new site language, whose "languageID" field is
374  // set to the PHP_INT_MAX placeholder, the next available language ID has
375  // to be used (auto-increment).
376  $childRowData[$childFieldName] = ++$lastLanguageId;
377  continue;
378  }
379  $type = $siteTca['site_language']['columns'][$childFieldName]['config']['type'];
380  switch ($type) {
381  case 'input':
382  case 'select':
383  case 'text':
384  $childRowData[$childFieldName] = $childFieldValue;
385  break;
386  case 'check':
387  $childRowData[$childFieldName] = (bool)$childFieldValue;
388  break;
389  default:
390  throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1624286814);
391  }
392  }
393  $newSysSiteData[$fieldName][] = $childRowData;
394  }
395  break;
396 
397  case 'select':
399  $fieldValue = (int)$fieldValue;
400  } elseif (is_array($fieldValue)) {
401  $fieldValue = implode(',', $fieldValue);
402  }
403 
404  $newSysSiteData[$fieldName] = $fieldValue;
405  break;
406 
407  case 'check':
408  $newSysSiteData[$fieldName] = (bool)$fieldValue;
409  break;
410 
411  default:
412  throw new \RuntimeException('TCA type "' . $type . '" is not implemented in site handling', 1521032781);
413  }
414  }
415 
416  $newSiteConfiguration = $this->‪validateFullStructure(
417  $this->‪getMergeSiteData($currentSiteConfiguration, $newSysSiteData)
418  );
419 
420  // Persist the configuration
421  $siteConfigurationManager = GeneralUtility::makeInstance(SiteConfiguration::class);
422  try {
423  if (!$isNewConfiguration && $currentIdentifier !== $siteIdentifier) {
424  $siteConfigurationManager->rename($currentIdentifier, $siteIdentifier);
425  $this->‪getBackendUser()->‪writelog(‪Type::SITE, SiteAction::RENAME, SystemLogErrorClassification::MESSAGE, 0, 'Site configuration \'%s\' was renamed to \'%s\'.', [$currentIdentifier, $siteIdentifier], 'site');
426  }
427  $siteConfigurationManager->write($siteIdentifier, $newSiteConfiguration, true);
428  if ($isNewConfiguration) {
429  $this->‪getBackendUser()->‪writelog(‪Type::SITE, SiteAction::CREATE, SystemLogErrorClassification::MESSAGE, 0, 'Site configuration \'%s\' was created.', [$siteIdentifier], 'site');
430  } else {
431  $this->‪getBackendUser()->‪writelog(‪Type::SITE, SiteAction::UPDATE, SystemLogErrorClassification::MESSAGE, 0, 'Site configuration \'%s\' was updated.', [$siteIdentifier], 'site');
432  }
433  } catch (SiteConfigurationWriteException $e) {
434  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $e->getMessage(), '', ‪FlashMessage::WARNING, true);
435  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
436  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
437  $defaultFlashMessageQueue->enqueue($flashMessage);
438  }
439  } catch (SiteValidationErrorException $e) {
440  // Do not store new config if a validation error is thrown, but redirect only to show a generated flash message
441  }
442 
443  $saveRoute = $this->uriBuilder->buildUriFromRoute('site_configuration', ['action' => 'edit', 'site' => $siteIdentifier]);
444  if ($isSaveClose) {
445  return new RedirectResponse($overviewRoute);
446  }
447  return new RedirectResponse($saveRoute);
448  }
449 
458  protected function ‪validateAndProcessIdentifier(bool $isNew, string $identifier, int $rootPageId)
459  {
460  $languageService = $this->‪getLanguageService();
461  // Normal "eval" processing of field first
462  $identifier = $this->‪validateAndProcessValue('site', 'identifier', $identifier);
463  if ($isNew) {
464  // Verify no other site with this identifier exists. If so, find a new unique name as
465  // identifier and show a flash message the identifier has been adapted
466  try {
467  $this->siteFinder->getSiteByIdentifier($identifier);
468  // Force this identifier to be unique
469  $originalIdentifier = $identifier;
470  $identifier = ‪StringUtility::getUniqueId($identifier . '-');
471  $message = sprintf(
472  $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.message'),
473  $originalIdentifier,
474  $identifier
475  );
476  $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.title');
477  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, ‪FlashMessage::WARNING, true);
478  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
479  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
480  $defaultFlashMessageQueue->enqueue($flashMessage);
481  } catch (SiteNotFoundException $e) {
482  // Do nothing, this new identifier is ok
483  }
484  } else {
485  // If this is an existing config, the site for this identifier must have the same rootPageId, otherwise
486  // a user tried to rename a site identifier to a different site that already exists. If so, we do not rename
487  // the site and show a flash message
488  try {
489  $site = $this->siteFinder->getSiteByIdentifier($identifier);
490  if ($site->getRootPageId() !== $rootPageId) {
491  // Find original value and keep this
492  $origSite = $this->siteFinder->getSiteByRootPageId($rootPageId);
493  $originalIdentifier = $identifier;
494  $identifier = $origSite->getIdentifier();
495  $message = sprintf(
496  $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.message'),
497  $originalIdentifier,
498  $identifier
499  );
500  $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.title');
501  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, ‪FlashMessage::WARNING, true);
502  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
503  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
504  $defaultFlashMessageQueue->enqueue($flashMessage);
505  }
506  } catch (SiteNotFoundException $e) {
507  // User is renaming identifier which does not exist yet. That's ok
508  }
509  }
510  return $identifier;
511  }
512 
525  protected function ‪validateAndProcessValue(string $tableName, string $fieldName, $fieldValue)
526  {
527  $languageService = $this->‪getLanguageService();
528  $fieldConfig = ‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
529  $handledEvals = [];
530  if (!empty($fieldConfig['eval'])) {
531  $evalArray = ‪GeneralUtility::trimExplode(',', $fieldConfig['eval'], true);
532  // Processing
533  if (in_array('alphanum_x', $evalArray, true)) {
534  $handledEvals[] = 'alphanum_x';
535  $fieldValue = preg_replace('/[^a-zA-Z0-9_-]/', '', $fieldValue);
536  }
537  if (in_array('lower', $evalArray, true)) {
538  $handledEvals[] = 'lower';
539  $fieldValue = mb_strtolower($fieldValue, 'utf-8');
540  }
541  if (in_array('trim', $evalArray, true)) {
542  $handledEvals[] = 'trim';
543  $fieldValue = trim($fieldValue);
544  }
545  if (in_array('int', $evalArray, true)) {
546  $handledEvals[] = 'int';
547  $fieldValue = (int)$fieldValue;
548  }
549  // Validation throws - these should be handled client side already,
550  // eg. 'required' being set and receiving empty, shouldn't happen server side
551  if (in_array('required', $evalArray, true)) {
552  $handledEvals[] = 'required';
553  if (empty($fieldValue)) {
554  $message = sprintf(
555  $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.message'),
556  $fieldName
557  );
558  $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.title');
559  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, ‪FlashMessage::WARNING, true);
560  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
561  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
562  $defaultFlashMessageQueue->enqueue($flashMessage);
563  throw new SiteValidationErrorException(
564  'Field ' . $fieldName . ' is set to required, but received empty.',
565  1521726421
566  );
567  }
568  }
569  if (!empty(array_diff($evalArray, $handledEvals))) {
570  throw new \RuntimeException('At least one not implemented \'eval\' in list ' . $fieldConfig['eval'], 1522491734);
571  }
572  }
573  if (isset($fieldConfig['range']['lower'])) {
574  $fieldValue = (int)$fieldValue < (int)$fieldConfig['range']['lower'] ? (int)$fieldConfig['range']['lower'] : (int)$fieldValue;
575  }
576  if (isset($fieldConfig['range']['upper'])) {
577  $fieldValue = (int)$fieldValue > (int)$fieldConfig['range']['upper'] ? (int)$fieldConfig['range']['upper'] : (int)$fieldValue;
578  }
579  return $fieldValue;
580  }
581 
590  protected function ‪validateFullStructure(array $newSysSiteData): array
591  {
592  $languageService = $this->‪getLanguageService();
593  // Verify there are not two error handlers with the same error code
594  if (isset($newSysSiteData['errorHandling']) && is_array($newSysSiteData['errorHandling'])) {
595  $uniqueCriteria = [];
596  $validChildren = [];
597  foreach ($newSysSiteData['errorHandling'] as $child) {
598  if (!isset($child['errorCode'])) {
599  throw new \RuntimeException('No errorCode found', 1521788518);
600  }
601  if (!in_array((int)$child['errorCode'], $uniqueCriteria, true)) {
602  $uniqueCriteria[] = (int)$child['errorCode'];
603  $child['errorCode'] = (int)$child['errorCode'];
604  $validChildren[] = $child;
605  } else {
606  $message = sprintf(
607  $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.message'),
608  $child['errorCode']
609  );
610  $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.title');
611  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, ‪FlashMessage::WARNING, true);
612  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
613  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
614  $defaultFlashMessageQueue->enqueue($flashMessage);
615  }
616  }
617  $newSysSiteData['errorHandling'] = $validChildren;
618  }
619 
620  // Verify there is only one inline child per sys_language record configured.
621  if (!isset($newSysSiteData['languages']) || !is_array($newSysSiteData['languages']) || count($newSysSiteData['languages']) < 1) {
622  throw new \RuntimeException(
623  'No default language definition found. The interface does not allow this. Aborting',
624  1521789306
625  );
626  }
627  $uniqueCriteria = [];
628  $validChildren = [];
629  foreach ($newSysSiteData['languages'] as $child) {
630  if (!isset($child['languageId'])) {
631  throw new \RuntimeException('languageId not found', 1521789455);
632  }
633  if (!in_array((int)$child['languageId'], $uniqueCriteria, true)) {
634  $uniqueCriteria[] = (int)$child['languageId'];
635  $child['languageId'] = (int)$child['languageId'];
636  $validChildren[] = $child;
637  } else {
638  $message = sprintf(
639  $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.title'),
640  $child['languageId']
641  );
642  $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.title');
643  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, ‪FlashMessage::WARNING, true);
644  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
645  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
646  $defaultFlashMessageQueue->enqueue($flashMessage);
647  }
648  }
649  $newSysSiteData['languages'] = $validChildren;
650 
651  // cleanup configuration
652  foreach ($newSysSiteData as $identifier => $value) {
653  if (is_array($value) && empty($value)) {
654  unset($newSysSiteData[$identifier]);
655  }
656  }
657 
658  return $newSysSiteData;
659  }
660 
667  protected function ‪deleteAction(ServerRequestInterface $request): ResponseInterface
668  {
669  $siteIdentifier = $request->getQueryParams()['site'] ?? '';
670  if (empty($siteIdentifier)) {
671  throw new \RuntimeException('Not site identifier given', 1521565182);
672  }
673  try {
674  // Verify site does exist, method throws if not
675  GeneralUtility::makeInstance(SiteConfiguration::class)->delete($siteIdentifier);
676  $this->‪getBackendUser()->‪writelog(‪Type::SITE, SiteAction::DELETE, SystemLogErrorClassification::MESSAGE, 0, 'Site configuration \'%s\' was deleted.', [$siteIdentifier], 'site');
677  } catch (SiteConfigurationWriteException $e) {
678  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $e->getMessage(), '', ‪FlashMessage::WARNING, true);
679  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
680  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
681  $defaultFlashMessageQueue->enqueue($flashMessage);
682  }
683  $overviewRoute = $this->uriBuilder->buildUriFromRoute('site_configuration', ['action' => 'overview']);
684  return new RedirectResponse($overviewRoute);
685  }
686 
692  protected function ‪initializeView(string $templateName): void
693  {
694  $this->view = GeneralUtility::makeInstance(StandaloneView::class);
695  $this->view->setTemplate($templateName);
696  $this->view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/SiteConfiguration']);
697  $this->view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials']);
698  $this->view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
699  }
700 
704  protected function ‪configureEditViewDocHeader(): void
705  {
706  $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
707  $lang = $this->‪getLanguageService();
708  $closeButton = $buttonBar->makeLinkButton()
709  ->setHref('#')
710  ->setClasses('t3js-editform-close')
711  ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
712  ->setShowLabelText(true)
713  ->setIcon($this->iconFactory->getIcon('actions-close', ‪Icon::SIZE_SMALL));
714  $saveButton = $buttonBar->makeInputButton()
715  ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'))
716  ->setName('_savedok')
717  ->setValue('1')
718  ->setShowLabelText(true)
719  ->setForm('siteConfigurationController')
720  ->setIcon($this->iconFactory->getIcon('actions-document-save', ‪Icon::SIZE_SMALL));
721  $buttonBar->addButton($closeButton);
722  $buttonBar->addButton($saveButton, ‪ButtonBar::BUTTON_POSITION_LEFT, 2);
723  }
724 
730  protected function ‪configureOverViewDocHeader(string $requestUri): void
731  {
732  $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
733  $reloadButton = $buttonBar->makeLinkButton()
734  ->setHref($requestUri)
735  ->setTitle($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
736  ->setIcon($this->iconFactory->getIcon('actions-refresh', ‪Icon::SIZE_SMALL));
737  $buttonBar->addButton($reloadButton, ‪ButtonBar::BUTTON_POSITION_RIGHT);
738  $shortcutButton = $buttonBar->makeShortcutButton()
739  ->setRouteIdentifier('site_configuration')
740  ->setDisplayName($this->‪getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf:mlang_labels_tablabel'));
741  $buttonBar->addButton($shortcutButton, ‪ButtonBar::BUTTON_POSITION_RIGHT);
742  }
743 
748  protected function ‪getAllSitePages(): array
749  {
750  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
751  $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
752  $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, 0));
753  $statement = $queryBuilder
754  ->select('*')
755  ->from('pages')
756  ->where(
757  $queryBuilder->expr()->eq('sys_language_uid', 0),
758  $queryBuilder->expr()->orX(
759  $queryBuilder->expr()->andX(
760  $queryBuilder->expr()->eq('pid', 0),
761  $queryBuilder->expr()->notIn('doktype', [
766  ])
767  ),
768  $queryBuilder->expr()->eq('is_siteroot', 1)
769  )
770  )
771  ->orderBy('pid')
772  ->addOrderBy('sorting')
773  ->executeQuery();
774 
775  $pages = [];
776  while ($row = $statement->fetchAssociative()) {
777  $row['rootline'] = BackendUtility::BEgetRootLine((int)$row['uid']);
778  array_pop($row['rootline']);
779  $row['rootline'] = array_reverse($row['rootline']);
780  $i = 0;
781  foreach ($row['rootline'] as &$record) {
782  $record['margin'] = $i++ * 20;
783  }
784  $pages[(int)$row['uid']] = $row;
785  }
786  return $pages;
787  }
788 
796  protected function ‪getDuplicatedEntryPoints(array $allSites, array $pages): array
797  {
798  $duplicatedEntryPoints = [];
799 
800  foreach ($allSites as $identifier => $site) {
801  if (!isset($pages[$site->getRootPageId()])) {
802  continue;
803  }
804  foreach ($site->getAllLanguages() as $language) {
805  $base = $language->getBase();
806  $entryPoint = rtrim((string)$language->getBase(), '/');
807  $scheme = $base->getScheme() ? $base->getScheme() . '://' : '//';
808  $entryPointWithoutScheme = str_replace($scheme, '', $entryPoint);
809  if (!isset($duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint])) {
810  $duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint] = 1;
811  } else {
812  $duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint]++;
813  }
814  }
815  }
816  return array_filter($duplicatedEntryPoints, static function (array $variants): bool {
817  return count($variants) > 1 || reset($variants) > 1;
818  }, ARRAY_FILTER_USE_BOTH);
819  }
820 
826  protected function ‪getLastLanguageId(): int
827  {
828  $lastLanguageId = 0;
829  foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) {
830  foreach ($site->getAllLanguages() as $language) {
831  if ($language->getLanguageId() > $lastLanguageId) {
832  $lastLanguageId = $language->getLanguageId();
833  }
834  }
835  }
836  return $lastLanguageId;
837  }
838 
848  protected function ‪getMergeSiteData(array $currentSiteConfiguration, array $newSysSiteData): array
849  {
850  $newSysSiteData = array_merge($currentSiteConfiguration, $newSysSiteData);
851 
852  // @todo: this should go away, once base variants for languages are managable via the GUI.
853  $existingLanguageConfigurationsWithBaseVariants = [];
854  foreach ($currentSiteConfiguration['languages'] ?? [] as $languageConfiguration) {
855  if (isset($languageConfiguration['baseVariants'])) {
856  $existingLanguageConfigurationsWithBaseVariants[$languageConfiguration['languageId']] = $languageConfiguration['baseVariants'];
857  }
858  }
859  foreach ($newSysSiteData['languages'] ?? [] as $key => $languageConfiguration) {
860  if (isset($existingLanguageConfigurationsWithBaseVariants[$languageConfiguration['languageId']])) {
861  $newSysSiteData['languages'][$key]['baseVariants'] = $existingLanguageConfigurationsWithBaseVariants[$languageConfiguration['languageId']];
862  }
863  }
864 
865  return $newSysSiteData;
866  }
867 
871  protected function ‪getLanguageService(): LanguageService
872  {
873  return ‪$GLOBALS['LANG'];
874  }
875 
879  protected function ‪getBackendUser(): BackendUserAuthentication
880  {
881  return ‪$GLOBALS['BE_USER'];
882  }
883 }
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\$siteFinder
‪SiteFinder $siteFinder
Definition: SiteConfigurationController.php:79
‪TYPO3\CMS\Core\Imaging\Icon\SIZE_SMALL
‪const SIZE_SMALL
Definition: Icon.php:30
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:999
‪TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
Definition: HiddenRestriction.php:27
‪TYPO3\CMS\Backend\Configuration\SiteTcaConfiguration
Definition: SiteTcaConfiguration.php:33
‪TYPO3\CMS\Core\SysLog\Action\Site
Definition: Site.php:24
‪TYPO3\CMS\Backend\Form\FormResultCompiler
Definition: FormResultCompiler.php:31
‪TYPO3\CMS\Backend\Template\Components\ButtonBar\BUTTON_POSITION_LEFT
‪const BUTTON_POSITION_LEFT
Definition: ButtonBar.php:36
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger($var)
Definition: MathUtility.php:74
‪TYPO3\CMS\Core\Configuration\Exception\SiteConfigurationWriteException
Definition: SiteConfigurationWriteException.php:25
‪TYPO3\CMS\Backend\Template\Components\ButtonBar
Definition: ButtonBar.php:32
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\configureOverViewDocHeader
‪configureOverViewDocHeader(string $requestUri)
Definition: SiteConfigurationController.php:728
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\validateAndProcessValue
‪mixed validateAndProcessValue(string $tableName, string $fieldName, $fieldValue)
Definition: SiteConfigurationController.php:523
‪TYPO3\CMS\Core\Imaging\Icon
Definition: Icon.php:26
‪TYPO3\CMS\Backend\Template\ModuleTemplateFactory
Definition: ModuleTemplateFactory.php:29
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\$moduleTemplate
‪ModuleTemplate $moduleTemplate
Definition: SiteConfigurationController.php:73
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\$uriBuilder
‪UriBuilder $uriBuilder
Definition: SiteConfigurationController.php:82
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\$moduleTemplateFactory
‪ModuleTemplateFactory $moduleTemplateFactory
Definition: SiteConfigurationController.php:83
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\$iconFactory
‪IconFactory $iconFactory
Definition: SiteConfigurationController.php:80
‪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:111
‪TYPO3\CMS\Core\Exception\SiteNotFoundException
Definition: SiteNotFoundException.php:25
‪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:135
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\__construct
‪__construct(SiteFinder $siteFinder, IconFactory $iconFactory, PageRenderer $pageRenderer, UriBuilder $uriBuilder, ModuleTemplateFactory $moduleTemplateFactory)
Definition: SiteConfigurationController.php:85
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\$pageRenderer
‪PageRenderer $pageRenderer
Definition: SiteConfigurationController.php:81
‪TYPO3\CMS\Core\Configuration\SiteConfiguration
Definition: SiteConfiguration.php:43
‪TYPO3\CMS\Backend\Template\ModuleTemplate
Definition: ModuleTemplate.php:46
‪TYPO3\CMS\Core\Site\Entity\Site
Definition: Site.php:42
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\validateAndProcessIdentifier
‪mixed validateAndProcessIdentifier(bool $isNew, string $identifier, int $rootPageId)
Definition: SiteConfigurationController.php:456
‪TYPO3\CMS\Core\Messaging\AbstractMessage\WARNING
‪const WARNING
Definition: AbstractMessage.php:30
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\saveAction
‪ResponseInterface saveAction(ServerRequestInterface $request)
Definition: SiteConfigurationController.php:235
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController
Definition: SiteConfigurationController.php:68
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\deleteAction
‪ResponseInterface deleteAction(ServerRequestInterface $request)
Definition: SiteConfigurationController.php:665
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:40
‪TYPO3\CMS\Backend\Exception\SiteValidationErrorException
Definition: SiteValidationErrorException.php:25
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_SPACER
‪const DOKTYPE_SPACER
Definition: PageRepository.php:115
‪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:116
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\$view
‪ViewInterface $view
Definition: SiteConfigurationController.php:77
‪TYPO3\CMS\Core\Http\RedirectResponse
Definition: RedirectResponse.php:28
‪TYPO3\CMS\Backend\Form\NodeFactory
Definition: NodeFactory.php:37
‪TYPO3\CMS\Core\Utility\StringUtility\getUniqueId
‪static string getUniqueId($prefix='')
Definition: StringUtility.php:128
‪TYPO3\CMS\Core\Messaging\FlashMessage
Definition: FlashMessage.php:26
‪TYPO3\CMS\Fluid\View\StandaloneView
Definition: StandaloneView.php:31
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication\writelog
‪int writelog($type, $action, $error, $details_nr, $details, $data, $tablename='', $recuid='', $recpid='', $event_pid=-1, $NEWid='', $userId=0)
Definition: BackendUserAuthentication.php:2020
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\editAction
‪editAction(ServerRequestInterface $request)
Definition: SiteConfigurationController.php:167
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\initializeView
‪initializeView(string $templateName)
Definition: SiteConfigurationController.php:690
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\validateFullStructure
‪array validateFullStructure(array $newSysSiteData)
Definition: SiteConfigurationController.php:588
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\handleRequest
‪ResponseInterface handleRequest(ServerRequestInterface $request)
Definition: SiteConfigurationController.php:105
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\ALLOWED_ACTIONS
‪const ALLOWED_ACTIONS
Definition: SiteConfigurationController.php:69
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:22
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:42
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:53
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\getLastLanguageId
‪int getLastLanguageId()
Definition: SiteConfigurationController.php:824
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Backend\Form\FormDataCompiler
Definition: FormDataCompiler.php:25
‪TYPO3\CMS\Core\Utility\StringUtility
Definition: StringUtility.php:22
‪TYPO3\CMS\Backend\Template\Components\ButtonBar\BUTTON_POSITION_RIGHT
‪const BUTTON_POSITION_RIGHT
Definition: ButtonBar.php:41
‪TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup
Definition: SiteConfigurationDataGroup.php:33
‪TYPO3\CMS\Backend\Controller
Definition: AboutController.php:16
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_RECYCLER
‪const DOKTYPE_RECYCLER
Definition: PageRepository.php:117
‪TYPO3\CMS\Core\Messaging\FlashMessageService
Definition: FlashMessageService.php:27
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\getLanguageService
‪LanguageService getLanguageService()
Definition: SiteConfigurationController.php:869
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\getDuplicatedEntryPoints
‪array getDuplicatedEntryPoints(array $allSites, array $pages)
Definition: SiteConfigurationController.php:794
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\getBackendUser
‪BackendUserAuthentication getBackendUser()
Definition: SiteConfigurationController.php:877
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\configureEditViewDocHeader
‪configureEditViewDocHeader()
Definition: SiteConfigurationController.php:702
‪TYPO3\CMS\Core\Http\HtmlResponse
Definition: HtmlResponse.php:26
‪TYPO3\CMS\Core\SysLog\Type
Definition: Type.php:28
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\getMergeSiteData
‪getMergeSiteData(array $currentSiteConfiguration, array $newSysSiteData)
Definition: SiteConfigurationController.php:846
‪TYPO3\CMS\Backend\Controller\SiteConfigurationController\getAllSitePages
‪getAllSitePages()
Definition: SiteConfigurationController.php:746
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:40