Passed
Push — master ( fb6744...da9c6d )
by
unknown
17:04
created

SiteConfigurationController::getLastLanguageId()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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