Completed
Push — master ( e3e559...61eb4b )
by
unknown
38:09 queued 20:12
created

getDuplicatedEntryPoints()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 16
nc 3
nop 2
dl 0
loc 23
rs 8.8333
c 0
b 0
f 0
1
<?php
2
declare(strict_types = 1);
3
namespace TYPO3\CMS\Backend\Controller;
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
use Psr\Http\Message\ResponseInterface;
19
use Psr\Http\Message\ServerRequestInterface;
20
use TYPO3\CMS\Backend\Configuration\SiteTcaConfiguration;
21
use TYPO3\CMS\Backend\Exception\SiteValidationErrorException;
22
use TYPO3\CMS\Backend\Form\FormDataCompiler;
23
use TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup;
24
use TYPO3\CMS\Backend\Form\FormResultCompiler;
25
use TYPO3\CMS\Backend\Form\NodeFactory;
26
use TYPO3\CMS\Backend\Routing\UriBuilder;
27
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
28
use TYPO3\CMS\Backend\Template\ModuleTemplate;
29
use TYPO3\CMS\Backend\Utility\BackendUtility;
30
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
31
use TYPO3\CMS\Core\Configuration\SiteConfiguration;
32
use TYPO3\CMS\Core\Core\Environment;
33
use TYPO3\CMS\Core\Database\ConnectionPool;
34
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
35
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
36
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
37
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
38
use TYPO3\CMS\Core\Http\HtmlResponse;
39
use TYPO3\CMS\Core\Http\RedirectResponse;
40
use TYPO3\CMS\Core\Imaging\Icon;
41
use TYPO3\CMS\Core\Localization\LanguageService;
42
use TYPO3\CMS\Core\Messaging\FlashMessage;
43
use TYPO3\CMS\Core\Messaging\FlashMessageService;
44
use TYPO3\CMS\Core\Site\Entity\Site;
45
use TYPO3\CMS\Core\Site\SiteFinder;
46
use TYPO3\CMS\Core\Utility\GeneralUtility;
47
use TYPO3\CMS\Core\Utility\MathUtility;
48
use TYPO3\CMS\Fluid\View\StandaloneView;
49
use TYPO3Fluid\Fluid\View\ViewInterface;
50
51
/**
52
 * Backend controller: The "Site management" -> "Sites" module
53
 *
54
 * List all site root pages, CRUD site configuration.
55
 *
56
 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
57
 */
58
class SiteConfigurationController
59
{
60
    /**
61
     * @var ModuleTemplate
62
     */
63
    protected $moduleTemplate;
64
65
    /**
66
     * @var ViewInterface
67
     */
68
    protected $view;
69
70
    /**
71
     * @var SiteFinder
72
     */
73
    protected $siteFinder;
74
75
    /**
76
     * Default constructor
77
     */
78
    public function __construct()
79
    {
80
        $this->siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
81
        $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
82
    }
83
84
    /**
85
     * Main entry method: Dispatch to other actions - those method names that end with "Action".
86
     *
87
     * @param ServerRequestInterface $request the current request
88
     * @return ResponseInterface the response with the content
89
     */
90
    public function handleRequest(ServerRequestInterface $request): ResponseInterface
91
    {
92
        // forcing uncached sites will re-initialize `SiteFinder`
93
        // which is used later by FormEngine (implicit behavior)
94
        $this->siteFinder->getAllSites(false);
95
        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
96
        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
97
        $action = $request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'overview';
98
        $this->initializeView($action);
99
        $result = call_user_func_array([$this, $action . 'Action'], [$request]);
100
        if ($result instanceof ResponseInterface) {
101
            return $result;
102
        }
103
        $this->moduleTemplate->setContent($this->view->render());
104
        return new HtmlResponse($this->moduleTemplate->renderContent());
105
    }
106
107
    /**
108
     * List pages that have 'is_siteroot' flag set - those that have the globe icon in page tree.
109
     * Link to Add / Edit / Delete for each.
110
     */
111
    protected function overviewAction(): void
112
    {
113
        $this->configureOverViewDocHeader();
114
        $allSites = $this->siteFinder->getAllSites();
115
        $pages = $this->getAllSitePages();
116
        $unassignedSites = [];
117
        foreach ($allSites as $identifier => $site) {
118
            $rootPageId = $site->getRootPageId();
119
            if (isset($pages[$rootPageId])) {
120
                $pages[$rootPageId]['siteIdentifier'] = $identifier;
121
                $pages[$rootPageId]['siteConfiguration'] = $site;
122
            } else {
123
                $unassignedSites[] = $site;
124
            }
125
        }
126
127
        $this->view->assignMultiple([
128
            'pages' => $pages,
129
            'unassignedSites' => $unassignedSites,
130
            'duplicatedEntryPoints' => $this->getDuplicatedEntryPoints($allSites, $pages),
131
        ]);
132
    }
133
134
    /**
135
     * Shows a form to create a new site configuration, or edit an existing one.
136
     *
137
     * @param ServerRequestInterface $request
138
     * @throws \RuntimeException
139
     */
140
    protected function editAction(ServerRequestInterface $request): void
141
    {
142
        $this->configureEditViewDocHeader();
143
144
        // Put site and friends TCA into global TCA
145
        // @todo: We might be able to get rid of that later
146
        $GLOBALS['TCA'] = array_merge($GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca());
147
148
        $siteIdentifier = $request->getQueryParams()['site'] ?? null;
149
        $pageUid = (int)($request->getQueryParams()['pageUid'] ?? 0);
150
151
        if (empty($siteIdentifier) && empty($pageUid)) {
152
            throw new \RuntimeException('Either site identifier to edit a config or page uid to add new config must be set', 1521561148);
153
        }
154
        $isNewConfig = empty($siteIdentifier);
155
156
        $defaultValues = [];
157
        if ($isNewConfig) {
158
            $defaultValues['site']['rootPageId'] = $pageUid;
159
        }
160
161
        $allSites = $this->siteFinder->getAllSites();
162
        if (!$isNewConfig && !isset($allSites[$siteIdentifier])) {
163
            throw new \RuntimeException('Existing config for site ' . $siteIdentifier . ' not found', 1521561226);
164
        }
165
166
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
167
        $returnUrl = $uriBuilder->buildUriFromRoute('site_configuration');
168
169
        $formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class);
170
        $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
171
        $formDataCompilerInput = [
172
            'tableName' => 'site',
173
            'vanillaUid' => $isNewConfig ? $pageUid : $allSites[$siteIdentifier]->getRootPageId(),
174
            'command' => $isNewConfig ? 'new' : 'edit',
175
            'returnUrl' => (string)$returnUrl,
176
            'customData' => [
177
                'siteIdentifier' => $isNewConfig ? '' : $siteIdentifier,
178
            ],
179
            'defaultValues' => $defaultValues,
180
        ];
181
        $formData = $formDataCompiler->compile($formDataCompilerInput);
182
        $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
183
        $formData['renderType'] = 'outerWrapContainer';
184
        $formResult = $nodeFactory->create($formData)->render();
185
        // Needed to be set for 'onChange="reload"' and reload on type change to work
186
        $formResult['doSaveFieldName'] = 'doSave';
187
        $formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
188
        $formResultCompiler->mergeResult($formResult);
189
        $formResultCompiler->addCssFiles();
190
        // Always add rootPageId as additional field to have a reference for new records
191
        $this->view->assign('rootPageId', $isNewConfig ? $pageUid : $allSites[$siteIdentifier]->getRootPageId());
192
        $this->view->assign('returnUrl', $returnUrl);
193
        $this->view->assign('formEngineHtml', $formResult['html']);
194
        $this->view->assign('formEngineFooter', $formResultCompiler->printNeededJSFunctions());
195
    }
196
197
    /**
198
     * Save incoming data from editAction and redirect to overview or edit
199
     *
200
     * @param ServerRequestInterface $request
201
     * @return ResponseInterface
202
     * @throws \RuntimeException
203
     */
204
    protected function saveAction(ServerRequestInterface $request): ResponseInterface
205
    {
206
        // Put site and friends TCA into global TCA
207
        // @todo: We might be able to get rid of that later
208
        $GLOBALS['TCA'] = array_merge($GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca());
209
210
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
211
        $siteTca = GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca();
212
213
        $overviewRoute = $uriBuilder->buildUriFromRoute('site_configuration', ['action' => 'overview']);
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
        try {
238
            $currentSite = $this->siteFinder->getSiteByRootPageId($pageId);
239
            $currentSiteConfiguration = $currentSite->getConfiguration();
240
            $currentIdentifier = $currentSite->getIdentifier();
241
        } catch (SiteNotFoundException $e) {
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);
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 'text':
264
                        $fieldValue = $this->validateAndProcessValue('site', $fieldName, $fieldValue);
265
                        $newSysSiteData[$fieldName] = $fieldValue;
266
                        break;
267
268
                    case 'inline':
269
                        $newSysSiteData[$fieldName] = [];
270
                        $childRowIds = GeneralUtility::trimExplode(',', $fieldValue, true);
271
                        if (!isset($siteTca['site']['columns'][$fieldName]['config']['foreign_table'])) {
272
                            throw new \RuntimeException('No foreign_table found for inline type', 1521555037);
273
                        }
274
                        $foreignTable = $siteTca['site']['columns'][$fieldName]['config']['foreign_table'];
275
                        foreach ($childRowIds as $childRowId) {
276
                            $childRowData = [];
277
                            if (!isset($data[$foreignTable][$childRowId])) {
278
                                if (!empty($currentSiteConfiguration[$fieldName][$childRowId])) {
279
                                    // A collapsed inline record: Fetch data from existing config
280
                                    $newSysSiteData[$fieldName][] = $currentSiteConfiguration[$fieldName][$childRowId];
281
                                    continue;
282
                                }
283
                                throw new \RuntimeException('No data found for table ' . $foreignTable . ' with id ' . $childRowId, 1521555177);
284
                            }
285
                            $childRow = $data[$foreignTable][$childRowId];
286
                            foreach ($childRow as $childFieldName => $childFieldValue) {
287
                                if ($childFieldName === 'pid') {
288
                                    // pid is added by inline by default, but not relevant for yml storage
289
                                    continue;
290
                                }
291
                                $type = $siteTca[$foreignTable]['columns'][$childFieldName]['config']['type'];
292
                                switch ($type) {
293
                                    case 'input':
294
                                    case 'select':
295
                                    case 'text':
296
                                        $childRowData[$childFieldName] = $childFieldValue;
297
                                        break;
298
                                    case 'check':
299
                                        $childRowData[$childFieldName] = (bool)$childFieldValue;
300
                                        break;
301
                                    default:
302
                                        throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1521555340);
303
                                }
304
                            }
305
                            $newSysSiteData[$fieldName][] = $childRowData;
306
                        }
307
                        break;
308
309
                    case 'select':
310
                        $newSysSiteData[$fieldName] = MathUtility::canBeInterpretedAsInteger($fieldValue) ? (int)$fieldValue : $fieldValue;
311
                        break;
312
313
                    case 'check':
314
                        $newSysSiteData[$fieldName] = (bool)$fieldValue;
315
                        break;
316
317
                    default:
318
                        throw new \RuntimeException('TCA type "' . $type . '" is not implemented in site handling', 1521032781);
319
                }
320
            }
321
322
            // keep root config objects not given via GUI
323
            // this way extension authors are able to use their own objects on root level
324
            // that are not configurable via GUI
325
            // however: we overwrite the full subset of any GUI object to make sure we have a clean state
326
            $newSysSiteData = array_merge($currentSiteConfiguration, $newSysSiteData);
327
            $newSiteConfiguration = $this->validateFullStructure($newSysSiteData);
328
329
            // Persist the configuration
330
            $siteConfigurationManager = GeneralUtility::makeInstance(SiteConfiguration::class, Environment::getConfigPath() . '/sites');
331
            if (!$isNewConfiguration && $currentIdentifier !== $siteIdentifier) {
332
                $siteConfigurationManager->rename($currentIdentifier, $siteIdentifier);
333
            }
334
            $siteConfigurationManager->write($siteIdentifier, $newSiteConfiguration);
335
        } catch (SiteValidationErrorException $e) {
336
            // Do not store new config if a validation error is thrown, but redirect only to show a generated flash message
337
        }
338
339
        $saveRoute = $uriBuilder->buildUriFromRoute('site_configuration', ['action' => 'edit', 'site' => $siteIdentifier]);
340
        if ($isSaveClose) {
341
            return new RedirectResponse($overviewRoute);
342
        }
343
        return new RedirectResponse($saveRoute);
344
    }
345
346
    /**
347
     * Validation and processing of site identifier
348
     *
349
     * @param bool $isNew If true, we're dealing with a new record
350
     * @param string $identifier Given identifier to validate and process
351
     * @param int $rootPageId Page uid this identifier is bound to
352
     * @return mixed Verified / modified value
353
     */
354
    protected function validateAndProcessIdentifier(bool $isNew, string $identifier, int $rootPageId)
355
    {
356
        $languageService = $this->getLanguageService();
357
        // Normal "eval" processing of field first
358
        $identifier = $this->validateAndProcessValue('site', 'identifier', $identifier);
359
        if ($isNew) {
360
            // Verify no other site with this identifier exists. If so, find a new unique name as
361
            // identifier and show a flash message the identifier has been adapted
362
            try {
363
                $this->siteFinder->getSiteByIdentifier($identifier);
364
                // Force this identifier to be unique
365
                $originalIdentifier = $identifier;
366
                $identifier = $identifier . '-' . str_replace('.', '', uniqid((string)random_int(0, mt_getrandmax()), true));
367
                $message = sprintf(
368
                    $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.message'),
369
                    $originalIdentifier,
370
                    $identifier
371
                );
372
                $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.title');
373
                $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true);
374
                $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
375
                $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
376
                $defaultFlashMessageQueue->enqueue($flashMessage);
377
            } catch (SiteNotFoundException $e) {
378
                // Do nothing, this new identifier is ok
379
            }
380
        } else {
381
            // If this is an existing config, the site for this identifier must have the same rootPageId, otherwise
382
            // a user tried to rename a site identifier to a different site that already exists. If so, we do not rename
383
            // the site and show a flash message
384
            try {
385
                $site = $this->siteFinder->getSiteByIdentifier($identifier);
386
                if ($site->getRootPageId() !== $rootPageId) {
387
                    // Find original value and keep this
388
                    $origSite = $this->siteFinder->getSiteByRootPageId($rootPageId);
389
                    $originalIdentifier = $identifier;
390
                    $identifier = $origSite->getIdentifier();
391
                    $message = sprintf(
392
                        $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.message'),
393
                        $originalIdentifier,
394
                        $identifier
395
                    );
396
                    $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.title');
397
                    $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true);
398
                    $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
399
                    $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
400
                    $defaultFlashMessageQueue->enqueue($flashMessage);
401
                }
402
            } catch (SiteNotFoundException $e) {
403
                // User is renaming identifier which does not exist yet. That's ok
404
            }
405
        }
406
        return $identifier;
407
    }
408
409
    /**
410
     * Simple validation and processing method for incoming form field values.
411
     *
412
     * Note this does not support all TCA "eval" options but only what we really need.
413
     *
414
     * @param string $tableName Table name
415
     * @param string $fieldName Field name
416
     * @param mixed $fieldValue Incoming value from FormEngine
417
     * @return mixed Verified / modified value
418
     * @throws SiteValidationErrorException
419
     * @throws \RuntimeException
420
     */
421
    protected function validateAndProcessValue(string $tableName, string $fieldName, $fieldValue)
422
    {
423
        $languageService = $this->getLanguageService();
424
        $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
425
        $handledEvals = [];
426
        if (!empty($fieldConfig['eval'])) {
427
            $evalArray = GeneralUtility::trimExplode(',', $fieldConfig['eval'], true);
428
            // Processing
429
            if (in_array('alphanum_x', $evalArray, true)) {
430
                $handledEvals[] = 'alphanum_x';
431
                $fieldValue = preg_replace('/[^a-zA-Z0-9_-]/', '', $fieldValue);
432
            }
433
            if (in_array('lower', $evalArray, true)) {
434
                $handledEvals[] = 'lower';
435
                $fieldValue = mb_strtolower($fieldValue, 'utf-8');
436
            }
437
            if (in_array('trim', $evalArray, true)) {
438
                $handledEvals[] = 'trim';
439
                $fieldValue = trim($fieldValue);
440
            }
441
            if (in_array('int', $evalArray, true)) {
442
                $handledEvals[] = 'int';
443
                $fieldValue = (int)$fieldValue;
444
            }
445
            // Validation throws - these should be handled client side already,
446
            // eg. 'required' being set and receiving empty, shouldn't happen server side
447
            if (in_array('required', $evalArray, true)) {
448
                $handledEvals[] = 'required';
449
                if (empty($fieldValue)) {
450
                    $message = sprintf(
451
                        $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.message'),
452
                        $fieldName
453
                    );
454
                    $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.title');
455
                    $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true);
456
                    $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
457
                    $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
458
                    $defaultFlashMessageQueue->enqueue($flashMessage);
459
                    throw new SiteValidationErrorException(
460
                        'Field ' . $fieldName . ' is set to required, but received empty.',
461
                        1521726421
462
                    );
463
                }
464
            }
465
            if (!empty(array_diff($evalArray, $handledEvals))) {
466
                throw new \RuntimeException('At least one not implemented \'eval\' in list ' . $fieldConfig['eval'], 1522491734);
467
            }
468
        }
469
        if (isset($fieldConfig['range']['lower'])) {
470
            $fieldValue = (int)$fieldValue < (int)$fieldConfig['range']['lower'] ? (int)$fieldConfig['range']['lower'] : (int)$fieldValue;
471
        }
472
        if (isset($fieldConfig['range']['upper'])) {
473
            $fieldValue = (int)$fieldValue > (int)$fieldConfig['range']['upper'] ? (int)$fieldConfig['range']['upper'] : (int)$fieldValue;
474
        }
475
        return $fieldValue;
476
    }
477
478
    /**
479
     * Last sanitation method after all data has been gathered. Check integrity
480
     * of full record, manipulate if possible, or throw exception if unfixable broken.
481
     *
482
     * @param array $newSysSiteData Incoming data
483
     * @return array Updated data if needed
484
     * @throws \RuntimeException
485
     */
486
    protected function validateFullStructure(array $newSysSiteData): array
487
    {
488
        $languageService = $this->getLanguageService();
489
        // Verify there are not two error handlers with the same error code
490
        if (isset($newSysSiteData['errorHandling']) && is_array($newSysSiteData['errorHandling'])) {
491
            $uniqueCriteria = [];
492
            $validChildren = [];
493
            foreach ($newSysSiteData['errorHandling'] as $child) {
494
                if (!isset($child['errorCode'])) {
495
                    throw new \RuntimeException('No errorCode found', 1521788518);
496
                }
497
                if (!in_array((int)$child['errorCode'], $uniqueCriteria, true)) {
498
                    $uniqueCriteria[] = (int)$child['errorCode'];
499
                    $validChildren[] = $child;
500
                } else {
501
                    $message = sprintf(
502
                        $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.message'),
503
                        $child['errorCode']
504
                    );
505
                    $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.title');
506
                    $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true);
507
                    $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
508
                    $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
509
                    $defaultFlashMessageQueue->enqueue($flashMessage);
510
                }
511
            }
512
            $newSysSiteData['errorHandling'] = $validChildren;
513
        }
514
515
        // Verify there is only one inline child per sys_language record configured.
516
        if (!isset($newSysSiteData['languages']) || !is_array($newSysSiteData['languages']) || count($newSysSiteData['languages']) < 1) {
517
            throw new \RuntimeException(
518
                'No default language definition found. The interface does not allow this. Aborting',
519
                1521789306
520
            );
521
        }
522
        $uniqueCriteria = [];
523
        $validChildren = [];
524
        foreach ($newSysSiteData['languages'] as $child) {
525
            if (!isset($child['languageId'])) {
526
                throw new \RuntimeException('languageId not found', 1521789455);
527
            }
528
            if (!in_array((int)$child['languageId'], $uniqueCriteria, true)) {
529
                $uniqueCriteria[] = (int)$child['languageId'];
530
                $validChildren[] = $child;
531
            } else {
532
                $message = sprintf(
533
                    $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.title'),
534
                    $child['languageId']
535
                );
536
                $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.title');
537
                $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true);
538
                $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
539
                $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
540
                $defaultFlashMessageQueue->enqueue($flashMessage);
541
            }
542
        }
543
        $newSysSiteData['languages'] = $validChildren;
544
545
        return $newSysSiteData;
546
    }
547
548
    /**
549
     * Delete an existing configuration
550
     *
551
     * @param ServerRequestInterface $request
552
     * @return ResponseInterface
553
     */
554
    protected function deleteAction(ServerRequestInterface $request): ResponseInterface
555
    {
556
        $siteIdentifier = $request->getQueryParams()['site'] ?? '';
557
        if (empty($siteIdentifier)) {
558
            throw new \RuntimeException('Not site identifier given', 1521565182);
559
        }
560
        // Verify site does exist, method throws if not
561
        GeneralUtility::makeInstance(SiteConfiguration::class, Environment::getConfigPath() . '/sites')->delete($siteIdentifier);
562
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
563
        $overviewRoute = $uriBuilder->buildUriFromRoute('site_configuration', ['action' => 'overview']);
564
        return new RedirectResponse($overviewRoute);
565
    }
566
567
    /**
568
     * Sets up the Fluid View.
569
     *
570
     * @param string $templateName
571
     */
572
    protected function initializeView(string $templateName): void
573
    {
574
        $this->view = GeneralUtility::makeInstance(StandaloneView::class);
575
        $this->view->setTemplate($templateName);
576
        $this->view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/SiteConfiguration']);
577
        $this->view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials']);
578
        $this->view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
579
    }
580
581
    /**
582
     * Create document header buttons of "edit" action
583
     */
584
    protected function configureEditViewDocHeader(): void
585
    {
586
        $iconFactory = $this->moduleTemplate->getIconFactory();
587
        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
588
        $lang = $this->getLanguageService();
589
        $closeButton = $buttonBar->makeLinkButton()
590
            ->setHref('#')
591
            ->setClasses('t3js-editform-close')
592
            ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
593
            ->setShowLabelText(true)
594
            ->setIcon($iconFactory->getIcon('actions-close', Icon::SIZE_SMALL));
595
        $saveButton = $buttonBar->makeInputButton()
596
            ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'))
597
            ->setName('_savedok')
598
            ->setValue('1')
599
            ->setShowLabelText(true)
600
            ->setForm('siteConfigurationController')
601
            ->setIcon($iconFactory->getIcon('actions-document-save', Icon::SIZE_SMALL));
602
        $buttonBar->addButton($closeButton);
603
        $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
604
    }
605
606
    /**
607
     * Create document header buttons of "overview" action
608
     */
609
    protected function configureOverViewDocHeader(): void
610
    {
611
        $iconFactory = $this->moduleTemplate->getIconFactory();
612
        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
613
        $reloadButton = $buttonBar->makeLinkButton()
614
            ->setHref(GeneralUtility::getIndpEnv('REQUEST_URI'))
615
            ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
616
            ->setIcon($iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL));
617
        $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
618
        if ($this->getBackendUser()->mayMakeShortcut()) {
619
            $getVars = ['id', 'route'];
620
            $shortcutButton = $buttonBar->makeShortcutButton()
621
                ->setModuleName('site_configuration')
622
                ->setGetVariables($getVars);
623
            $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT);
624
        }
625
    }
626
627
    /**
628
     * Returns a list of pages that have 'is_siteroot' set
629
     *
630
     * @return array
631
     */
632
    protected function getAllSitePages(): array
633
    {
634
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
635
        $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
636
        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, 0, false));
637
        $statement = $queryBuilder
638
            ->select('*')
639
            ->from('pages')
640
            ->where(
641
                $queryBuilder->expr()->eq('sys_language_uid', 0),
642
                $queryBuilder->expr()->orX(
643
                    $queryBuilder->expr()->andX(
644
                        $queryBuilder->expr()->eq('pid', 0),
645
                        $queryBuilder->expr()->neq('doktype', PageRepository::DOKTYPE_SYSFOLDER)
646
                    ),
647
                    $queryBuilder->expr()->eq('is_siteroot', 1)
648
                )
649
            )
650
            ->orderBy('pid')
651
            ->addOrderBy('sorting')
652
            ->execute();
653
654
        $pages = [];
655
        while ($row = $statement->fetch()) {
656
            $row['rootline'] = BackendUtility::BEgetRootLine((int)$row['uid']);
657
            array_pop($row['rootline']);
658
            $row['rootline'] = array_reverse($row['rootline']);
659
            $i = 0;
660
            foreach ($row['rootline'] as &$record) {
661
                $record['margin'] = $i++ * 20;
662
            }
663
            $pages[(int)$row['uid']] = $row;
664
        }
665
        return $pages;
666
    }
667
668
    /**
669
     * Get all entry duplicates which are used multiple times
670
     *
671
     * @param Site[] $allSites
672
     * @param array $pages
673
     * @return array
674
     */
675
    protected function getDuplicatedEntryPoints(array $allSites, array $pages): array
676
    {
677
        $duplicatedEntryPoints = [];
678
679
        foreach ($allSites as $identifier => $site) {
680
            if (!isset($pages[$site->getRootPageId()])) {
681
                continue;
682
            }
683
            foreach ($site->getAllLanguages() as $language) {
684
                $base = $language->getBase();
685
                $entryPoint = rtrim((string)$language->getBase(), '/');
686
                $scheme = $base->getScheme() ? $base->getScheme() . '://' : '//';
687
                $entryPointWithoutScheme = str_replace($scheme, '', $entryPoint);
688
                if (!isset($duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint])) {
689
                    $duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint] = 1;
690
                } else {
691
                    $duplicatedEntryPoints[$entryPointWithoutScheme][$entryPoint]++;
692
                }
693
            }
694
        }
695
        return array_filter($duplicatedEntryPoints, static function (array $variants): bool {
696
            return count($variants) > 1 || reset($variants) > 1;
697
        }, ARRAY_FILTER_USE_BOTH);
698
    }
699
700
    /**
701
     * @return LanguageService
702
     */
703
    protected function getLanguageService(): LanguageService
704
    {
705
        return $GLOBALS['LANG'];
706
    }
707
708
    /**
709
     * @return BackendUserAuthentication
710
     */
711
    protected function getBackendUser(): BackendUserAuthentication
712
    {
713
        return $GLOBALS['BE_USER'];
714
    }
715
}
716