diff --git a/typo3/sysext/backend/Classes/Configuration/SiteTcaConfiguration.php b/typo3/sysext/backend/Classes/Configuration/SiteTcaConfiguration.php new file mode 100644 index 0000000000000000000000000000000000000000..9347d2907d4bf894560d8632fdd525a3a2f90f96 --- /dev/null +++ b/typo3/sysext/backend/Classes/Configuration/SiteTcaConfiguration.php @@ -0,0 +1,72 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Configuration; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Symfony\Component\Finder\Finder; +use TYPO3\CMS\Core\Package\PackageManager; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Helper class for the backend "Sites" module + * + * Load Site configuration TCA from ext:*Configuration/SiteConfigurationTCA + * and ext:*Configuration/SiteConfigurationTCA/Overrides + */ +class SiteTcaConfiguration +{ + /** + * Returns a "fake TCA" array that is syntactically identical to + * "normal" TCA, and just isn't available as $GLOBALS['TCA']. + * + * @return array + */ + public function getTca(): array + { + // To allow casual ExtensionManagementUtility methods that works on $GLOBALS['TCA'] + // to change our fake TCA, just kick original TCA, and reset to original at the end. + $originalTca = $GLOBALS['TCA']; + $GLOBALS['TCA'] = []; + $activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages(); + // First load "full table" files from Configuration/SiteConfigurationTCA + $finder = new Finder(); + foreach ($activePackages as $package) { + try { + $finder->files()->depth(0)->name('*.php')->in($package->getPackagePath() . 'Configuration/SiteConfigurationTCA'); + } catch (\InvalidArgumentException $e) { + // No such directory in this package + continue; + } + foreach ($finder as $fileInfo) { + $GLOBALS['TCA'][substr($fileInfo->getBasename(), 0, -4)] = require $fileInfo->getPathname(); + } + } + // Execute override files from Configuration/TCA/Overrides + foreach ($activePackages as $package) { + try { + $finder->files()->depth(0)->name('*.php')->in($package->getPackagePath() . 'Configuration/SiteConfigurationTCA/Overrides'); + } catch (\InvalidArgumentException $e) { + // No such directory in this package + continue; + } + foreach ($finder as $fileInfo) { + require $fileInfo->getPathname(); + } + } + $result = $GLOBALS['TCA']; + $GLOBALS['TCA'] = $originalTca; + return $result; + } +} diff --git a/typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php b/typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php new file mode 100644 index 0000000000000000000000000000000000000000..de8ea402b2536282e06315952cb90d2627a12f01 --- /dev/null +++ b/typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php @@ -0,0 +1,645 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Controller; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Backend\Configuration\SiteTcaConfiguration; +use TYPO3\CMS\Backend\Exception\SiteValidationErrorException; +use TYPO3\CMS\Backend\Form\FormDataCompiler; +use TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup; +use TYPO3\CMS\Backend\Form\FormResultCompiler; +use TYPO3\CMS\Backend\Form\NodeFactory; +use TYPO3\CMS\Backend\Routing\UriBuilder; +use TYPO3\CMS\Backend\Template\Components\ButtonBar; +use TYPO3\CMS\Backend\Template\ModuleTemplate; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Configuration\SiteConfiguration; +use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Exception\SiteNotFoundException; +use TYPO3\CMS\Core\Http\HtmlResponse; +use TYPO3\CMS\Core\Http\RedirectResponse; +use TYPO3\CMS\Core\Imaging\Icon; +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Messaging\FlashMessage; +use TYPO3\CMS\Core\Messaging\FlashMessageService; +use TYPO3\CMS\Core\Site\Entity\Site; +use TYPO3\CMS\Core\Site\SiteFinder; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Fluid\View\StandaloneView; +use TYPO3Fluid\Fluid\View\ViewInterface; + +/** + * Backend controller: The "Site management" -> "Sites" module + * + * List all site root pages, CRUD site configuration. + */ +class SiteConfigurationController +{ + /** + * @var ModuleTemplate + */ + protected $moduleTemplate; + + /** + * @var ViewInterface + */ + protected $view; + + /** + * @var SiteFinder + */ + protected $siteFinder; + + /** + * Default constructor + */ + public function __construct() + { + $this->siteFinder = GeneralUtility::makeInstance(SiteFinder::class); + $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class); + } + + /** + * Main entry method: Dispatch to other actions - those method names that end with "Action". + * + * @param ServerRequestInterface $request the current request + * @return ResponseInterface the response with the content + */ + public function handleRequest(ServerRequestInterface $request): ResponseInterface + { + $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu'); + $action = $request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'overview'; + $this->initializeView($action); + $result = call_user_func_array([$this, $action . 'Action'], [$request]); + if ($result instanceof ResponseInterface) { + return $result; + } + $this->moduleTemplate->setContent($this->view->render()); + return new HtmlResponse($this->moduleTemplate->renderContent()); + } + + /** + * List pages that have 'is_siteroot' flag set - those that have the globe icon in page tree. + * Link to Add / Edit / Delete for each. + */ + protected function overviewAction(): void + { + $this->configureOverViewDocHeader(); + $allSites = $this->siteFinder->getAllSites(); + $pages = $this->getAllSitePages(); + foreach ($allSites as $identifier => $site) { + $rootPageId = $site->getRootPageId(); + if (isset($pages[$rootPageId])) { + $pages[$rootPageId]['siteIdentifier'] = $identifier; + $pages[$rootPageId]['siteConfiguration'] = $site; + } + } + $this->view->assign('pages', $pages); + } + + /** + * Shows a form to create a new site configuration, or edit an existing one. + * + * @param ServerRequestInterface $request + * @throws \RuntimeException + */ + protected function editAction(ServerRequestInterface $request): void + { + $this->configureEditViewDocHeader(); + + // Put sys_site and friends TCA into global TCA + // @todo: We might be able to get rid of that later + $GLOBALS['TCA'] = array_merge($GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca()); + + $siteIdentifier = $request->getQueryParams()['site'] ?? null; + $pageUid = (int)($request->getQueryParams()['pageUid'] ?? 0); + + if (empty($siteIdentifier) && empty($pageUid)) { + throw new \RuntimeException('Either site identifier to edit a config or page uid to add new config must be set', 1521561148); + } + $isNewConfig = empty($siteIdentifier); + + $defaultValues = []; + if ($isNewConfig) { + $defaultValues['sys_site']['rootPageId'] = $pageUid; + } + + $allSites = $this->siteFinder->getAllSites(); + if (!$isNewConfig && !isset($allSites[$siteIdentifier])) { + throw new \RuntimeException('Existing config for site ' . $siteIdentifier . ' not found', 1521561226); + } + + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $returnUrl = $uriBuilder->buildUriFromRoute('site_configuration'); + + $formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class); + $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup); + $formDataCompilerInput = [ + 'tableName' => 'sys_site', + 'vanillaUid' => $isNewConfig ? $pageUid : $allSites[$siteIdentifier]->getRootPageId(), + 'command' => $isNewConfig ? 'new' : 'edit', + 'returnUrl' => (string)$returnUrl, + 'customData' => [ + 'siteIdentifier' => $isNewConfig ? '' : $siteIdentifier, + ], + 'defaultValues' => $defaultValues, + ]; + $formData = $formDataCompiler->compile($formDataCompilerInput); + $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class); + $formData['renderType'] = 'outerWrapContainer'; + $formResult = $nodeFactory->create($formData)->render(); + // Needed to be set for 'onChange="reload"' and reload on type change to work + $formResult['doSaveFieldName'] = 'doSave'; + $formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class); + $formResultCompiler->mergeResult($formResult); + $formResultCompiler->addCssFiles(); + // Always add rootPageId as additional field to have a reference for new records + $this->view->assign('rootPageId', $isNewConfig ? $pageUid : $allSites[$siteIdentifier]->getRootPageId()); + $this->view->assign('returnUrl', $returnUrl); + $this->view->assign('formEngineHtml', $formResult['html']); + $this->view->assign('formEngineFooter', $formResultCompiler->printNeededJSFunctions()); + } + + /** + * Save incoming data from editAction and redirect to overview or edit + * + * @param ServerRequestInterface $request + * @return ResponseInterface + * @throws \RuntimeException + */ + protected function saveAction(ServerRequestInterface $request): ResponseInterface + { + // Put sys_site and friends TCA into global TCA + // @todo: We might be able to get rid of that later + $GLOBALS['TCA'] = array_merge($GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca()); + + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $siteTca = GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca(); + + $overviewRoute = $uriBuilder->buildUriFromRoute('site_configuration', ['action' => 'overview']); + $parsedBody = $request->getParsedBody(); + if (isset($parsedBody['closeDoc']) && (int)$parsedBody['closeDoc'] === 1) { + // Closing means no save, just redirect to overview + return new RedirectResponse($overviewRoute); + } + $isSave = $parsedBody['_savedok'] ?? $parsedBody['doSave'] ?? false; + $isSaveClose = $parsedBody['_saveandclosedok'] ?? false; + if (!$isSave && !$isSaveClose) { + throw new \RuntimeException('Either save or save and close', 1520370364); + } + + if (!isset($parsedBody['data']['sys_site']) || !is_array($parsedBody['data']['sys_site'])) { + throw new \RuntimeException('No sys_site data or sys_site identifier given', 1521030950); + } + + $data = $parsedBody['data']; + // This can be NEW123 for new records + $pageId = (int)key($data['sys_site']); + $sysSiteRow = current($data['sys_site']); + $siteIdentifier = $sysSiteRow['identifier'] ?? ''; + + $isNewConfiguration = false; + $currentIdentifier = ''; + try { + $currentSite = $this->siteFinder->getSiteByRootPageId($pageId); + $currentSiteConfiguration = $currentSite->getConfiguration(); + $currentIdentifier = $currentSite->getIdentifier(); + } catch (SiteNotFoundException $e) { + $isNewConfiguration = true; + $pageId = (int)$parsedBody['rootPageId']; + if (!$pageId > 0) { + // Early validation of rootPageId - it must always be given and greater than 0 + throw new \RuntimeException('No root page id found', 1521719709); + } + } + + // Validate site identifier and do not store or further process it + $siteIdentifier = $this->validateAndProcessIdentifier($isNewConfiguration, $siteIdentifier, $pageId); + unset($sysSiteRow['identifier']); + + try { + $newSysSiteData = []; + // Hard set rootPageId: This is TCA readOnly and not transmitted by FormEngine, but is also the "uid" of the sys_site record + $newSysSiteData['site']['rootPageId'] = $pageId; + foreach ($sysSiteRow as $fieldName => $fieldValue) { + $type = $siteTca['sys_site']['columns'][$fieldName]['config']['type']; + if ($type === 'input') { + $fieldValue = $this->validateAndProcessValue('sys_site', $fieldName, $fieldValue); + $newSysSiteData['site'][$fieldName] = $fieldValue; + } elseif ($type === 'inline') { + $newSysSiteData['site'][$fieldName] = []; + $childRowIds = GeneralUtility::trimExplode(',', $fieldValue, true); + if (!isset($siteTca['sys_site']['columns'][$fieldName]['config']['foreign_table'])) { + throw new \RuntimeException('No foreign_table found for inline type', 1521555037); + } + $foreignTable = $siteTca['sys_site']['columns'][$fieldName]['config']['foreign_table']; + foreach ($childRowIds as $childRowId) { + $childRowData = []; + if (!isset($data[$foreignTable][$childRowId])) { + if (!empty($currentSiteConfiguration[$fieldName][$childRowId])) { + // A collapsed inline record: Fetch data from existing config + $newSysSiteData['site'][$fieldName][] = $currentSiteConfiguration[$fieldName][$childRowId]; + continue; + } + throw new \RuntimeException('No data found for table ' . $foreignTable . ' with id ' . $childRowId, 1521555177); + } + $childRow = $data[$foreignTable][$childRowId]; + foreach ($childRow as $childFieldName => $childFieldValue) { + if ($childFieldName === 'pid') { + // pid is added by inline by default, but not relevant for yml storage + continue; + } + $type = $siteTca[$foreignTable]['columns'][$childFieldName]['config']['type']; + if ($type === 'input') { + $childRowData[$childFieldName] = $childFieldValue; + } elseif ($type === 'select') { + $childRowData[$childFieldName] = $childFieldValue; + } else { + throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1521555340); + } + } + $newSysSiteData['site'][$fieldName][] = $childRowData; + } + } elseif ($type === 'select') { + $newSysSiteData['site'][$fieldName] = (int)$fieldValue; + } else { + throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1521032781); + } + } + + $newSiteConfiguration = $this->validateFullStructure($newSysSiteData); + + // Persist the configuration + $siteConfigurationManager = GeneralUtility::makeInstance(SiteConfiguration::class, Environment::getConfigPath() . '/sites'); + if (!$isNewConfiguration && $currentIdentifier !== $siteIdentifier) { + $siteConfigurationManager->rename($currentIdentifier, $siteIdentifier); + } + $siteConfigurationManager->write($siteIdentifier, $newSiteConfiguration); + } catch (SiteValidationErrorException $e) { + // Do not store new config if a validation error is thrown, but redirect only to show a generated flash message + } + + $saveRoute = $uriBuilder->buildUriFromRoute('site_configuration', ['action' => 'edit', 'site' => $siteIdentifier]); + if ($isSaveClose) { + return new RedirectResponse($overviewRoute); + } + return new RedirectResponse($saveRoute); + } + + /** + * Validation and processing of site identifier + * + * @param bool $isNew If true, we're dealing with a new record + * @param string $identifier Given identifier to validate and process + * @param int $rootPageId Page uid this identifier is bound to + * @return mixed Verified / modified value + */ + protected function validateAndProcessIdentifier(bool $isNew, string $identifier, int $rootPageId) + { + $languageService = $this->getLanguageService(); + // Normal "eval" processing of field first + $identifier = $this->validateAndProcessValue('sys_site', 'identifier', $identifier); + if ($isNew) { + // Verify no other site with this identifier exists. If so, find a new unique name as + // identifier and show a flash message the identifier has been adapted + try { + $this->siteFinder->getSiteByIdentifier($identifier); + // Force this identifier to be unique + $originalIdentifier = $identifier; + $identifier = $identifier . '-' . str_replace('.', '', uniqid((string)mt_rand(), true)); + $message = sprintf( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.message'), + $originalIdentifier, + $identifier + ); + $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.title'); + $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true); + $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); + $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } catch (SiteNotFoundException $e) { + // Do nothing, this new identifier is ok + } + } else { + // If this is an existing config, the site for this identifier must have the same rootPageId, otherwise + // a user tried to rename a site identifier to a different site that already exists. If so, we do not rename + // the site and show a flash message + try { + $site = $this->siteFinder->getSiteByIdentifier($identifier); + if ($site->getRootPageId() !== $rootPageId) { + // Find original value and keep this + $origSite = $this->siteFinder->getSiteByRootPageId($rootPageId); + $originalIdentifier = $identifier; + $identifier = $origSite->getIdentifier(); + $message = sprintf( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.message'), + $originalIdentifier, + $identifier + ); + $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.title'); + $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true); + $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); + $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + } catch (SiteNotFoundException $e) { + // User is renaming identifier which does not exist yet. That's ok + } + } + return $identifier; + } + + /** + * Simple validation and processing method for incoming form field values. + * + * Note this does not support all TCA "eval" options but only what we really need. + * + * @param string $tableName Table name + * @param string $fieldName Field name + * @param mixed $fieldValue Incoming value from FormEngine + * @return mixed Verified / modified value + * @throws SiteValidationErrorException + * @throws \RuntimeException + */ + protected function validateAndProcessValue(string $tableName, string $fieldName, $fieldValue) + { + $languageService = $this->getLanguageService(); + $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']; + $handledEvals = []; + if (!empty($fieldConfig['eval'])) { + $evalArray = GeneralUtility::trimExplode(',', $fieldConfig['eval'], true); + // Processing + if (in_array('alphanum_x', $evalArray, true)) { + $handledEvals[] = 'alphanum_x'; + $fieldValue = preg_replace('/[^a-zA-Z0-9_-]/', '', $fieldValue); + } + if (in_array('lower', $evalArray, true)) { + $handledEvals[] = 'lower'; + $fieldValue = mb_strtolower($fieldValue, 'utf-8'); + } + if (in_array('trim', $evalArray, true)) { + $handledEvals[] = 'trim'; + $fieldValue = trim($fieldValue); + } + if (in_array('int', $evalArray, true)) { + $handledEvals[] = 'int'; + $fieldValue = (int)$fieldValue; + } + // Validation throws - these should be handled client side already, + // eg. 'required' being set and receiving empty, shouldn't happen server side + if (in_array('required', $evalArray, true)) { + $handledEvals[] = 'required'; + if (empty($fieldValue)) { + $message = sprintf( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.message'), + $fieldName + ); + $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.title'); + $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true); + $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); + $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + throw new SiteValidationErrorException( + 'Field ' . $fieldName . ' is set to required, but received empty.', + 1521726421 + ); + } + } + if (!empty(array_diff($evalArray, $handledEvals))) { + throw new \RuntimeException('At least one not implemented \'eval\' in list ' . $fieldConfig['eval'], 1522491734); + } + } + if (isset($fieldConfig['range']['lower'])) { + $fieldValue = (int)$fieldValue < (int)$fieldConfig['range']['lower'] ? (int)$fieldConfig['range']['lower'] : (int)$fieldValue; + } + if (isset($fieldConfig['range']['upper'])) { + $fieldValue = (int)$fieldValue > (int)$fieldConfig['range']['upper'] ? (int)$fieldConfig['range']['upper'] : (int)$fieldValue; + } + return $fieldValue; + } + + /** + * Last sanitation method after all data has been gathered. Check integrity + * of full record, manipulate if possible, or throw exception if unfixable broken. + * + * @param array $newSysSiteData Incoming data + * @return array Updated data if needed + * @throws \RuntimeException + */ + protected function validateFullStructure(array $newSysSiteData): array + { + $languageService = $this->getLanguageService(); + // Verify there are not two error handlers with the same error code + if (isset($newSysSiteData['site']['errorHandling']) && is_array($newSysSiteData['site']['errorHandling'])) { + $uniqueCriteria = []; + $validChildren = []; + foreach ($newSysSiteData['site']['errorHandling'] as $child) { + if (!isset($child['errorCode'])) { + throw new \RuntimeException('No errorCode found', 1521788518); + } + if (!in_array((int)$child['errorCode'], $uniqueCriteria, true)) { + $uniqueCriteria[] = (int)$child['errorCode']; + $validChildren[] = $child; + } else { + $message = sprintf( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.message'), + $child['errorCode'] + ); + $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.title'); + $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true); + $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); + $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + } + $newSysSiteData['site']['errorHandling'] = $validChildren; + } + + // Verify there is only one inline child per sys_language record configured. + if (!isset($newSysSiteData['site']['languages']) || !is_array($newSysSiteData['site']['languages']) || count($newSysSiteData['site']['languages']) < 1) { + throw new \RuntimeException( + 'No default language definition found. The interface does not allow this. Aborting', + 1521789306 + ); + } + $uniqueCriteria = []; + $validChildren = []; + foreach ($newSysSiteData['site']['languages'] as $child) { + if (!isset($child['languageId'])) { + throw new \RuntimeException('languageId not found', 1521789455); + } + if (!in_array((int)$child['languageId'], $uniqueCriteria, true)) { + $uniqueCriteria[] = (int)$child['languageId']; + $validChildren[] = $child; + } else { + $message = sprintf( + $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.title'), + $child['languageId'] + ); + $messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.title'); + $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true); + $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); + $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); + $defaultFlashMessageQueue->enqueue($flashMessage); + } + } + $newSysSiteData['site']['languages'] = $validChildren; + + return $newSysSiteData; + } + + /** + * Delete an existing configuration + * + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + protected function deleteAction(ServerRequestInterface $request): ResponseInterface + { + $siteIdentifier = $request->getQueryParams()['site'] ?? ''; + if (empty($siteIdentifier)) { + throw new \RuntimeException('Not site identifier given', 1521565182); + } + // Verify site does exist, method throws if not + GeneralUtility::makeInstance(SiteConfiguration::class, Environment::getConfigPath() . '/sites')->delete($siteIdentifier); + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $overviewRoute = $uriBuilder->buildUriFromRoute('site_configuration', ['action' => 'overview']); + return new RedirectResponse($overviewRoute); + } + + /** + * Sets up the Fluid View. + * + * @param string $templateName + */ + protected function initializeView(string $templateName): void + { + $this->view = GeneralUtility::makeInstance(StandaloneView::class); + $this->view->setTemplate($templateName); + $this->view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/SiteConfiguration']); + $this->view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials']); + $this->view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']); + } + + /** + * Create document header buttons of "edit" action + */ + protected function configureEditViewDocHeader(): void + { + $iconFactory = $this->moduleTemplate->getIconFactory(); + $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar(); + $lang = $this->getLanguageService(); + $closeButton = $buttonBar->makeLinkButton() + ->setHref('#') + ->setClasses('t3js-editform-close') + ->setTitle($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.closeDoc')) + ->setIcon($iconFactory->getIcon('actions-close', Icon::SIZE_SMALL)); + $saveButton = $buttonBar->makeInputButton() + ->setTitle($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.saveDoc')) + ->setName('_savedok') + ->setValue('1') + ->setForm('siteConfigurationController') + ->setIcon($iconFactory->getIcon('actions-document-save', Icon::SIZE_SMALL)); + $saveAndCloseButton = $buttonBar->makeInputButton() + ->setName('_saveandclosedok') + ->setClasses('t3js-editform-submitButton') + ->setValue('1') + ->setForm('siteConfigurationController') + ->setTitle($lang->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.saveCloseDoc')) + ->setIcon($iconFactory->getIcon('actions-document-save-close', Icon::SIZE_SMALL)); + $saveSplitButton = $buttonBar->makeSplitButton(); + $saveSplitButton->addItem($saveButton, true); + $saveSplitButton->addItem($saveAndCloseButton); + $buttonBar->addButton($closeButton); + $buttonBar->addButton($saveSplitButton, ButtonBar::BUTTON_POSITION_LEFT, 2); + } + + /** + * Create document header buttons of "overview" action + */ + protected function configureOverViewDocHeader(): void + { + $iconFactory = $this->moduleTemplate->getIconFactory(); + $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar(); + $reloadButton = $buttonBar->makeLinkButton() + ->setHref(GeneralUtility::getIndpEnv('REQUEST_URI')) + ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.reload')) + ->setIcon($iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL)); + $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT); + if ($this->getBackendUser()->mayMakeShortcut()) { + $getVars = ['id', 'route']; + $shortcutButton = $buttonBar->makeShortcutButton() + ->setModuleName('site_configuration') + ->setGetVariables($getVars); + $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT); + } + } + + /** + * Returns a list of pages that have 'is_siteroot' set + * + * @return array + */ + protected function getAllSitePages(): array + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); + $statement = $queryBuilder + ->select('*') + ->from('pages') + ->where( + $queryBuilder->expr()->eq('sys_language_uid', 0), + $queryBuilder->expr()->orX( + $queryBuilder->expr()->eq('pid', 0), + $queryBuilder->expr()->eq('is_siteroot', 1) + ) + ) + ->orderBy('pid') + ->addOrderBy('sorting') + ->execute(); + + $pages = []; + while ($row = $statement->fetch()) { + $row['rootline'] = BackendUtility::BEgetRootLine((int)$row['uid']); + array_pop($row['rootline']); + $row['rootline'] = array_reverse($row['rootline']); + $i = 0; + foreach ($row['rootline'] as &$record) { + $record['margin'] = $i++ * 20; + } + $pages[(int)$row['uid']] = $row; + } + return $pages; + } + + /** + * @return LanguageService + */ + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + /** + * @return BackendUserAuthentication + */ + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/typo3/sysext/backend/Classes/Controller/SiteInlineAjaxController.php b/typo3/sysext/backend/Classes/Controller/SiteInlineAjaxController.php new file mode 100644 index 0000000000000000000000000000000000000000..d732aeeeed9577bc8e54199baf02cb7d25ed3226 --- /dev/null +++ b/typo3/sysext/backend/Classes/Controller/SiteInlineAjaxController.php @@ -0,0 +1,405 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Controller; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Backend\Configuration\SiteTcaConfiguration; +use TYPO3\CMS\Backend\Form\FormDataCompiler; +use TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup; +use TYPO3\CMS\Backend\Form\InlineStackProcessor; +use TYPO3\CMS\Backend\Form\NodeFactory; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; +use TYPO3\CMS\Core\Http\JsonResponse; +use TYPO3\CMS\Core\Utility\ArrayUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\MathUtility; + +/** + * Site configuration FormEngine controller class. Receives inline "edit" and "new" + * commands to expand / create site configuration inline records + */ +class SiteInlineAjaxController extends AbstractFormEngineAjaxController +{ + /** + * Default constructor + */ + public function __construct() + { + // Bring site TCA into global scope. + // @todo: We might be able to get rid of that later + $GLOBALS['TCA'] = array_merge($GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca()); + } + + /** + * Inline "create" new child of site configuration child records + * + * @param ServerRequestInterface $request + * @return ResponseInterface + * @throws \RuntimeException + */ + public function newInlineChildAction(ServerRequestInterface $request): ResponseInterface + { + $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax']; + $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']); + $domObjectId = $ajaxArguments[0]; + $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); + $childChildUid = null; + if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) { + $childChildUid = (int)$ajaxArguments[1]; + } + // Parse the DOM identifier, add the levels to the structure stack + $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); + $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId); + $inlineStackProcessor->injectAjaxConfiguration($parentConfig); + $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0); + // Parent, this table embeds the child table + $parent = $inlineStackProcessor->getStructureLevel(-1); + // Child, a record from this table should be rendered + $child = $inlineStackProcessor->getUnstableStructure(); + if (MathUtility::canBeInterpretedAsInteger($child['uid'])) { + // If uid comes in, it is the id of the record neighbor record "create after" + $childVanillaUid = -1 * abs((int)$child['uid']); + } else { + // Else inline first Pid is the storage pid of new inline records + $childVanillaUid = (int)$inlineFirstPid; + } + $childTableName = $parentConfig['foreign_table']; + + $defaultDatabaseRow = []; + if ($childTableName === 'sys_site_language') { + // Feed new sys_site_language row with data from sys_language record if possible + if ($childChildUid > 0) { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_language'); + $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class); + $row = $queryBuilder->select('*')->from('sys_language') + ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($childChildUid, \PDO::PARAM_INT))) + ->execute()->fetch(); + if (empty($row)) { + throw new \RuntimeException('Referenced sys_language row not found', 1521783937); + } + if (!empty($row['language_isocode'])) { + $defaultDatabaseRow['iso-639-1'] = $row['language_isocode']; + $defaultDatabaseRow['base'] = '/' . $row['language_isocode'] . '/'; + } + if (!empty($row['flag']) && $row['flag'] === 'multiple') { + $defaultDatabaseRow['flag'] = 'global'; + } elseif (!empty($row)) { + $defaultDatabaseRow['flag'] = $row['flag']; + } + if (!empty($row['title'])) { + $defaultDatabaseRow['title'] = $row['title']; + } + } + } + + $formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class); + $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup); + $formDataCompilerInput = [ + 'command' => 'new', + 'tableName' => $childTableName, + 'vanillaUid' => $childVanillaUid, + 'databaseRow' => $defaultDatabaseRow, + 'isInlineChild' => true, + 'inlineStructure' => $inlineStackProcessor->getStructure(), + 'inlineFirstPid' => $inlineFirstPid, + 'inlineParentUid' => $parent['uid'], + 'inlineParentTableName' => $parent['table'], + 'inlineParentFieldName' => $parent['field'], + 'inlineParentConfig' => $parentConfig, + 'inlineTopMostParentUid' => $inlineTopMostParent['uid'], + 'inlineTopMostParentTableName' => $inlineTopMostParent['table'], + 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'], + ]; + if ($childChildUid) { + $formDataCompilerInput['inlineChildChildUid'] = $childChildUid; + } + $childData = $formDataCompiler->compile($formDataCompilerInput); + + if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) { + throw new \RuntimeException('useCombination not implemented in sites module', 1522493094); + } + + $childData['inlineParentUid'] = (int)$parent['uid']; + $childData['renderType'] = 'inlineRecordContainer'; + $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class); + $childResult = $nodeFactory->create($childData)->render(); + + $jsonArray = [ + 'data' => '', + 'stylesheetFiles' => [], + 'scriptCall' => [], + ]; + + // The HTML-object-id's prefix of the dynamically created record + $objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid); + $objectPrefix = $objectName . '-' . $child['table']; + $objectId = $objectPrefix . '-' . $childData['databaseRow']['uid']; + $expandSingle = $parentConfig['appearance']['expandSingle']; + if (!$child['uid']) { + $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);'; + $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');'; + } else { + $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);'; + $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');'; + } + $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult); + if ($parentConfig['appearance']['useSortable']) { + $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid); + $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');'; + } + if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) { + $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');'; + } + // Fade out and fade in the new record in the browser view to catch the user's eye + $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');'; + + return new JsonResponse($jsonArray); + } + + /** + * Show the details of site configuration child records. + * + * @param ServerRequestInterface $request + * @return ResponseInterface + * @throws \RuntimeException + */ + public function openInlineChildAction(ServerRequestInterface $request): ResponseInterface + { + $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax']; + + $domObjectId = $ajaxArguments[0]; + $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); + $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']); + + // Parse the DOM identifier, add the levels to the structure stack + $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); + $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId); + $inlineStackProcessor->injectAjaxConfiguration($parentConfig); + + // Parent, this table embeds the child table + $parent = $inlineStackProcessor->getStructureLevel(-1); + $parentFieldName = $parent['field']; + + // Set flag in config so that only the fields are rendered + // @todo: Solve differently / rename / whatever + $parentConfig['renderFieldsOnly'] = true; + + $parentData = [ + 'processedTca' => [ + 'columns' => [ + $parentFieldName => [ + 'config' => $parentConfig, + ], + ], + ], + 'tableName' => $parent['table'], + 'inlineFirstPid' => $inlineFirstPid, + // Hand over given original return url to compile stack. Needed if inline children compile links to + // another view (eg. edit metadata in a nested inline situation like news with inline content element image), + // so the back link is still the link from the original request. See issue #82525. This is additionally + // given down in TcaInline data provider to compiled children data. + 'returnUrl' => $parentConfig['originalReturnUrl'], + ]; + + // Child, a record from this table should be rendered + $child = $inlineStackProcessor->getUnstableStructure(); + + $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure()); + + $childData['inlineParentUid'] = (int)$parent['uid']; + $childData['renderType'] = 'inlineRecordContainer'; + $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class); + $childResult = $nodeFactory->create($childData)->render(); + + $jsonArray = [ + 'data' => '', + 'stylesheetFiles' => [], + 'scriptCall' => [], + ]; + + // The HTML-object-id's prefix of the dynamically created record + $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table']; + $objectId = $objectPrefix . '-' . (int)$child['uid']; + $expandSingle = $parentConfig['appearance']['expandSingle']; + $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);'; + if ($parentConfig['foreign_unique']) { + $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');'; + } + $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult); + if ($parentConfig['appearance']['useSortable']) { + $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid); + $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');'; + } + if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) { + $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');'; + } + + return new JsonResponse($jsonArray); + } + + /** + * Compile a full child record + * + * @param array $parentData Result array of parent + * @param string $parentFieldName Name of parent field + * @param int $childUid Uid of child to compile + * @param array $inlineStructure Current inline structure + * @return array Full result array + * @throws \RuntimeException + * + * @todo: This clones methods compileChild from TcaInline Provider. Find a better abstraction + * @todo: to also encapsulate the more complex scenarios with combination child and friends. + */ + protected function compileChild(array $parentData, string $parentFieldName, int $childUid, array $inlineStructure): array + { + $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config']; + + $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); + $inlineStackProcessor->initializeByGivenStructure($inlineStructure); + $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0); + + // @todo: do not use stack processor here ... + $child = $inlineStackProcessor->getUnstableStructure(); + $childTableName = $child['table']; + + $formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class); + $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup); + $formDataCompilerInput = [ + 'command' => 'edit', + 'tableName' => $childTableName, + 'vanillaUid' => (int)$childUid, + 'returnUrl' => $parentData['returnUrl'], + 'isInlineChild' => true, + 'inlineStructure' => $inlineStructure, + 'inlineFirstPid' => $parentData['inlineFirstPid'], + 'inlineParentConfig' => $parentConfig, + 'isInlineAjaxOpeningContext' => true, + + // values of the current parent element + // it is always a string either an id or new... + 'inlineParentUid' => $parentData['databaseRow']['uid'], + 'inlineParentTableName' => $parentData['tableName'], + 'inlineParentFieldName' => $parentFieldName, + + // values of the top most parent element set on first level and not overridden on following levels + 'inlineTopMostParentUid' => $inlineTopMostParent['uid'], + 'inlineTopMostParentTableName' => $inlineTopMostParent['table'], + 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'], + ]; + if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) { + throw new \RuntimeException('useCombination not implemented in sites module', 1522493095); + } + return $formDataCompiler->compile($formDataCompilerInput); + } + + /** + * Merge stuff from child array into json array. + * This method is needed since ajax handling methods currently need to put scriptCalls before and after child code. + * + * @param array $jsonResult Given json result + * @param array $childResult Given child result + * @return array Merged json array + */ + protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult): array + { + $jsonResult['data'] .= $childResult['html']; + $jsonResult['stylesheetFiles'] = []; + foreach ($childResult['stylesheetFiles'] as $stylesheetFile) { + $jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile); + } + if (!empty($childResult['inlineData'])) { + $jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');'; + } + if (!empty($childResult['additionalJavaScriptSubmit'])) { + $additionalJavaScriptSubmit = implode('', $childResult['additionalJavaScriptSubmit']); + $additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit); + $jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");'; + } + foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) { + $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost; + } + if (!empty($childResult['additionalInlineLanguageLabelFiles'])) { + $labels = []; + foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) { + ArrayUtility::mergeRecursiveWithOverrule( + $labels, + $this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile) + ); + } + $javaScriptCode = []; + $javaScriptCode[] = 'if (typeof TYPO3 === \'undefined\' || typeof TYPO3.lang === \'undefined\') {'; + $javaScriptCode[] = ' TYPO3.lang = {}'; + $javaScriptCode[] = '}'; + $javaScriptCode[] = 'var additionalInlineLanguageLabels = ' . json_encode($labels) . ';'; + $javaScriptCode[] = 'for (var attributeName in additionalInlineLanguageLabels) {'; + $javaScriptCode[] = ' if (typeof TYPO3.lang[attributeName] === \'undefined\') {'; + $javaScriptCode[] = ' TYPO3.lang[attributeName] = additionalInlineLanguageLabels[attributeName]'; + $javaScriptCode[] = ' }'; + $javaScriptCode[] = '}'; + + $jsonResult['scriptCall'][] = implode(LF, $javaScriptCode); + } + $requireJsModule = $this->createExecutableStringRepresentationOfRegisteredRequireJsModules($childResult); + $jsonResult['scriptCall'] = array_merge($requireJsModule, $jsonResult['scriptCall']); + + return $jsonResult; + } + + /** + * Inline ajax helper method. + * + * Validates the config that is transferred over the wire to provide the + * correct TCA config for the parent table + * + * @param string $contextString + * @throws \RuntimeException + * @return array + */ + protected function extractSignedParentConfigFromRequest(string $contextString): array + { + if ($contextString === '') { + throw new \RuntimeException('Empty context string given', 1522771624); + } + $context = json_decode($contextString, true); + if (empty($context['config'])) { + throw new \RuntimeException('Empty context config section given', 1522771632); + } + if (!hash_equals(GeneralUtility::hmac(json_encode($context['config']), 'InlineContext'), $context['hmac'])) { + throw new \RuntimeException('Hash does not validate', 1522771640); + } + return $context['config']; + } + + /** + * Get inlineFirstPid from a given objectId string + * + * @param string $domObjectId The id attribute of an element + * @return int|null Pid or null + */ + protected function getInlineFirstPidFromDomObjectId(string $domObjectId): ?int + { + // Substitute FlexForm addition and make parsing a bit easier + $domObjectId = str_replace('---', ':', $domObjectId); + // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>) + $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/'; + if (preg_match($pattern, $domObjectId, $match)) { + return (int)$match[1]; + } + return null; + } +} diff --git a/typo3/sysext/backend/Classes/Exception/SiteValidationErrorException.php b/typo3/sysext/backend/Classes/Exception/SiteValidationErrorException.php new file mode 100644 index 0000000000000000000000000000000000000000..e7a84fa399f10c03a5b307cce0f4541db92a6d13 --- /dev/null +++ b/typo3/sysext/backend/Classes/Exception/SiteValidationErrorException.php @@ -0,0 +1,25 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Exception; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Backend\Exception; + +/** + * Exception thrown if site configuration for a page is not found + */ +class SiteValidationErrorException extends Exception +{ +} diff --git a/typo3/sysext/backend/Classes/Form/FieldInformation/SiteConfiguration.php b/typo3/sysext/backend/Classes/Form/FieldInformation/SiteConfiguration.php new file mode 100644 index 0000000000000000000000000000000000000000..648f22ff1e7d3446a89713899319187f67e33c3d --- /dev/null +++ b/typo3/sysext/backend/Classes/Form/FieldInformation/SiteConfiguration.php @@ -0,0 +1,51 @@ +<?php +declare(strict_types=1); + +namespace TYPO3\CMS\Backend\Form\FieldInformation; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Backend\Form\AbstractNode; +use TYPO3\CMS\Core\Localization\LanguageService; + +/** + * Provides field information texts for form engine fields concerning site configuration module + */ +class SiteConfiguration extends AbstractNode +{ + /** + * Handler for single nodes + * + * @return array As defined in initializeResultArray() of AbstractNode + */ + public function render() + { + $resultArray = $this->initializeResultArray(); + $fieldInformationText = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/siteconfiguration_fieldinformation.xlf:' . $this->data['tableName'] . '.' . $this->data['fieldName']); + if ($fieldInformationText !== $this->data['fieldName']) { + $resultArray['html'] = $fieldInformationText; + } + return $resultArray; + } + + /** + * Returns the LanguageService + * + * @return LanguageService + */ + protected function getLanguageService() + { + return $GLOBALS['LANG']; + } +} diff --git a/typo3/sysext/backend/Classes/Form/FormDataGroup/SiteConfigurationDataGroup.php b/typo3/sysext/backend/Classes/Form/FormDataGroup/SiteConfigurationDataGroup.php new file mode 100644 index 0000000000000000000000000000000000000000..f5b47e9b3cf7c19c79586aeabda1d242e7a97e1f --- /dev/null +++ b/typo3/sysext/backend/Classes/Form/FormDataGroup/SiteConfigurationDataGroup.php @@ -0,0 +1,47 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Form\FormDataGroup; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Backend\Form\FormDataGroupInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * A data provider group for site and language configuration. + * + * This data group is for data fetched from sites yml files, + * it is fed by "fake TCA" since there are no real db records. + * + * It's similar to "fullDatabaseRecord", with some unused TCA types + * kicked out and some own data providers for record data and inline handling. + */ +class SiteConfigurationDataGroup implements FormDataGroupInterface +{ + /** + * Compile form data + * + * @param array $result Initialized result array + * @return array Result filled with data + * @throws \UnexpectedValueException + */ + public function compile(array $result) + { + $orderedProviderList = GeneralUtility::makeInstance(OrderedProviderList::class); + $orderedProviderList->setProviderList( + $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['siteConfiguration'] + ); + return $orderedProviderList->compile($result); + } +} diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/SiteDatabaseEditRow.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/SiteDatabaseEditRow.php new file mode 100644 index 0000000000000000000000000000000000000000..b4f1a60558f2e2589409dcf14222904b991ca3bf --- /dev/null +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/SiteDatabaseEditRow.php @@ -0,0 +1,72 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Form\FormDataProvider; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Backend\Form\FormDataProviderInterface; +use TYPO3\CMS\Core\Site\SiteFinder; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Special data provider for the sites configuration module. + * + * Fetch "row" data from yml file and set as 'databaseRow' + */ +class SiteDatabaseEditRow implements FormDataProviderInterface +{ + /** + * First level of ['customData']['siteData'] to ['databaseRow'] + * + * @param array $result + * @return array + * @throws \RuntimeException + */ + public function addData(array $result): array + { + if ($result['command'] !== 'edit' || !empty($result['databaseRow'])) { + return $result; + } + + $tableName = $result['tableName']; + $siteFinder = GeneralUtility::makeInstance(SiteFinder::class); + if ($tableName === 'sys_site') { + $siteConfigurationForPageUid = (int)$result['vanillaUid']; + $rowData = $siteFinder->getSiteByRootPageId($siteConfigurationForPageUid)->getConfiguration(); + $result['databaseRow']['uid'] = $rowData['rootPageId']; + $result['databaseRow']['identifier'] = $result['customData']['siteIdentifier']; + } elseif ($tableName === 'sys_site_errorhandling' || $tableName === 'sys_site_language') { + $siteConfigurationForPageUid = (int)($result['inlineTopMostParentUid'] ?? $result['inlineParentUid']); + $rowData = $siteFinder->getSiteByRootPageId($siteConfigurationForPageUid)->getConfiguration(); + $parentFieldName = $result['inlineParentFieldName']; + if (!isset($rowData[$parentFieldName])) { + throw new \RuntimeException('Field "' . $parentFieldName . '" not found', 1520886092); + } + $rowData = $rowData[$parentFieldName][$result['vanillaUid']]; + $result['databaseRow']['uid'] = $result['vanillaUid']; + } else { + throw new \RuntimeException('Other tables not implemented', 1520886234); + } + + foreach ($rowData as $fieldName => $value) { + // Flat values only - databaseRow has no "tree" + if (!is_array($value)) { + $result['databaseRow'][$fieldName] = $value; + } + } + // All "records" are always on pid 0 + $result['databaseRow']['pid'] = 0; + return $result; + } +} diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/SiteTcaInline.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/SiteTcaInline.php new file mode 100644 index 0000000000000000000000000000000000000000..9b6e5400f2c04e9ba589b0d879732c9939e64481 --- /dev/null +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/SiteTcaInline.php @@ -0,0 +1,309 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Form\FormDataProvider; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Backend\Form\FormDataCompiler; +use TYPO3\CMS\Backend\Form\FormDataGroup\OnTheFly; +use TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup; +use TYPO3\CMS\Backend\Form\FormDataProviderInterface; +use TYPO3\CMS\Backend\Form\InlineStackProcessor; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Site\SiteFinder; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Special data provider for the sites configuration module. + * + * Handle inline children of 'sys_site" + */ +class SiteTcaInline extends AbstractDatabaseRecordProvider implements FormDataProviderInterface +{ + /** + * Resolve inline fields + * + * @param array $result + * @return array + */ + public function addData(array $result): array + { + $result = $this->addInlineFirstPid($result); + foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) { + if (!$this->isInlineField($fieldConfig)) { + continue; + } + $childTableName = $fieldConfig['config']['foreign_table']; + if ($childTableName !== 'sys_site_errorhandling' && $childTableName !== 'sys_site_language') { + throw new \RuntimeException('Inline relation to other tables not implemented', 1522494737); + } + $result['processedTca']['columns'][$fieldName]['children'] = []; + $result = $this->resolveSiteRelatedChildren($result, $fieldName); + $result = $this->addForeignSelectorAndUniquePossibleRecords($result, $fieldName); + } + + return $result; + } + + /** + * Is column of type "inline" + * + * @param array $fieldConfig + * @return bool + */ + protected function isInlineField(array $fieldConfig): bool + { + return !empty($fieldConfig['config']['type']) && $fieldConfig['config']['type'] === 'inline'; + } + + /** + * The "entry" pid for inline records. Nested inline records can potentially hang around on different + * pid's, but the entry pid is needed for AJAX calls, so that they would know where the action takes place on the page structure. + * + * @param array $result Incoming result + * @return array Modified result + * @todo: Find out when and if this is different from 'effectivePid' + */ + protected function addInlineFirstPid(array $result): array + { + if ($result['inlineFirstPid'] === null) { + $table = $result['tableName']; + $row = $result['databaseRow']; + // If the parent is a page, use the uid(!) of the (new?) page as pid for the child records: + if ($table === 'pages') { + $liveVersionId = BackendUtility::getLiveVersionIdOfRecord('pages', $row['uid']); + $pid = $liveVersionId === null ? $row['uid'] : $liveVersionId; + } elseif ($row['pid'] < 0) { + $prevRec = BackendUtility::getRecord($table, abs($row['pid'])); + $pid = $prevRec['pid']; + } else { + $pid = $row['pid']; + } + $pageRecord = BackendUtility::getRecord('pages', $pid); + if ((int)$pageRecord[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] > 0) { + $pid = (int)$pageRecord[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']]; + } + $result['inlineFirstPid'] = (int)$pid; + } + return $result; + } + + /** + * Substitute the value in databaseRow of this inline field with an array + * that contains the databaseRows of currently connected records and some meta information. + * + * @param array $result Result array + * @param string $fieldName Current handle field name + * @return array Modified item array + */ + protected function resolveSiteRelatedChildren(array $result, string $fieldName): array + { + $connectedUids = []; + if ($result['command'] === 'edit') { + $siteConfigurationForPageUid = (int)$result['databaseRow']['rootPageId'][0]; + $siteFinder = GeneralUtility::makeInstance(SiteFinder::class); + $site = $siteFinder->getSiteByRootPageId($siteConfigurationForPageUid); + $siteConfiguration = $site ? $site->getConfiguration() : []; + if (is_array($siteConfiguration[$fieldName])) { + $connectedUids = array_keys($siteConfiguration[$fieldName]); + } + } + + // If we are dealing with sys_site_language, we *always* force a relation to sys_language "0" + $foreignTable = $result['processedTca']['columns'][$fieldName]['config']['foreign_table']; + if ($foreignTable === 'sys_site_language' && $result['command'] === 'new') { + // If new, just add a new default child + $child = $this->compileDefaultSysSiteLanguageChild($result, $fieldName); + $connectedUids[] = $child['databaseRow']['uid']; + $result['processedTca']['columns'][$fieldName]['children'][] = $child; + } + + $result['databaseRow'][$fieldName] = implode(',', $connectedUids); + if ($result['inlineCompileExistingChildren']) { + foreach ($connectedUids as $uid) { + if (strpos((string)$uid, 'NEW') !== 0) { + $compiledChild = $this->compileChild($result, $fieldName, $uid); + $result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild; + } + } + } + + // If we are dealing with sys_site_language, we *always* force a relation to sys_language "0" + if ($foreignTable === 'sys_site_language' && $result['command'] === 'edit') { + // If edit, find out if a child using sys_language "0" exists, else add it on top + $defaultSysSiteLanguageChildFound = false; + foreach ($result['processedTca']['columns'][$fieldName]['children'] as $child) { + if (isset($child['databaseRow']['languageId']) && (int)$child['databaseRow']['languageId'][0] == 0) { + $defaultSysSiteLanguageChildFound = true; + } + } + if (!$defaultSysSiteLanguageChildFound) { + // Compile and add child as first child + $child = $this->compileDefaultSysSiteLanguageChild($result, $fieldName); + $result['databaseRow'][$fieldName] = $child['databaseRow']['uid'] . ',' . $result['databaseRow'][$fieldName]; + array_unshift($result['processedTca']['columns'][$fieldName]['children'], $child); + } + } + + return $result; + } + + /** + * If there is a foreign_selector or foreign_unique configuration, fetch + * the list of possible records that can be connected and attach them to the + * inline configuration. + * + * @param array $result Result array + * @param string $fieldName Current handle field name + * @return array Modified item array + */ + protected function addForeignSelectorAndUniquePossibleRecords(array $result, string $fieldName): array + { + if (!is_array($result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'])) { + return $result; + } + + $selectorOrUniqueConfiguration = $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration']; + $foreignFieldName = $selectorOrUniqueConfiguration['fieldName']; + $selectorOrUniquePossibleRecords = []; + + if ($selectorOrUniqueConfiguration['config']['type'] === 'select') { + // Compile child table data for this field only + $selectDataInput = [ + 'tableName' => $result['processedTca']['columns'][$fieldName]['config']['foreign_table'], + 'command' => 'new', + // Since there is no existing record that may have a type, it does not make sense to + // do extra handling of pageTsConfig merged here. Just provide "parent" pageTS as is + 'pageTsConfig' => $result['pageTsConfig'], + 'userTsConfig' => $result['userTsConfig'], + 'databaseRow' => $result['databaseRow'], + 'processedTca' => [ + 'ctrl' => [], + 'columns' => [ + $foreignFieldName => [ + 'config' => $selectorOrUniqueConfiguration['config'], + ], + ], + ], + 'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'], + ]; + $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class); + $formDataGroup->setProviderList([TcaSelectItems::class]); + $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup); + $compilerResult = $formDataCompiler->compile($selectDataInput); + $selectorOrUniquePossibleRecords = $compilerResult['processedTca']['columns'][$foreignFieldName]['config']['items']; + } + + $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniquePossibleRecords'] = $selectorOrUniquePossibleRecords; + + return $result; + } + + /** + * Compile a full child record + * + * @param array $result Result array of parent + * @param string $parentFieldName Name of parent field + * @param int $childUid Uid of child to compile + * @return array Full result array + */ + protected function compileChild(array $result, string $parentFieldName, int $childUid): array + { + $parentConfig = $result['processedTca']['columns'][$parentFieldName]['config']; + $childTableName = $parentConfig['foreign_table']; + + $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); + $inlineStackProcessor->initializeByGivenStructure($result['inlineStructure']); + $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0); + + $formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class); + $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup); + $formDataCompilerInput = [ + 'command' => 'edit', + 'tableName' => $childTableName, + 'vanillaUid' => $childUid, + // Give incoming returnUrl down to children so they generate a returnUrl back to + // the originally opening record, also see "originalReturnUrl" in inline container + // and FormInlineAjaxController + 'returnUrl' => $result['returnUrl'], + 'isInlineChild' => true, + 'inlineStructure' => $result['inlineStructure'], + 'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'], + 'inlineFirstPid' => $result['inlineFirstPid'], + 'inlineParentConfig' => $parentConfig, + + // values of the current parent element + // it is always a string either an id or new... + 'inlineParentUid' => $result['databaseRow']['uid'], + 'inlineParentTableName' => $result['tableName'], + 'inlineParentFieldName' => $parentFieldName, + + // values of the top most parent element set on first level and not overridden on following levels + 'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'], + 'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'], + 'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'], + ]; + + if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) { + throw new \RuntimeException('useCombination not implemented in sites module', 1522493097); + } + return $formDataCompiler->compile($formDataCompilerInput); + } + + /** + * Compile default sys_site_language child using sys_language uid "0" + * + * @param array $result + * @param string $parentFieldName + * @return array + */ + protected function compileDefaultSysSiteLanguageChild(array $result, string $parentFieldName): array + { + $parentConfig = $result['processedTca']['columns'][$parentFieldName]['config']; + $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); + $inlineStackProcessor->initializeByGivenStructure($result['inlineStructure']); + $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0); + $formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class); + $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup); + $formDataCompilerInput = [ + 'command' => 'new', + 'tableName' => 'sys_site_language', + 'vanillaUid' => $result['inlineFirstPid'], + 'returnUrl' => $result['returnUrl'], + 'isInlineChild' => true, + 'inlineStructure' => [], + 'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'], + 'inlineFirstPid' => $result['inlineFirstPid'], + 'inlineParentConfig' => $parentConfig, + 'inlineParentUid' => $result['databaseRow']['uid'], + 'inlineParentTableName' => $result['tableName'], + 'inlineParentFieldName' => $parentFieldName, + 'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'], + 'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'], + 'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'], + // The sys_language uid 0 + 'inlineChildChildUid' => 0, + ]; + return $formDataCompiler->compile($formDataCompilerInput); + } + + /** + * @return BackendUserAuthentication + */ + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/SiteTcaSelectItems.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/SiteTcaSelectItems.php new file mode 100644 index 0000000000000000000000000000000000000000..958c92953776e6e11e4f73fead28fdec2f7c75aa --- /dev/null +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/SiteTcaSelectItems.php @@ -0,0 +1,63 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Form\FormDataProvider; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Backend\Form\FormDataProviderInterface; +use TYPO3\CMS\Core\Localization\Locales; +use TYPO3\CMS\Core\Service\IsoCodeService; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Special data provider for the sites configuration module. + * + * Resolve some specialities of the "site configuration" + */ +class SiteTcaSelectItems implements FormDataProviderInterface +{ + /** + * Resolve select items for + * * 'sys_site_language' -> 'typo3language' + * + * @param array $result + * @return array + * @throws \UnexpectedValueException + */ + public function addData(array $result): array + { + $table = $result['tableName']; + if ($table !== 'sys_site_language') { + return $result; + } + + // Available languages from Locales class put as "typo3Language" items + $locales = GeneralUtility::makeInstance(Locales::class); + $languages = $locales->getLanguages(); + $items = []; + foreach ($languages as $key => $label) { + $items[] = [ + 0 => $label, + 1 => $key, + ]; + } + $result['processedTca']['columns']['typo3Language']['config']['items'] = $items; + + // Available ISO-639-1 codes fetch from service class and put as "iso-639-1" items + $isoItems = GeneralUtility::makeInstance(IsoCodeService::class)->renderIsoCodeSelectDropdown(['items' => []]); + $result['processedTca']['columns']['iso-639-1']['config']['items'] = $isoItems['items']; + + return $result; + } +} diff --git a/typo3/sysext/backend/Classes/Middleware/SiteResolver.php b/typo3/sysext/backend/Classes/Middleware/SiteResolver.php new file mode 100644 index 0000000000000000000000000000000000000000..f28021cb48c57ddaf43c9730c71f016162cac84a --- /dev/null +++ b/typo3/sysext/backend/Classes/Middleware/SiteResolver.php @@ -0,0 +1,61 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Middleware; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use TYPO3\CMS\Core\Exception\SiteNotFoundException; +use TYPO3\CMS\Core\Site\SiteFinder; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Usually called after the route object is resolved, however, this is not possible yet as this happens + * within the RequestHandler/RouteDispatcher right now and should go away. + * + * This middleware checks for a "id" parameter. If present, it adds a site information to this page ID. + * + * Very useful for all "Web" related modules to resolve all available languages for a site. + */ +class SiteResolver implements MiddlewareInterface +{ + /** + * Resolve the site information by checking the page ID ("id" parameter) which is typically used in BE modules + * of type "web". + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $finder = GeneralUtility::makeInstance(SiteFinder::class); + $site = null; + $pageId = (int)($request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0); + + // Check if we have a _GET/_POST parameter for "id", then a site information can be resolved based. + if ($pageId > 0) { + try { + $site = $finder->getSiteByPageId($pageId); + $request = $request->withAttribute('site', $site); + $GLOBALS['TYPO3_REQUEST'] = $request; + } catch (SiteNotFoundException $e) { + } + } + return $handler->handle($request); + } +} diff --git a/typo3/sysext/backend/Classes/Routing/PageUriBuilder.php b/typo3/sysext/backend/Classes/Routing/PageUriBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..13e5cca310bb8205c9b44563d2dffea8e4b9e0fa --- /dev/null +++ b/typo3/sysext/backend/Classes/Routing/PageUriBuilder.php @@ -0,0 +1,118 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\Routing; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Psr\Http\Message\UriInterface; +use TYPO3\CMS\Core\Exception\SiteNotFoundException; +use TYPO3\CMS\Core\Http\Uri; +use TYPO3\CMS\Core\SingletonInterface; +use TYPO3\CMS\Core\Site\SiteFinder; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Responsible for generates URLs to pages which are NOT bound to any permissions or frontend restrictions. + * + * If a page is built with a site in the root line, the base of the site (+ language) is used + * and the &L parameter is then dropped explicitly. + * + * @internal as this might change until TYPO3 v9 LTS + * @todo: check handling of MP parameter. + */ +class PageUriBuilder implements SingletonInterface +{ + /** + * Generates an absolute URL + */ + const ABSOLUTE_URL = 'url'; + + /** + * Generates an absolute path + */ + const ABSOLUTE_PATH = 'path'; + + /** + * @var SiteFinder + */ + protected $siteFinder; + + /** + * PageUriBuilder constructor. + */ + public function __construct() + { + $this->siteFinder = GeneralUtility::makeInstance(SiteFinder::class); + } + + /** + * Main entrypoint for generating an Uri for a page. + * + * @param int $pageId + * @param array $queryParameters + * @param string $fragment + * @param array $options ['language' => 123, 'rootLine' => etc.] + * @param string $referenceType + * @return UriInterface + */ + public function buildUri(int $pageId, array $queryParameters = [], string $fragment = null, array $options = [], string $referenceType = self::ABSOLUTE_PATH): UriInterface + { + // Resolve site + $languageOption = isset($options['language']) ? (int)$options['language'] : null; + $languageQueryParameter = isset($queryParameters['L']) ? (int)$queryParameters['L'] : null; + $languageId = $languageOption ?? $languageQueryParameter ?? null; + + // alternative page ID - Used to set as alias as well + $alternativePageId = $options['alternativePageId'] ?? $pageId; + $siteLanguage = null; + try { + $site = $this->siteFinder->getSiteByPageId($pageId, $options['rootLine'] ?? null); + if ($site) { + // Resolve language (based on the options / query parameters, and remove it from GET variables, + // as the language is determined by the language path + unset($queryParameters['L']); + $siteLanguage = $site->getLanguageById($languageId ?? 0); + } + } catch (SiteNotFoundException | \InvalidArgumentException $e) { + } + + // If something is found, use /en/?id=123&additionalParams + // Only if a language is configured for the site, build a URL with a site prefix / base + if ($siteLanguage) { + unset($options['legacyUrlPrefix']); + $prefix = $siteLanguage->getBase() . '?id=' . $alternativePageId; + } else { + // If nothing is found, use index.php?id=123&additionalParams + $prefix = $options['legacyUrlPrefix'] ?? null; + if ($prefix === null) { + $prefix = $referenceType === self::ABSOLUTE_URL ? GeneralUtility::getIndpEnv('TYPO3_SITE_URL') : ''; + } + $prefix .= 'index.php?id=' . $alternativePageId; + if ($languageId !== null) { + $queryParameters['L'] = $languageId; + } + } + + // Add the query parameters as string + $queryString = http_build_query($queryParameters, '', '&', PHP_QUERY_RFC3986); + $uri = new Uri($prefix . ($queryString ? '&' . $queryString : '')); + if ($fragment) { + $uri = $uri->withFragment($fragment); + } + if ($referenceType === self::ABSOLUTE_PATH && !isset($options['legacyUrlPrefix'])) { + $uri = $uri->withScheme('')->withHost('')->withPort(null); + } + return $uri; + } +} diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php index f22e7e6a2f19a0f1d1fe0f6589cd5da0b048dc86..076cdd3bce014b0a7e80ace3f7a282a91c37b749 100644 --- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php +++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php @@ -15,6 +15,7 @@ namespace TYPO3\CMS\Backend\Utility; */ use Psr\Log\LoggerInterface; +use TYPO3\CMS\Backend\Routing\PageUriBuilder; use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Cache\CacheManager; @@ -27,6 +28,7 @@ use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; use TYPO3\CMS\Core\Database\RelationHandler; +use TYPO3\CMS\Core\Exception\SiteNotFoundException; use TYPO3\CMS\Core\Imaging\Icon; use TYPO3\CMS\Core\Imaging\IconFactory; use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection; @@ -37,6 +39,7 @@ use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\ProcessedFile; use TYPO3\CMS\Core\Resource\ResourceFactory; +use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Type\Bitmask\Permission; use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser; use TYPO3\CMS\Core\Utility\ArrayUtility; @@ -2569,7 +2572,20 @@ class BackendUtility $permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW); $pageInfo = self::readPageAccess($pageUid, $permissionClause); $additionalGetVars .= self::ADMCMD_previewCmds($pageInfo); - $previewUrl = self::createPreviewUrl($pageUid, $rootLine, $anchorSection, $additionalGetVars, $viewScript); + + // Build the URL with a site as prefix, if configured + $siteFinder = GeneralUtility::makeInstance(SiteFinder::class); + // Check if the page (= its rootline) has a site attached, otherwise just keep the URL as is + $rootLine = $rootLine ?? BackendUtility::BEgetRootLine($pageUid); + try { + $site = $siteFinder->getSiteByPageId((int)$pageUid, $rootLine); + // Create a multi-dimensional array out of the additional get vars + $additionalGetVars = GeneralUtility::explodeUrl2Array($additionalGetVars, true); + $uriBuilder = GeneralUtility::makeInstance(PageUriBuilder::class); + $previewUrl = (string)$uriBuilder->buildUri($pageUid, $additionalGetVars, $anchorSection, ['rootLine' => $rootLine], $uriBuilder::ABSOLUTE_URL); + } catch (SiteNotFoundException $e) { + $previewUrl = self::createPreviewUrl($pageUid, $rootLine, $anchorSection, $additionalGetVars, $viewScript); + } } foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) { diff --git a/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php b/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php index 5f1383e6dfdc840472936d2be7d81ea18d25af3d..002a36add17145449fa68666287d0212fad13db4 100644 --- a/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php +++ b/typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php @@ -53,6 +53,18 @@ return [ 'target' => Controller\FormInlineAjaxController::class . '::expandOrCollapseAction' ], + // Site configuration inline create route + 'site_configuration_inline_create' => [ + 'path' => '/siteconfiguration/inline/create', + 'target' => Controller\SiteInlineAjaxController::class . '::newInlineChildAction' + ], + + // Site configuration inline open existing "record" route + 'site_configuration_inline_details' => [ + 'path' => '/siteconfiguration/inline/details', + 'target' => Controller\SiteInlineAjaxController::class . '::openInlineChildAction' + ], + // Add a flex form section container 'record_flex_container_add' => [ 'path' => '/record/flex/containeradd', diff --git a/typo3/sysext/backend/Configuration/RequestMiddlewares.php b/typo3/sysext/backend/Configuration/RequestMiddlewares.php index d29b921f8b75b4a977e7ae989f23a561ee795719..bb8048e441a84f04957a18c142b9da794c4d9d7d 100644 --- a/typo3/sysext/backend/Configuration/RequestMiddlewares.php +++ b/typo3/sysext/backend/Configuration/RequestMiddlewares.php @@ -39,6 +39,12 @@ return [ 'typo3/cms-backend/backend-routing' ] ], + 'typo3/cms-backend/site-resolver' => [ + 'target' => \TYPO3\CMS\Backend\Middleware\SiteResolver::class, + 'after' => [ + 'typo3/cms-backend/backend-routing' + ] + ], 'typo3/cms-backend/legacy-document-template' => [ 'target' => \TYPO3\CMS\Backend\Middleware\LegacyBackendTemplateInitialization::class, 'after' => [ diff --git a/typo3/sysext/backend/Configuration/SiteConfigurationTCA/sys_site.php b/typo3/sysext/backend/Configuration/SiteConfigurationTCA/sys_site.php new file mode 100644 index 0000000000000000000000000000000000000000..834cfb3f6019697cb5ea992768883de6d4f827d7 --- /dev/null +++ b/typo3/sysext/backend/Configuration/SiteConfigurationTCA/sys_site.php @@ -0,0 +1,91 @@ +<?php + +return [ + 'ctrl' => [ + 'label' => 'identifier', + 'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site.ctrl.title', + 'typeicon_classes' => [ + 'default' => 'mimetypes-x-content-domain', + ], + ], + 'columns' => [ + 'identifier' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site.identifier', + 'config' => [ + 'type' => 'input', + 'size' => 35, + 'max' => 255, + // identifier is used as directory name - allow a-z,0-9,_,- as chars only. + // unique is additionally checked server side + 'eval' => 'required,lower,alphanum_x', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'rootPageId' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site.rootPageId', + 'config' => [ + 'type' => 'select', + 'readOnly' => true, + 'renderType' => 'selectSingle', + 'foreign_table' => 'pages', + 'foreign_table_where' => ' AND (is_siteroot=1 OR (pid=0 AND doktype IN (1,6,7))) AND l10n_parent = 0 ORDER BY pid, sorting', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'base' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site.base', + 'config' => [ + 'type' => 'input', + 'eval' => 'required', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'languages' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site.languages', + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'sys_site_language', + 'foreign_selector' => 'languageId', + 'foreign_unique' => 'languageId', + 'size' => 4, + 'minitems' => 1, + 'appearance' => [ + 'enabledControls' => [ + 'info' => false, + ], + ], + ], + ], + 'errorHandling' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site.errorHandling', + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'sys_site_errorhandling', + 'appearance' => [ + 'enabledControls' => [ + 'info' => false, + ], + ], + ], + ], + ], + 'types' => [ + '0' => [ + 'showitem' => 'identifier, rootPageId, base, + --div--;LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site.tab.languages, languages, + --div--;LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site.tab.errorHandling, errorHandling', + ], + ], +]; diff --git a/typo3/sysext/backend/Configuration/SiteConfigurationTCA/sys_site_errorhandling.php b/typo3/sysext/backend/Configuration/SiteConfigurationTCA/sys_site_errorhandling.php new file mode 100644 index 0000000000000000000000000000000000000000..c90677310a2e908093b9eb9a69f24d426669d7c0 --- /dev/null +++ b/typo3/sysext/backend/Configuration/SiteConfigurationTCA/sys_site_errorhandling.php @@ -0,0 +1,157 @@ +<?php + +return [ + 'ctrl' => [ + 'label' => 'errorCode', + 'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.ctrl.title', + 'type' => 'errorHandler', + 'typeicon_column' => 'errorHandler', + 'typeicon_classes' => [ + 'default' => 'default-not-found', + 'Fluid' => 'mimetypes-text-html', + 'ContentFromPid' => 'apps-pagetree-page-content-from-page', + 'ClassDispatcher' => 'mimetypes-text-php', + ], + ], + 'columns' => [ + 'errorCode' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorCode', + 'config' => [ + 'type' => 'input', + 'eval' => 'required, trim, int', + 'range' => [ + 'lower' => 0, + 'upper' => 599, + ], + 'default' => 404, + 'valuePicker' => [ + 'mode' => '', + 'items' => [ + ['LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorCode.404', '404'], + ['LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorCode.403', '403'], + ['LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorCode.401', '401'], + ['LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorCode.500', '500'], + ['LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorCode.503', '503'], + ['LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorCode.0', '0'], + ], + ], + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'errorHandler' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorHandler', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + [' - select an handler type - ', ''], + ['Fluid Template', 'Fluid'], + ['Show Content from Page', 'Page'], + ['PHP Class (must implement the PageErrorHandlerInterface)', 'PHP'], + ], + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'errorFluidTemplate' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorFluidTemplate', + 'config' => [ + 'type' => 'input', + 'eval' => 'required', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'errorFluidTemplatesRootPath' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorFluidTemplatesRootPath', + 'config' => [ + 'type' => 'input', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'errorFluidLayoutsRootPath' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorFluidLayoutsRootPath', + 'config' => [ + 'type' => 'input', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'errorFluidPartialsRootPath' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorFluidPartialsRootPath', + 'config' => [ + 'type' => 'input', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'errorContentSource' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorContentSource', + 'config' => [ + 'type' => 'input', + 'renderType' => 'inputLink', + 'eval' => 'required', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + 'fieldControl' => [ + 'linkPopup' => [ + 'options' => [ + 'blindLinkOptions' => 'file,mail,spec,folder', + ] + ] + ], + ], + ], + 'errorPhpClassFQCN' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.errorPhpClassFQCN', + 'config' => [ + 'type' => 'input', + 'eval' => 'required', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + ], + 'types' => [ + '1' => [ + 'showitem' => 'errorCode, errorHandler', + ], + 'Fluid' => [ + 'showitem' => 'errorCode, errorHandler, errorFluidTemplate, + --div--;LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_errorhandling.tab.rootpaths, + errorFluidTemplatesRootPath, errorFluidLayoutsRootPath, errorFluidPartialsRootPath', + ], + 'Page' => [ + 'showitem' => 'errorCode, errorHandler, errorContentSource', + ], + 'PHP' => [ + 'showitem' => 'errorCode, errorHandler, errorPhpClassFQCN', + ], + ], +]; diff --git a/typo3/sysext/backend/Configuration/SiteConfigurationTCA/sys_site_language.php b/typo3/sysext/backend/Configuration/SiteConfigurationTCA/sys_site_language.php new file mode 100644 index 0000000000000000000000000000000000000000..389ac30b3875ff3d1bf6d14ebc37b5c188cad5d9 --- /dev/null +++ b/typo3/sysext/backend/Configuration/SiteConfigurationTCA/sys_site_language.php @@ -0,0 +1,456 @@ +<?php + +return [ + 'ctrl' => [ + 'label' => 'languageId', + 'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.ctrl.title', + 'typeicon_classes' => [ + 'default' => 'mimetypes-x-content-domain', + ], + ], + 'columns' => [ + 'languageId' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.language', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + ['Default Language', 0], + ], + 'foreign_table' => 'sys_language', + 'size' => 1, + 'min' => 1, + 'max' => 1, + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'title' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.title', + 'config' => [ + 'type' => 'input', + 'size' => 10, + 'eval' => 'required', + 'placeholder' => 'English', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'navigationTitle' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.navigationTitle', + 'config' => [ + 'type' => 'input', + 'size' => 10, + 'placeholder' => 'English', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'base' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.base', + 'config' => [ + 'type' => 'input', + 'eval' => 'required', + 'default' => '/', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'locale' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.locale', + 'config' => [ + 'type' => 'input', + 'eval' => 'required', + 'placeholder' => 'en_US.UTF-8', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'iso-639-1' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.iso-639-1', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + // Fed by data provider + 'items' => [], + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'hreflang' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.hreflang', + 'config' => [ + 'type' => 'input', + 'placeholder' => 'en-US', + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'direction' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.direction', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + ['None', '', ''], + ['Left to Right', 'ltr', ''], + ['Right to Left', 'rtl', ''], + ], + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'typo3Language' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.typo3Language', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + // Fed by data provider + 'items' => [], + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'flag' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.flag', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + ['global', 'global', 'flags-multiple'], + ['ad', 'ad', 'flags-ad'], + ['ae', 'ae', 'flags-ae'], + ['af', 'af', 'flags-af'], + ['ag', 'ag', 'flags-ag'], + ['ai', 'ai', 'flags-ai'], + ['al', 'al', 'flags-al'], + ['am', 'am', 'flags-am'], + ['an', 'an', 'flags-an'], + ['ao', 'ao', 'flags-ao'], + ['ar', 'ar', 'flags-ar'], + ['as', 'as', 'flags-as'], + ['at', 'at', 'flags-at'], + ['au', 'au', 'flags-au'], + ['aw', 'aw', 'flags-aw'], + ['ax', 'ax', 'flags-ax'], + ['az', 'az', 'flags-az'], + ['ba', 'ba', 'flags-ba'], + ['bb', 'bb', 'flags-bb'], + ['bd', 'bd', 'flags-bd'], + ['be', 'be', 'flags-be'], + ['bf', 'bf', 'flags-bf'], + ['bg', 'bg', 'flags-bg'], + ['bh', 'bh', 'flags-bh'], + ['bi', 'bi', 'flags-bi'], + ['bj', 'bj', 'flags-bj'], + ['bm', 'bm', 'flags-bm'], + ['bn', 'bn', 'flags-bn'], + ['bo', 'bo', 'flags-bo'], + ['br', 'br', 'flags-br'], + ['bs', 'bs', 'flags-bs'], + ['bt', 'bt', 'flags-bt'], + ['bv', 'bv', 'flags-bv'], + ['bw', 'bw', 'flags-bw'], + ['by', 'by', 'flags-by'], + ['bz', 'bz', 'flags-bz'], + ['ca', 'ca', 'flags-ca'], + ['catalonia', 'catalonia', 'flags-catalonia'], + ['cc', 'cc', 'flags-cc'], + ['cd', 'cd', 'flags-cd'], + ['cf', 'cf', 'flags-cf'], + ['cg', 'cg', 'flags-cg'], + ['ch', 'ch', 'flags-ch'], + ['ci', 'ci', 'flags-ci'], + ['ck', 'ck', 'flags-ck'], + ['cl', 'cl', 'flags-cl'], + ['cm', 'cm', 'flags-cm'], + ['cn', 'cn', 'flags-cn'], + ['co', 'co', 'flags-co'], + ['cr', 'cr', 'flags-cr'], + ['cs', 'cs', 'flags-cs'], + ['cu', 'cu', 'flags-cu'], + ['cv', 'cv', 'flags-cv'], + ['cx', 'cx', 'flags-cx'], + ['cy', 'cy', 'flags-cy'], + ['cz', 'cz', 'flags-cz'], + ['de', 'de', 'flags-de'], + ['dj', 'dj', 'flags-dj'], + ['dk', 'dk', 'flags-dk'], + ['dm', 'dm', 'flags-dm'], + ['do', 'do', 'flags-do'], + ['dz', 'dz', 'flags-dz'], + ['ec', 'ec', 'flags-ec'], + ['ee', 'ee', 'flags-ee'], + ['eg', 'eg', 'flags-eg'], + ['eh', 'eh', 'flags-eh'], + ['en-us-gb', 'en-us-gb', 'flags-en-us-gb'], + ['england', 'england', 'flags-gb-eng'], + ['er', 'er', 'flags-er'], + ['es', 'es', 'flags-es'], + ['et', 'et', 'flags-et'], + ['eu', 'eu', 'flags-eu'], + ['fm', 'fm', 'flags-fm'], + ['fi', 'fi', 'flags-fi'], + ['fj', 'fj', 'flags-fj'], + ['fk', 'fk', 'flags-fk'], + ['fm', 'fm', 'flags-fm'], + ['fo', 'fo', 'flags-fo'], + ['fr', 'fr', 'flags-fr'], + ['ga', 'ga', 'flags-ga'], + ['gb', 'gb', 'flags-gb'], + ['gd', 'gd', 'flags-gd'], + ['ge', 'ge', 'flags-ge'], + ['gf', 'gf', 'flags-gf'], + ['gh', 'gh', 'flags-gh'], + ['gi', 'gi', 'flags-gi'], + ['gl', 'gl', 'flags-gl'], + ['gm', 'gm', 'flags-gm'], + ['gn', 'gn', 'flags-gn'], + ['gp', 'gp', 'flags-gp'], + ['gq', 'gq', 'flags-gq'], + ['gr', 'gr', 'flags-gr'], + ['gs', 'gs', 'flags-gs'], + ['gt', 'gt', 'flags-gt'], + ['gu', 'gu', 'flags-gu'], + ['gw', 'gw', 'flags-gw'], + ['gy', 'gy', 'flags-gy'], + ['hk', 'hk', 'flags-hk'], + ['hm', 'hm', 'flags-hm'], + ['hn', 'hn', 'flags-hn'], + ['hr', 'hr', 'flags-hr'], + ['ht', 'ht', 'flags-ht'], + ['hu', 'hu', 'flags-hu'], + ['id', 'id', 'flags-id'], + ['ie', 'ie', 'flags-ie'], + ['il', 'il', 'flags-il'], + ['in', 'in', 'flags-in'], + ['io', 'io', 'flags-io'], + ['iq', 'iq', 'flags-iq'], + ['ir', 'ir', 'flags-ir'], + ['is', 'is', 'flags-is'], + ['it', 'it', 'flags-it'], + ['jm', 'jm', 'flags-jm'], + ['jo', 'jo', 'flags-jo'], + ['jp', 'jp', 'flags-jp'], + ['ke', 'ke', 'flags-ke'], + ['kg', 'kg', 'flags-kg'], + ['kh', 'kh', 'flags-kh'], + ['ki', 'ki', 'flags-ki'], + ['km', 'km', 'flags-km'], + ['kn', 'kn', 'flags-kn'], + ['kp', 'kp', 'flags-kp'], + ['kr', 'kr', 'flags-kr'], + ['kw', 'kw', 'flags-kw'], + ['ky', 'ky', 'flags-ky'], + ['kz', 'kz', 'flags-kz'], + ['la', 'la', 'flags-la'], + ['lb', 'lb', 'flags-lb'], + ['lc', 'lc', 'flags-lc'], + ['li', 'li', 'flags-li'], + ['lk', 'lk', 'flags-lk'], + ['lr', 'lr', 'flags-lr'], + ['ls', 'ls', 'flags-ls'], + ['lt', 'lt', 'flags-lt'], + ['lu', 'lu', 'flags-lu'], + ['lv', 'lv', 'flags-lv'], + ['ly', 'ly', 'flags-ly'], + ['ma', 'ma', 'flags-ma'], + ['mc', 'mc', 'flags-mc'], + ['md', 'md', 'flags-md'], + ['me', 'me', 'flags-me'], + ['mg', 'mg', 'flags-mg'], + ['mh', 'mh', 'flags-mh'], + ['mk', 'mk', 'flags-mk'], + ['ml', 'ml', 'flags-ml'], + ['mm', 'mm', 'flags-mm'], + ['mn', 'mn', 'flags-mn'], + ['mo', 'mo', 'flags-mo'], + ['mp', 'mp', 'flags-mp'], + ['mq', 'mq', 'flags-mq'], + ['mr', 'mr', 'flags-mr'], + ['ms', 'ms', 'flags-ms'], + ['mt', 'mt', 'flags-mt'], + ['mu', 'mu', 'flags-mu'], + ['mv', 'mv', 'flags-mv'], + ['mw', 'mw', 'flags-mw'], + ['mx', 'mx', 'flags-mx'], + ['my', 'my', 'flags-my'], + ['mz', 'mz', 'flags-mz'], + ['na', 'na', 'flags-na'], + ['nc', 'nc', 'flags-nc'], + ['ne', 'ne', 'flags-ne'], + ['nf', 'nf', 'flags-nf'], + ['ng', 'ng', 'flags-ng'], + ['ni', 'ni', 'flags-ni'], + ['nl', 'nl', 'flags-nl'], + ['no', 'no', 'flags-no'], + ['np', 'np', 'flags-np'], + ['nr', 'nr', 'flags-nr'], + ['nu', 'nu', 'flags-nu'], + ['nz', 'nz', 'flags-nz'], + ['om', 'om', 'flags-om'], + ['pa', 'pa', 'flags-pa'], + ['pe', 'pe', 'flags-pe'], + ['pf', 'pf', 'flags-pf'], + ['pg', 'pg', 'flags-pg'], + ['ph', 'ph', 'flags-ph'], + ['pk', 'pk', 'flags-pk'], + ['pl', 'pl', 'flags-pl'], + ['pm', 'pm', 'flags-pm'], + ['pn', 'pn', 'flags-pn'], + ['pr', 'pr', 'flags-pr'], + ['ps', 'ps', 'flags-ps'], + ['pt', 'pt', 'flags-pt'], + ['pw', 'pw', 'flags-pw'], + ['py', 'py', 'flags-py'], + ['qa', 'qa', 'flags-qa'], + ['qc', 'qc', 'flags-qc'], + ['re', 're', 'flags-re'], + ['ro', 'ro', 'flags-ro'], + ['rs', 'rs', 'flags-rs'], + ['ru', 'ru', 'flags-ru'], + ['rw', 'rw', 'flags-rw'], + ['sa', 'sa', 'flags-sa'], + ['sb', 'sb', 'flags-sb'], + ['sc', 'sc', 'flags-sc'], + ['gb-sct', 'gb-sct', 'flags-gb-sct'], + ['sd', 'sd', 'flags-sd'], + ['se', 'se', 'flags-se'], + ['sg', 'sg', 'flags-sg'], + ['sh', 'sh', 'flags-sh'], + ['si', 'si', 'flags-si'], + ['sj', 'sj', 'flags-sj'], + ['sk', 'sk', 'flags-sk'], + ['sl', 'sl', 'flags-sl'], + ['sm', 'sm', 'flags-sm'], + ['sn', 'sn', 'flags-sn'], + ['so', 'so', 'flags-so'], + ['sr', 'sr', 'flags-sr'], + ['st', 'st', 'flags-st'], + ['sv', 'sv', 'flags-sv'], + ['sy', 'sy', 'flags-sy'], + ['sz', 'sz', 'flags-sz'], + ['tc', 'tc', 'flags-tc'], + ['td', 'td', 'flags-td'], + ['tf', 'tf', 'flags-tf'], + ['tg', 'tg', 'flags-tg'], + ['th', 'th', 'flags-th'], + ['tj', 'tj', 'flags-tj'], + ['tk', 'tk', 'flags-tk'], + ['tl', 'tl', 'flags-tl'], + ['tm', 'tm', 'flags-tm'], + ['tn', 'tn', 'flags-tn'], + ['to', 'to', 'flags-to'], + ['tr', 'tr', 'flags-tr'], + ['tt', 'tt', 'flags-tt'], + ['tv', 'tv', 'flags-tv'], + ['tw', 'tw', 'flags-tw'], + ['tz', 'tz', 'flags-tz'], + ['ua', 'ua', 'flags-ua'], + ['ug', 'ug', 'flags-ug'], + ['um', 'um', 'flags-um'], + ['us', 'us', 'flags-us'], + ['uy', 'uy', 'flags-uy'], + ['uz', 'uz', 'flags-uz'], + ['va', 'va', 'flags-va'], + ['vc', 'vc', 'flags-vc'], + ['ve', 've', 'flags-ve'], + ['vg', 'vg', 'flags-vg'], + ['vi', 'vi', 'flags-vi'], + ['vn', 'vn', 'flags-vn'], + ['vu', 'vu', 'flags-vu'], + ['gb-wls', 'gb-wls', 'flags-gb-wls'], + ['wf', 'wf', 'flags-wf'], + ['ws', 'ws', 'flags-ws'], + ['ye', 'ye', 'flags-ye'], + ['yt', 'yt', 'flags-yt'], + ['za', 'za', 'flags-za'], + ['zm', 'zm', 'flags-zm'], + ['zw', 'zw', 'flags-zw'], + ], + 'size' => 1, + 'minitems' => 0, + 'maxitems' => 1, + 'fieldWizard' => [ + 'selectIcons' => [ + 'disabled' => false, + ], + ], + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'fallbackType' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.fallbackType', + 'displayCond' => 'FIELD:languageId:>:0', + 'onChange' => 'reload', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + 'items' => [ + ['No fallback (strict)', 'strict'], + ['Fallback to other language', 'fallback'], + ], + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + 'fallbacks' => [ + 'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:sys_site_language.fallbacks', + 'displayCond' => 'FIELD:fallbackType:=:fallback', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectMultipleSideBySide', + 'items' => [ + ['Default Language', 0], + ], + 'foreign_table' => 'sys_language', + 'size' => 5, + 'min' => 0, + 'fieldInformation' => [ + 'SiteConfigurationModuleFieldInformation' => [ + 'renderType' => 'SiteConfigurationModuleFieldInformation', + ], + ], + ], + ], + ], + 'types' => [ + '1' => [ + 'showitem' => 'languageId, title, navigationTitle, base, locale, iso-639-1, hreflang, direction, typo3Language, flag, fallbackType, fallbacks', + ], + ], +]; diff --git a/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration.xlf b/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration.xlf new file mode 100644 index 0000000000000000000000000000000000000000..cd16846f2189be9e58ed4b0491bcd2c4a8f22946 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration.xlf @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff"> + <file t3:id="1522780424" source-language="en" datatype="plaintext" original="messages" date="2018-02-27T22:22:32Z" product-name="backend"> + <header/> + <body> + <trans-unit id="overview.title"> + <source>Site Configuration</source> + </trans-unit> + <trans-unit id="overview.site"> + <source>Site</source> + </trans-unit> + <trans-unit id="overview.configuration"> + <source>Configuration Folder</source> + </trans-unit> + <trans-unit id="overview.noSiteConfiguration"> + <source>This site does not have a configuration, yet.</source> + </trans-unit> + <trans-unit id="overview.addSiteConfiguration"> + <source>Add new site configuration for this site</source> + </trans-unit> + <trans-unit id="overview.baseUrl"> + <source>Base URLs</source> + </trans-unit> + <trans-unit id="validation.identifierRenamed.title"> + <source>Renamed identifier</source> + </trans-unit> + <trans-unit id="validation.identifierRenamed.message"> + <source>Given site identifier "%1s" already exists. It has been renamed to "%2s". Maybe you want to edit the site again and give it a better name.</source> + </trans-unit> + <trans-unit id="validation.identifierExists.title"> + <source>Identifier not changed</source> + </trans-unit> + <trans-unit id="validation.identifierExists.message"> + <source>Given site identifier "%1s" already exists and points to a different site. The existing identifier "%2s" for this site is kept.';</source> + </trans-unit> + <trans-unit id="validation.required.title"> + <source>Site configuration not saved</source> + </trans-unit> + <trans-unit id="validation.required.message"> + <source>Field "%1s" is a required field, but no value has been provided.</source> + </trans-unit> + <trans-unit id="validation.duplicateErrorCode.title"> + <source>Duplicate error code removed</source> + </trans-unit> + <trans-unit id="validation.duplicateErrorCode.message"> + <source>Two error handler configurations for error code "%1s" found. This would be an invalid configuration, the dangling one has been removed.</source> + </trans-unit> + <trans-unit id="validation.duplicateLanguageId.title"> + <source>Duplicate language configuration removed</source> + </trans-unit> + <trans-unit id="validation.duplicateLanguageId.message"> + <source>Two language configurations for language "%1s" found. This would be an invalid configuration, the dangling one has been removed.</source> + </trans-unit> + <trans-unit id="validation.duplicateLanguageId.message"> + <source>Two language configurations for language "%1s" found. This would be an invalid configuration, the dangling one has been removed.</source> + </trans-unit> + </body> + </file> +</xliff> diff --git a/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf b/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf new file mode 100644 index 0000000000000000000000000000000000000000..d8edf48ac3e7e46020a03178c032d66596f83d01 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff"> + <file t3:id="1522771784" source-language="en" datatype="plaintext" original="messages" date="2018-02-27T22:22:32Z" product-name="backend"> + <header/> + <body> + <trans-unit id="mlang_labels_tablabel"> + <source>Site configuration</source> + </trans-unit> + <trans-unit id="mlang_labels_tabdescr"> + <source>This module allows you to configure your entrypoints (called sites).</source> + </trans-unit> + <trans-unit id="mlang_tabs_tab"> + <source>Configuration</source> + </trans-unit> + </body> + </file> +</xliff> diff --git a/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf b/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf new file mode 100644 index 0000000000000000000000000000000000000000..f3a5fd2a068dc6524e183e2572eda76c0d439305 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf @@ -0,0 +1,121 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff"> + <file t3:id="1522785604" source-language="en" datatype="plaintext" original="messages" date="2018-02-27T22:22:32Z" product-name="backend"> + <header/> + <body> + <trans-unit id="sys_site.ctrl.title"> + <source>Site Configuration</source> + </trans-unit> + <trans-unit id="sys_site.identifier"> + <source>Site Identifier</source> + </trans-unit> + <trans-unit id="sys_site.rootPageId"> + <source>Root Page ID (You must create a page with a site root flag)</source> + </trans-unit> + <trans-unit id="sys_site.base"> + <source>Entry point (can be https://www.mydomain/ or just /, if it is just / you can not rely on TYPO3 creating full URLs)</source> + </trans-unit> + <trans-unit id="sys_site.languages"> + <source>Available Languages for this site</source> + </trans-unit> + <trans-unit id="sys_site.errorHandling"> + <source>Error Handling</source> + </trans-unit> + <trans-unit id="sys_site.tab.languages"> + <source>Languages</source> + </trans-unit> + <trans-unit id="sys_site.tab.errorHandling"> + <source>Error Handling</source> + </trans-unit> + + <trans-unit id="sys_site_language.ctrl.title"> + <source>Language Configuration for a Site</source> + </trans-unit> + <trans-unit id="sys_site_language.language"> + <source>Language</source> + </trans-unit> + <trans-unit id="sys_site_language.title"> + <source>Language title (e.g. "English")</source> + </trans-unit> + <trans-unit id="sys_site_language.navigationTitle"> + <source>Navigation title (e.g. "English", "Deutsch", "Français")</source> + </trans-unit> + <trans-unit id="sys_site_language.base"> + <source>Entry point (either https://www.mydomain.fr/ or /fr/)</source> + </trans-unit> + <trans-unit id="sys_site_language.locale"> + <source>Language locale</source> + </trans-unit> + <trans-unit id="sys_site_language.iso-639-1"> + <source>Two letter ISO code</source> + </trans-unit> + <trans-unit id="sys_site_language.hreflang"> + <source>Language tag defined by RFC 1766 / 3066 for "lang" and "hreflang" attributes</source> + </trans-unit> + <trans-unit id="sys_site_language.direction"> + <source>Language direction for "dir" attribute</source> + </trans-unit> + <trans-unit id="sys_site_language.typo3Language"> + <source>Language key for XLF files</source> + </trans-unit> + <trans-unit id="sys_site_language.flag"> + <source>Select flag icon</source> + </trans-unit> + <trans-unit id="sys_site_language.fallbackType"> + <source>Fallback type</source> + </trans-unit> + <trans-unit id="sys_site_language.fallbacks"> + <source>Fallback to other language(s) - order is important!</source> + </trans-unit> + + <trans-unit id="sys_site_errorhandling.ctrl.title"> + <source>Error Handling</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.tab.rootpaths"> + <source>Root Paths (optional)</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorCode"> + <source>Error Status Code</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorCode.404"> + <source>404 (Page not found)</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorCode.403"> + <source>403 (Forbidden)</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorCode.401"> + <source>401 (Unauthorized)</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorCode.500"> + <source>500 (Internal Server Error)</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorCode.503"> + <source>503 (Service Unavailable)</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorCode.0"> + <source>any error not defined otherwise</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorHandler"> + <source>How to handle errors</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorFluidTemplate"> + <source>Fluid Template File</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorFluidTemplatesRootPath"> + <source>Fluid Templates Root Path</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorFluidLayoutsRootPath"> + <source>Fluid Layouts Root Path</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorFluidPartialsRootPath"> + <source>Fluid Partials Root Path</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorContentSource"> + <source>Show Content From Page</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorPhpClassFQCN"> + <source>ErrorHandler Class Target (FQCN)</source> + </trans-unit> + </body> + </file> +</xliff> diff --git a/typo3/sysext/backend/Resources/Private/Language/siteconfiguration_fieldinformation.xlf b/typo3/sysext/backend/Resources/Private/Language/siteconfiguration_fieldinformation.xlf new file mode 100644 index 0000000000000000000000000000000000000000..5051ff417db107171e2ae1a16caa48dc55f1fded --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Language/siteconfiguration_fieldinformation.xlf @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff"> + <file t3:id="1522923345" source-language="en" datatype="plaintext" original="messages" date="2015-01-02T11:16:11Z" product-name="backend"> + <header/> + <body> + <trans-unit id="sys_site.identifier"> + <source>This name will be used to create the configuration directory. Mind the recommendations for directory names (only a-z,0-9,_,-) and make it unique.</source> + </trans-unit> + <trans-unit id="sys_site.base"> + <source>Main URL to call the frontend in default language.</source> + </trans-unit> + <trans-unit id="sys_site_language.base"> + <source>use / to use keep the main URL as configured at field Entry Point. Add language specific suffixes to use those, or configure complete URLs for independent domains.</source> + </trans-unit> + <trans-unit id="sys_site_language.locale"> + <source>should be something like de_DE or en_EN.UTF8</source> + </trans-unit> + <trans-unit id="sys_site_language.typo3Language"> + <source>Select the language to be used from translation files. Keep default if no translation files are available.</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorCode"> + <source>make sure to have at least 0 (not defined otherwise) configured in order to serve helpful error messages to your visitors.</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorFluidTemplate"> + <source>absolute or relative path (from site root) to fluid template file</source> + </trans-unit> + <trans-unit id="sys_site_errorhandling.errorPhpClassFQCN"> + <source>PHP class full qualified name that serves the error page.</source> + </trans-unit> + </body> + </file> +</xliff> diff --git a/typo3/sysext/backend/Resources/Private/Templates/SiteConfiguration/Edit.html b/typo3/sysext/backend/Resources/Private/Templates/SiteConfiguration/Edit.html new file mode 100644 index 0000000000000000000000000000000000000000..eca5d6776513e3bd5eba4539d7fe93a36eec0fb9 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Templates/SiteConfiguration/Edit.html @@ -0,0 +1,18 @@ +<f:be.pageRenderer includeRequireJsModules="{0: 'TYPO3/CMS/Backend/SiteInlineActions'}" /> +<form + action="{f:be.uri(route:'site_configuration', parameters:{action: 'save'})}" + method="post" + enctype="multipart/form-data" + name="editform" + id="siteConfigurationController" + onsubmit="TBE_EDITOR.checkAndDoSubmit(1); return false;" +> + {formEngineHtml -> f:format.raw()} + + <input type="hidden" name="returnUrl" value="{returnUrl -> f:format.raw()}" /> + <input type="hidden" name="closeDoc" value="0" /> + <input type="hidden" name="doSave" value="0" /> + <input type="hidden" name="rootPageId" value="{rootPageId}" /> + + {formEngineFooter -> f:format.raw()} +</form> \ No newline at end of file diff --git a/typo3/sysext/backend/Resources/Private/Templates/SiteConfiguration/Overview.html b/typo3/sysext/backend/Resources/Private/Templates/SiteConfiguration/Overview.html new file mode 100644 index 0000000000000000000000000000000000000000..ebf3726439a2e28296613543523cad458c4a429a --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Templates/SiteConfiguration/Overview.html @@ -0,0 +1,79 @@ +<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers" data-namespace-typo3-fluid="true"> +<h1><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:overview.title" /></h1> + +<div class="table-fit"> + <table class="table table-striped table-hover table-condensed"> + <thead> + <tr> + <th><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:overview.site" /></th> + <th><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:overview.configuration" /></th> + <th><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:overview.baseUrl" /></th> + <th> </th> + </tr> + </thead> + <tbody> + <f:for each="{pages}" as="page"> + <tr> + <td nowrap valign="top"> + <f:for each="{page.rootline}" as="rootLinePage" iteration="i"> + <f:if condition="{rootLinePage.uid} == {page.uid}"> + <f:then> + <a href="#" class="t3js-contextmenutrigger" data-table="pages" data-uid="{rootLinePage.uid}"> + <core:iconForRecord table="pages" row="{rootLinePage}" /> + </a> {rootLinePage.title} [ID: {page.uid}] + </f:then> + <f:else> + <core:iconForRecord table="pages" row="{rootLinePage}" /> + {rootLinePage.title}<br> + </f:else> + </f:if> + </f:for> + </td> + <td> + <f:if condition="{page.siteIdentifier}"> + <f:then> + <code>{page.siteIdentifier}</code> + </f:then> + <f:else> + <div> + <f:be.link route="site_configuration" parameters="{action: 'edit', pageUid: page.uid}" title="Create configuration" class="btn btn-primary"> + <f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:overview.addSiteConfiguration" /> + </f:be.link> + </div> + </f:else> + </f:if> + </td> + <td> + <f:if condition="{page.siteConfiguration}"> + <table class="table table-striped table-no-borders"> + <tr> + <th>Language Name</th> + <th>Full URL Prefix</th> + </tr> + <f:for each="{page.siteConfiguration.languages}" as="siteLanguage"> + <tr> + <td><core:icon identifier="flags-{siteLanguage.flagIdentifier}" /> {siteLanguage.title}</td> + <td><a href="{siteLanguage.base}" target="_blank">{siteLanguage.base}</a></td> + </tr> + </f:for> + </table> + </f:if> + </td> + <td> + <div class="btn-group"> + <f:if condition="{page.siteIdentifier}"> + <f:be.link route="site_configuration" parameters="{action: 'edit', site: page.siteIdentifier}" title="Edit" class="btn btn-default"> + <core:icon identifier="actions-open" /> + </f:be.link> + <f:be.link route="site_configuration" parameters="{action: 'delete', site:page.siteIdentifier}" title="Delete configuration" class="btn btn-default"> + <core:icon identifier="actions-delete" /> + </f:be.link> + </f:if> + </div> + </td> + </tr> + </f:for> + </tbody> + </table> + </div> +</html> diff --git a/typo3/sysext/backend/Resources/Public/Icons/module-contentelements.svg b/typo3/sysext/backend/Resources/Public/Icons/module-contentelements.svg new file mode 100644 index 0000000000000000000000000000000000000000..342c843d5d79fcaad5bf583b3d28032272a96a66 --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/Icons/module-contentelements.svg @@ -0,0 +1 @@ +<!-- Copyright © 2015 MODULUS Sp. z o. o. / FUTURAMO™ --><svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="64px" height="64px" viewBox="0 0 64 64"><rect x="0" y="0" width="64" height="64" rx="0" ry="0" fill="#696DBB"></rect><path transform="translate(16, 16)" fill="#FFFFFF" d="M16,16c0-1.104,0.896-2,2-2c1.104,0,2,0.896,2,2c0,1.104-0.896,2-2,2C16.896,18,16,17.104,16,16z M16,20 l-4.286,2l-5.143-6L4,20.375V26h18L16,20z M6,4h26v20h-6v6H0V10h6V4z M24,12H2v16h22V12z M8,10h18v12h4V6H8V10z"></path></svg> \ No newline at end of file diff --git a/typo3/sysext/backend/Resources/Public/Icons/module-sites.svg b/typo3/sysext/backend/Resources/Public/Icons/module-sites.svg new file mode 100644 index 0000000000000000000000000000000000000000..fe87ff9fcfdee5efa69f3015452cc966298bf8fe --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/Icons/module-sites.svg @@ -0,0 +1 @@ +<!-- Copyright © 2015 MODULUS Sp. z o. o. / FUTURAMO™ --><svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="64px" height="64px" viewBox="0 0 64 64"><rect x="0" y="0" width="64" height="64" rx="0" ry="0" fill="#69BBB5"></rect><path transform="translate(16, 16)" fill="#FFFFFF" d="M31.634,6.064L30.109,5.81c-0.066-0.284-0.155-0.559-0.265-0.823c-0.11-0.264-0.241-0.517-0.391-0.757 l0.9-1.259v0l0.215-0.301l-0.157-0.157l-0.105-0.105l0,0l-0.713-0.714l0,0l-0.262-0.262L29.15,1.561h0l-1.38,0.986 c-0.481-0.3-1.012-0.524-1.58-0.656l-0.254-1.525l0,0L25.875,0h-0.222h-0.148h-1.009h-0.148h-0.222l-0.036,0.219l0,0L23.81,1.891 c-0.568,0.132-1.1,0.356-1.581,0.656l-1.259-0.9l0,0l-0.302-0.216l-0.157,0.157l0,0l-0.679,0.68l-0.139,0.139l0,0l-0.105,0.105 l-0.157,0.157L19.56,2.85l0,0l0.985,1.38c-0.299,0.481-0.523,1.012-0.655,1.58l-1.671,0.278l0,0L18,6.125l0,0.222l0,0l0.001,1.305v0 l0,0.222l0.365,0.061l0,0l0.819,0.136l0.705,0.117c0.066,0.284,0.155,0.559,0.265,0.823c0.11,0.264,0.241,0.517,0.391,0.757 l-0.986,1.38l0,0l-0.129,0.181l0.157,0.157c0,0,0,0,0,0l0.924,0.922h0l0.157,0.157l0.301-0.215h0l1.259-0.899 c0.481,0.3,1.012,0.524,1.58,0.656l0.278,1.671l0,0L24.125,14h0.222h0.148h1.009h0.148h0.222l0.061-0.365l0,0l0.254-1.525 c0.569-0.132,1.1-0.356,1.581-0.656l0.998,0.713l0.261,0.187h0l0.302,0.216l0.262-0.262l0.713-0.713l0.262-0.262l-0.215-0.302l0,0 l-0.9-1.26c0.15-0.24,0.281-0.493,0.391-0.757c0.11-0.264,0.199-0.539,0.265-0.823l1.525-0.254l0,0L32,7.875V7.653V7.504V6.496 V6.347V6.125L31.634,6.064L31.634,6.064z M25,10c-1.657,0-3-1.343-3-3s1.343-3,3-3s3,1.343,3,3S26.657,10,25,10z M30,16h2v8H18v6h8 v2H6v-2h8v-6H0V0h16v2H2v20h28V16z"></path></svg> \ No newline at end of file diff --git a/typo3/sysext/backend/Resources/Public/Icons/module-templates.svg b/typo3/sysext/backend/Resources/Public/Icons/module-templates.svg new file mode 100644 index 0000000000000000000000000000000000000000..a8156e7e687ffdae79e57a82106af2f6020d4a16 --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/Icons/module-templates.svg @@ -0,0 +1 @@ +<!-- Copyright © 2015 MODULUS Sp. z o. o. / FUTURAMO™ --><svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="64px" height="64px" viewBox="0 0 64 64"><rect x="0" y="0" width="64" height="64" rx="0" ry="0" fill="#B069BB"></rect><path transform="translate(16, 16)" fill="#FFFFFF" d="M28,4v4H4V4H28 M30,2H2v8h28V2L30,2z M28,14v14h-4V14H28 M30,12h-8v18h8V12L30,12z M18,14v14H4V14H18 M20,12 H2v18h18V12L20,12z"></path></svg> \ No newline at end of file diff --git a/typo3/sysext/backend/Resources/Public/Icons/module-urls.svg b/typo3/sysext/backend/Resources/Public/Icons/module-urls.svg new file mode 100644 index 0000000000000000000000000000000000000000..b0abf121da52cd07b093919a7bbcba0f5c5e7a56 --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/Icons/module-urls.svg @@ -0,0 +1 @@ +<!-- Copyright © 2015 MODULUS Sp. z o. o. / FUTURAMO™ --><svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="64px" height="64px" viewBox="0 0 64 64"><rect x="0" y="0" width="64" height="64" rx="0" ry="0" fill="#69bb7d"></rect><path transform="translate(16, 16)" fill="#FFFFFF" d="M20,26v4h10V20H20v4h-8V10h8v4h10V4H20v4H10v8H0l0,2h10v8H20z M22,6h6v6h-6V6z M22,22h6v6h-6V22z"></path></svg> \ No newline at end of file diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/SiteInlineActions.js b/typo3/sysext/backend/Resources/Public/JavaScript/SiteInlineActions.js new file mode 100644 index 0000000000000000000000000000000000000000..08db9e8947bed55bda66df1e3de3e30a089fe078 --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/SiteInlineActions.js @@ -0,0 +1,21 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +// Site configuration backend module FormEngine inline: +// Override inline 'create' and 'details' route to point to SiteInlineAjaxController +require(['jquery'], function($) { + $(function() { + TYPO3.inline.addMethod('create', 'site_configuration_inline_create'); + TYPO3.inline.addMethod('details', 'site_configuration_inline_details'); + }); +}); \ No newline at end of file diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataGroup/SiteConfigurationTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataGroup/SiteConfigurationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..079b94d815e6be1df8bc6e7c083694221cf457fe --- /dev/null +++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataGroup/SiteConfigurationTest.php @@ -0,0 +1,103 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Backend\Tests\Unit\Form\FormDataGroup; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Prophecy\Argument; +use Prophecy\Prophecy\ObjectProphecy; +use TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup; +use TYPO3\CMS\Backend\Form\FormDataProviderInterface; +use TYPO3\CMS\Core\Service\DependencyOrderingService; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * Test case + */ +class SiteConfigurationTest extends UnitTestCase +{ + /** + * @var SiteConfigurationDataGroup + */ + protected $subject; + + protected function setUp() + { + $this->subject = new SiteConfigurationDataGroup(); + } + + /** + * @test + */ + public function compileReturnsIncomingData() + { + /** @var DependencyOrderingService|ObjectProphecy $orderingServiceProphecy */ + $orderingServiceProphecy = $this->prophesize(DependencyOrderingService::class); + GeneralUtility::addInstance(DependencyOrderingService::class, $orderingServiceProphecy->reveal()); + $orderingServiceProphecy->orderByDependencies(Argument::cetera())->willReturnArgument(0); + + $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['siteConfiguration'] = []; + + $input = ['foo']; + + $this->assertEquals($input, $this->subject->compile($input)); + } + + /** + * @test + */ + public function compileReturnsResultChangedByDataProvider() + { + /** @var DependencyOrderingService|ObjectProphecy $orderingServiceProphecy */ + $orderingServiceProphecy = $this->prophesize(DependencyOrderingService::class); + GeneralUtility::addInstance(DependencyOrderingService::class, $orderingServiceProphecy->reveal()); + $orderingServiceProphecy->orderByDependencies(Argument::cetera())->willReturnArgument(0); + + /** @var FormDataProviderInterface|ObjectProphecy $formDataProviderProphecy */ + $formDataProviderProphecy = $this->prophesize(FormDataProviderInterface::class); + $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['siteConfiguration'] = [ + FormDataProviderInterface::class => [], + ]; + GeneralUtility::addInstance(FormDataProviderInterface::class, $formDataProviderProphecy->reveal()); + $providerResult = ['foo']; + $formDataProviderProphecy->addData(Argument::cetera())->shouldBeCalled()->willReturn($providerResult); + + $this->assertEquals($providerResult, $this->subject->compile([])); + } + + /** + * @test + */ + public function compileThrowsExceptionIfDataProviderDoesNotImplementInterface() + { + /** @var DependencyOrderingService|ObjectProphecy $orderingServiceProphecy */ + $orderingServiceProphecy = $this->prophesize(DependencyOrderingService::class); + GeneralUtility::addInstance(DependencyOrderingService::class, $orderingServiceProphecy->reveal()); + $orderingServiceProphecy->orderByDependencies(Argument::cetera())->willReturnArgument(0); + + /** @var FormDataProviderInterface|ObjectProphecy $formDataProviderProphecy */ + $formDataProviderProphecy = $this->prophesize(\stdClass::class); + $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['siteConfiguration'] = [ + \stdClass::class => [], + ]; + GeneralUtility::addInstance(\stdClass::class, $formDataProviderProphecy->reveal()); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionCode(1485299408); + + $this->subject->compile([]); + } +} diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/SiteDatabaseEditRowTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/SiteDatabaseEditRowTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b01f82973bdaec15ac1e1815e6b126dd775bb797 --- /dev/null +++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/SiteDatabaseEditRowTest.php @@ -0,0 +1,196 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Backend\Tests\Unit\Form\FormDataProvider; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Backend\Form\FormDataProvider\SiteDatabaseEditRow; +use TYPO3\CMS\Core\Site\Entity\Site; +use TYPO3\CMS\Core\Site\SiteFinder; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * Test case + */ +class SiteDatabaseEditRowTest extends UnitTestCase +{ + /** + * @test + */ + public function addDataDoesNotChangeResultIfCommandIsNotEdit() + { + $input = [ + 'command' => 'new', + 'foo' => 'bar', + ]; + $this->assertSame($input, (new SiteDatabaseEditRow())->addData($input)); + } + + /** + * @test + */ + public function addDataDoesNotChangeResultIfDatabaseRowIsNotEmpty() + { + $input = [ + 'command' => 'edit', + 'databaseRow' => [ + 'foo' => 'bar', + ] + ]; + $this->assertSame($input, (new SiteDatabaseEditRow())->addData($input)); + } + + /** + * @test + */ + public function addDataThrowsExceptionIfTableNameIsNotExpected() + { + $input = [ + 'command' => 'edit', + 'tableName' => 'foo', + ]; + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1520886234); + $siteFinderProphecy = $this->prophesize(SiteFinder::class); + GeneralUtility::addInstance(SiteFinder::class, $siteFinderProphecy->reveal()); + (new SiteDatabaseEditRow())->addData($input); + } + + /** + * @test + */ + public function addDataSetsDataForSysSite() + { + $input = [ + 'command' => 'edit', + 'tableName' => 'sys_site', + 'vanillaUid' => 23, + 'customData' => [ + 'siteIdentifier' => 'main', + ] + ]; + $rowData = [ + 'foo' => 'bar', + 'rootPageId' => 42, + 'someArray' => [ + 'foo' => 'bar', + ] + ]; + $siteFinderProphecy = $this->prophesize(SiteFinder::class); + GeneralUtility::addInstance(SiteFinder::class, $siteFinderProphecy->reveal()); + $siteProphecy = $this->prophesize(Site::class); + $siteFinderProphecy->getSiteByRootPageId(23)->willReturn($siteProphecy->reveal()); + $siteProphecy->getConfiguration()->willReturn($rowData); + + $expected = $input; + $expected['databaseRow'] = [ + 'uid' => 42, + 'identifier' => 'main', + 'rootPageId' => 42, + 'pid' => 0, + 'foo' => 'bar', + ]; + + $this->assertEquals($expected, (new SiteDatabaseEditRow())->addData($input)); + } + + /** + * @test + */ + public function addDataThrowsExceptionWithInvalidErrorHandling() + { + $input = [ + 'command' => 'edit', + 'tableName' => 'sys_site_errorhandling', + 'vanillaUid' => 23, + 'inlineTopMostParentUid' => 5, + 'inlineParentFieldName' => 'invalid', + ]; + $rowData = [ + 'foo' => 'bar', + ]; + $siteFinderProphecy = $this->prophesize(SiteFinder::class); + GeneralUtility::addInstance(SiteFinder::class, $siteFinderProphecy->reveal()); + $siteProphecy = $this->prophesize(Site::class); + $siteFinderProphecy->getSiteByRootPageId(5)->willReturn($siteProphecy->reveal()); + $siteProphecy->getConfiguration()->willReturn($rowData); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1520886092); + (new SiteDatabaseEditRow())->addData($input); + } + + /** + * @test + */ + public function addDataThrowsExceptionWithInvalidLanguage() + { + $input = [ + 'command' => 'edit', + 'tableName' => 'sys_site_language', + 'vanillaUid' => 23, + 'inlineTopMostParentUid' => 5, + 'inlineParentFieldName' => 'invalid', + ]; + $rowData = [ + 'foo' => 'bar', + ]; + $siteFinderProphecy = $this->prophesize(SiteFinder::class); + GeneralUtility::addInstance(SiteFinder::class, $siteFinderProphecy->reveal()); + $siteProphecy = $this->prophesize(Site::class); + $siteFinderProphecy->getSiteByRootPageId(5)->willReturn($siteProphecy->reveal()); + $siteProphecy->getConfiguration()->willReturn($rowData); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1520886092); + (new SiteDatabaseEditRow())->addData($input); + } + + /** + * @test + */ + public function addDataAddLanguageRow() + { + $input = [ + 'command' => 'edit', + 'tableName' => 'sys_site_language', + 'vanillaUid' => 23, + 'inlineTopMostParentUid' => 5, + 'inlineParentFieldName' => 'languages', + ]; + $rowData = [ + 'languages' => [ + 23 => [ + 'foo' => 'bar', + ], + ], + ]; + $siteFinderProphecy = $this->prophesize(SiteFinder::class); + GeneralUtility::addInstance(SiteFinder::class, $siteFinderProphecy->reveal()); + $siteProphecy = $this->prophesize(Site::class); + $siteFinderProphecy->getSiteByRootPageId(5)->willReturn($siteProphecy->reveal()); + $siteProphecy->getConfiguration()->willReturn($rowData); + + $expected = $input; + $expected['databaseRow'] = [ + 'foo' => 'bar', + 'uid' => 23, + 'pid' => 0, + ]; + + $this->assertEquals($expected, (new SiteDatabaseEditRow())->addData($input)); + } +} diff --git a/typo3/sysext/backend/ext_localconf.php b/typo3/sysext/backend/ext_localconf.php index 9b3f52749249fb9f8550deda0008d314ef56921b..5c9d69973f56ecf061208201599511713f77bc1d 100644 --- a/typo3/sysext/backend/ext_localconf.php +++ b/typo3/sysext/backend/ext_localconf.php @@ -43,3 +43,10 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['livesearch']['page'] = 'pages'; // Register BackendLayoutDataProvider for PageTs $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider']['pagets'] = \TYPO3\CMS\Backend\Provider\PageTsBackendLayoutDataProvider::class; + +// Register fieldInformation Provider for site configuration module +$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1522919823] = [ + 'nodeName' => 'SiteConfigurationModuleFieldInformation', + 'priority' => '70', + 'class' => \TYPO3\CMS\Backend\Form\FieldInformation\SiteConfiguration::class +]; diff --git a/typo3/sysext/backend/ext_tables.php b/typo3/sysext/backend/ext_tables.php index c23146803e9c781bb2cca5587949165b6ca33136..ac86abf7869d1dbd066b94cbe72e2c1e96093af6 100644 --- a/typo3/sysext/backend/ext_tables.php +++ b/typo3/sysext/backend/ext_tables.php @@ -23,6 +23,20 @@ $GLOBALS['TBE_STYLES']['skins']['backend'] = [ ] ); +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModule( + 'site', + 'configuration', + 'top', + '', + [ + 'routeTarget' => \TYPO3\CMS\Backend\Controller\SiteConfigurationController::class . '::handleRequest', + 'access' => 'admin', + 'name' => 'site_configuration', + 'icon' => 'EXT:backend/Resources/Public/Icons/module-sites.svg', + 'labels' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf' + ] +); + // "Sort sub pages" csh \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr( 'pages_sort', diff --git a/typo3/sysext/core/Classes/Configuration/SiteConfiguration.php b/typo3/sysext/core/Classes/Configuration/SiteConfiguration.php new file mode 100644 index 0000000000000000000000000000000000000000..f444f40f5ae1cf6b6e0fc4e7d68df044bb98e934 --- /dev/null +++ b/typo3/sysext/core/Classes/Configuration/SiteConfiguration.php @@ -0,0 +1,170 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Configuration; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Symfony\Component\Finder\Finder; +use Symfony\Component\Yaml\Yaml; +use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; +use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader; +use TYPO3\CMS\Core\Exception\SiteNotFoundException; +use TYPO3\CMS\Core\Site\Entity\Site; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Responsibility: Handles the format of the configuration (currently yaml), and the location of the file system folder + * + * Reads all available site configuration options, and puts them into Site objects. + * + * @internal + */ +class SiteConfiguration +{ + /** + * @var string + */ + protected $configPath; + + /** + * Config yaml file name. + * + * @internal + * @var string + */ + protected $configFileName = 'config.yaml'; + + /** + * Identifier to store all configuration data in cache_core cache. + * + * @internal + * @var string + */ + protected $cacheIdentifier = 'site-configuration'; + + /** + * @param string $configPath + */ + public function __construct(string $configPath) + { + $this->configPath = $configPath; + } + + /** + * Return all site objects which have been found in the filesystem. + * + * @return Site[] + */ + public function resolveAllExistingSites(): array + { + // Check if the data is already cached + if ($siteConfiguration = $this->getCache()->get($this->cacheIdentifier)) { + $siteConfiguration = json_decode($siteConfiguration, true); + } + + // Nothing in the cache (or no site found) + if (empty($siteConfiguration)) { + $finder = new Finder(); + try { + $finder->files()->depth(0)->name($this->configFileName)->in($this->configPath . '/*'); + } catch (\InvalidArgumentException $e) { + // Directory $this->configPath does not exist yet + $finder = []; + } + $loader = GeneralUtility::makeInstance(YamlFileLoader::class); + $siteConfiguration = []; + foreach ($finder as $fileInfo) { + $configuration = $loader->load((string)$fileInfo); + $identifier = basename($fileInfo->getPath()); + $siteConfiguration[$identifier] = $configuration; + } + $this->getCache()->set($this->cacheIdentifier, json_encode($siteConfiguration)); + } + $sites = []; + foreach ($siteConfiguration ?? [] as $identifier => $configuration) { + $rootPageId = (int)$configuration['site']['rootPageId'] ?? 0; + if ($rootPageId > 0) { + $sites[$identifier] = GeneralUtility::makeInstance(Site::class, $identifier, $rootPageId, $configuration['site']); + } + } + return $sites; + } + + /** + * Add or update a site configuration + * + * @param string $siteIdentifier + * @param array $configuration + * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + */ + public function write(string $siteIdentifier, array $configuration) + { + $fileName = $this->configPath . '/' . $siteIdentifier . '/' . $this->configFileName; + if (!file_exists($fileName)) { + GeneralUtility::mkdir_deep($this->configPath . '/' . $siteIdentifier); + } + $yamlFileContents = Yaml::dump($configuration, 99, 2); + GeneralUtility::writeFile($fileName, $yamlFileContents); + $this->getCache()->remove($this->cacheIdentifier); + } + + /** + * Renames a site identifier (and moves the folder) + * + * @param string $currentIdentifier + * @param string $newIdentifier + * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + */ + public function rename(string $currentIdentifier, string $newIdentifier) + { + $result = rename($this->configPath . '/' . $currentIdentifier, $this->configPath . '/' . $newIdentifier); + if (!$result) { + throw new \RuntimeException('Unable to rename folder sites/' . $currentIdentifier, 1522491300); + } + $this->getCache()->remove($this->cacheIdentifier); + } + + /** + * Removes the config.yaml file of a site configuration. + * Also clears the cache. + * + * @param string $siteIdentifier + * @throws SiteNotFoundException + */ + public function delete(string $siteIdentifier) + { + $sites = $this->resolveAllExistingSites(); + if (!isset($sites[$siteIdentifier])) { + throw new SiteNotFoundException('Site configuration named ' . $siteIdentifier . ' not found.', 1522866183); + } + $fileName = $this->configPath . '/' . $siteIdentifier . '/' . $this->configFileName; + if (!file_exists($fileName)) { + throw new SiteNotFoundException('Site configuration file ' . $this->configFileName . ' within the site ' . $siteIdentifier . ' not found.', 1522866184); + } + @unlink($fileName); + $this->getCache()->remove($this->cacheIdentifier); + } + + /** + * Short-hand function for the cache + * + * @return FrontendInterface + * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + */ + protected function getCache(): FrontendInterface + { + return GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_core'); + } +} diff --git a/typo3/sysext/core/Classes/Exception/SiteNotFoundException.php b/typo3/sysext/core/Classes/Exception/SiteNotFoundException.php new file mode 100644 index 0000000000000000000000000000000000000000..42ae05205cf66412f7de69ea97016f9e7cd7566d --- /dev/null +++ b/typo3/sysext/core/Classes/Exception/SiteNotFoundException.php @@ -0,0 +1,26 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Core\Exception; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Core\Exception; + +/** + * Exception thrown if site configuration or incoming data is invalid + */ +class SiteNotFoundException extends Exception +{ +} diff --git a/typo3/sysext/core/Classes/Site/Entity/Site.php b/typo3/sysext/core/Classes/Site/Entity/Site.php new file mode 100644 index 0000000000000000000000000000000000000000..908009a9d9298f9c0581aada737983c3bd475f90 --- /dev/null +++ b/typo3/sysext/core/Classes/Site/Entity/Site.php @@ -0,0 +1,223 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Site\Entity; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\PageErrorHandler\FluidPageErrorHandler; +use TYPO3\CMS\Frontend\PageErrorHandler\PageContentErrorHandler; +use TYPO3\CMS\Frontend\PageErrorHandler\PageErrorHandlerInterface; + +/** + * Entity representing a single site with available languages + */ +class Site +{ + const ERRORHANDLER_TYPE_PAGE = 'Page'; + const ERRORHANDLER_TYPE_FLUID = 'Fluid'; + const ERRORHANDLER_TYPE_PHP = 'PHP'; + + /** + * @var string + */ + protected $identifier; + + /** + * @var string + */ + protected $base; + + /** + * @var int + */ + protected $rootPageId; + + /** + * Any attributes for this site + * @var array + */ + protected $configuration; + + /** + * @var SiteLanguage[] + */ + protected $languages; + + /** + * @var array + */ + protected $errorHandlers; + + /** + * Sets up a site object, and its languages and error handlers + * + * @param string $identifier + * @param int $rootPageId + * @param array $configuration + */ + public function __construct(string $identifier, int $rootPageId, array $configuration) + { + $this->identifier = $identifier; + $this->rootPageId = $rootPageId; + $this->configuration = $configuration; + $configuration['languages'] = $configuration['languages'] ?: [0 => [ + 'languageId' => 0, + 'title' => 'Default', + 'navigationTitle' => '', + 'typo3Language' => 'default', + 'flag' => 'us', + 'locale' => 'en_US.UTF-8', + 'iso-639-1' => 'en', + 'hreflang' => 'en-US', + 'direction' => '', + ]]; + $this->base = $configuration['base'] ?? ''; + foreach ($configuration['languages'] as $languageConfiguration) { + $languageUid = (int)$languageConfiguration['languageId']; + $base = '/'; + if (!empty($languageConfiguration['base'])) { + $base = $languageConfiguration['base']; + } + $baseParts = parse_url($base); + if (empty($baseParts['scheme'])) { + $base = rtrim($this->base, '/') . '/' . ltrim($base, '/'); + } + $this->languages[$languageUid] = new SiteLanguage( + $this, + $languageUid, + $languageConfiguration['locale'], + $base, + $languageConfiguration + ); + } + foreach ($configuration['errorHandling'] ?? [] as $errorHandlingConfiguration) { + $code = $errorHandlingConfiguration['errorCode']; + unset($errorHandlingConfiguration['errorCode']); + $this->errorHandlers[(int)$code] = $errorHandlingConfiguration; + } + } + + /** + * Gets the identifier of this site + * + * @return string + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * Returns the base URL of this site + * + * @return string + */ + public function getBase(): string + { + return $this->base; + } + + /** + * Returns the root page ID of this site + * + * @return int + */ + public function getRootPageId(): int + { + return $this->rootPageId; + } + + /** + * Returns all available langauges of this site + * + * @return SiteLanguage[] + */ + public function getLanguages(): array + { + return $this->languages; + } + + /** + * Returns a language of this site, given by the sys_language_uid + * + * @param int $languageId + * @return SiteLanguage + * @throws \InvalidArgumentException + */ + public function getLanguageById(int $languageId): SiteLanguage + { + if (isset($this->languages[$languageId])) { + return $this->languages[$languageId]; + } + throw new \InvalidArgumentException( + 'Language ' . $languageId . ' does not exist on site ' . $this->identifier . '.', + 1522960188 + ); + } + + /** + * Returns a ready-to-use error handler, to be used within the ErrorController + * + * @param int $type + * @return PageErrorHandlerInterface + * @throws \RuntimeException + */ + public function getErrorHandler(int $type): PageErrorHandlerInterface + { + $errorHandler = $this->errorHandlers[$type]; + switch ($errorHandler['errorHandler']) { + case self::ERRORHANDLER_TYPE_FLUID: + return new FluidPageErrorHandler($type, $errorHandler); + case self::ERRORHANDLER_TYPE_PAGE: + return new PageContentErrorHandler($type, $errorHandler); + case self::ERRORHANDLER_TYPE_PHP: + // Check if the interface is implemented + $handler = GeneralUtility::makeInstance($errorHandler['errorPhpClassFQCN'], $type, $errorHandler); + if (!($handler instanceof PageErrorHandlerInterface)) { + // throw new exception + } + return $handler; + } + throw new \RuntimeException('Not implemented', 1522495914); + } + + /** + * Returns the whole configuration for this site + * + * @return array + */ + public function getConfiguration(): array + { + return $this->configuration; + } + + /** + * Returns a single configuration attribute + * + * @param string $attributeName + * @return mixed + * @throws \InvalidArgumentException + */ + public function getAttribute(string $attributeName) + { + if (isset($this->configuration[$attributeName])) { + return $this->configuration[$attributeName]; + } + throw new \InvalidArgumentException( + 'Attribute ' . $attributeName . ' does not exist on site ' . $this->identifier . '.', + 1522495954 + ); + } +} diff --git a/typo3/sysext/core/Classes/Site/Entity/SiteLanguage.php b/typo3/sysext/core/Classes/Site/Entity/SiteLanguage.php new file mode 100644 index 0000000000000000000000000000000000000000..2fc49c87b3b5effe62fe72efd32219fd61b21acc --- /dev/null +++ b/typo3/sysext/core/Classes/Site/Entity/SiteLanguage.php @@ -0,0 +1,289 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Site\Entity; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +/** + * Entity representing a sys_sitelanguage configuration of a site object. + */ +class SiteLanguage +{ + /** + * @var Site + */ + protected $site; + + /** + * The language mapped to the sys_language DB entry. + * + * @var int + */ + protected $languageId; + + /** + * Locale, like 'de_CH' or 'en_GB' + * + * @var string + */ + protected $locale; + + /** + * The Base URL for this language + * + * @var string + */ + protected $base; + + /** + * Label to be used within TYPO3 to identify the language + * @var string + */ + protected $title = 'Default'; + + /** + * Label to be used within language menus + * @var string + */ + protected $navigationTitle = ''; + + /** + * The flag key (like "gb" or "fr") used to be used in TYPO3's Backend. + * @var string + */ + protected $flagIdentifier = 'us'; + + /** + * The iso code for this language (two letter) ISO-639-1 + * @var string + */ + protected $twoLetterIsoCode = 'en'; + + /** + * Language tag for this language defined by RFC 1766 / 3066 for "lang" + * and "hreflang" attributes + * + * @var string + */ + protected $hreflang = 'en-US'; + + /** + * The direction for this language + * @var string + */ + protected $direction = ''; + + /** + * Prefix for TYPO3's language files + * "default" for english, otherwise one of TYPO3's internal language keys. + * Previously configured via TypoScript config.language = fr + * + * @var string + */ + protected $typo3Language = 'default'; + + /** + * @var string + */ + protected $fallbackType = 'strict'; + + /** + * @var array + */ + protected $fallbackLanguageIds = []; + + /** + * Additional parameters configured for this site language + * @var array + */ + protected $attributes = []; + + /** + * SiteLanguage constructor. + * @param Site $site + * @param int $languageId + * @param string $locale + * @param string $base + * @param array $attributes + */ + public function __construct(Site $site, int $languageId, string $locale, string $base, array $attributes) + { + $this->site = $site; + $this->languageId = $languageId; + $this->locale = $locale; + $this->base = $base; + $this->attributes = $attributes; + if (!empty($attributes['title'])) { + $this->title = $attributes['title']; + } + if (!empty($attributes['navigationTitle'])) { + $this->navigationTitle = $attributes['navigationTitle']; + } + if (!empty($attributes['flag'])) { + $this->flagIdentifier = $attributes['flag']; + } + if (!empty($attributes['typo3Language'])) { + $this->typo3Language = $attributes['typo3Language']; + } + if (!empty($attributes['iso-639-1'])) { + $this->twoLetterIsoCode = $attributes['iso-639-1']; + } + if (!empty($attributes['hreflang'])) { + $this->hreflang = $attributes['hreflang']; + } + if (!empty($attributes['direction'])) { + $this->direction = $attributes['direction']; + } + if (!empty($attributes['fallbackType'])) { + $this->fallbackType = $attributes['fallbackType']; + } + if (!empty($attributes['fallbacks'])) { + $this->fallbackLanguageIds = $attributes['fallbacks']; + } + } + + /** + * Returns the SiteLanguage in an array representation for e.g. the usage + * in TypoScript. + * + * @return array + */ + public function toArray() + { + return [ + 'languageId' => $this->getLanguageId(), + 'locale' => $this->getLocale(), + 'base' => $this->getBase(), + 'title' => $this->getTitle(), + 'navigationTitle' => $this->getNavigationTitle(), + 'twoLetterIsoCode' => $this->getTwoLetterIsoCode(), + 'hreflang' => $this->getHreflang(), + 'direction' => $this->getDirection(), + 'typo3Language' => $this->getTypo3Language(), + 'flagIdentifier' => $this->getFlagIdentifier(), + 'fallbackType' => $this->getFallbackType(), + 'fallbackLanguageIds' => $this->getFallbackLanguageIds(), + ]; + } + + /** + * @return Site + */ + public function getSite(): Site + { + return $this->site; + } + + /** + * @return int + */ + public function getLanguageId(): int + { + return $this->languageId; + } + + /** + * @return string + */ + public function getLocale(): string + { + return $this->locale; + } + + /** + * @return string + */ + public function getBase(): string + { + return $this->base; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @return string + */ + public function getNavigationTitle(): string + { + return $this->navigationTitle ?? $this->getTitle(); + } + + /** + * @return string + */ + public function getFlagIdentifier(): string + { + return $this->flagIdentifier; + } + + /** + * @return string + */ + public function getTypo3Language(): string + { + return $this->typo3Language; + } + + /** + * @return string + */ + public function getFallbackType(): string + { + return $this->fallbackType; + } + + /** + * Returns the ISO-639-1 language ISO code + * + * @return string + */ + public function getTwoLetterIsoCode(): string + { + return $this->twoLetterIsoCode ?? ''; + } + + /** + * Returns the RFC 1766 / 3066 language tag + * + * @return string + */ + public function getHreflang(): string + { + return $this->hreflang ?? ''; + } + + /** + * Returns the language direction + * + * @return string + */ + public function getDirection(): string + { + return $this->direction ?? ''; + } + + /** + * @return array + */ + public function getFallbackLanguageIds(): array + { + return $this->fallbackLanguageIds; + } +} diff --git a/typo3/sysext/core/Classes/Site/SiteFinder.php b/typo3/sysext/core/Classes/Site/SiteFinder.php new file mode 100644 index 0000000000000000000000000000000000000000..3c92524cde130b7fbb8831d9aab0b4db8bdd3ec0 --- /dev/null +++ b/typo3/sysext/core/Classes/Site/SiteFinder.php @@ -0,0 +1,178 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Site; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Core\Configuration\SiteConfiguration; +use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; +use TYPO3\CMS\Core\Exception\SiteNotFoundException; +use TYPO3\CMS\Core\Site\Entity\Site; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Is used in backend and frontend for all places where to read / identify sites and site languages. + */ +class SiteFinder +{ + /** + * @var Site[] + */ + protected $sites = []; + + /** + * Short-hand to quickly fetch a site based on a rootPageId + * + * @var array + */ + protected $mappingRootPageIdToIdentifier = []; + + /** + * Fetches all existing configurations as Site objects + */ + public function __construct() + { + $reader = GeneralUtility::makeInstance(SiteConfiguration::class, Environment::getConfigPath() . '/sites'); + $sites = $reader->resolveAllExistingSites(); + foreach ($sites as $identifier => $site) { + $this->sites[$identifier] = $site; + $this->mappingRootPageIdToIdentifier[$site->getRootPageId()] = $identifier; + } + } + + /** + * Return a list of all configured sites + * + * @return Site[] + */ + public function getAllSites(): array + { + return $this->sites; + } + + /** + * Get a list of all configured base uris of all sites + * + * @return array + */ + public function getBaseUris(): array + { + $baseUrls = []; + foreach ($this->sites as $site) { + /** @var SiteLanguage $language */ + foreach ($site->getLanguages() as $language) { + $baseUrls[$language->getBase()] = $language; + if ($language->getLanguageId() === 0) { + $baseUrls[$site->getBase()] = $language; + } + } + } + return $baseUrls; + } + + /** + * Find a site by given root page id + * + * @param int $rootPageId + * @return Site + * @throws SiteNotFoundException + */ + public function getSiteByRootPageId(int $rootPageId): Site + { + if (isset($this->mappingRootPageIdToIdentifier[$rootPageId])) { + return $this->sites[$this->mappingRootPageIdToIdentifier[$rootPageId]]; + } + throw new SiteNotFoundException('No site found for root page id ' . $rootPageId, 1521668882); + } + + /** + * Get a site language by given base URI + * + * @param string $uri + * @return mixed|null + */ + public function getSiteLanguageByBase(string $uri) + { + $baseUris = $this->getBaseUris(); + $bestMatchedUri = null; + foreach ($baseUris as $base => $language) { + if (strpos($uri, $base) === 0 && strlen($bestMatchedUri ?? '') < strlen($base)) { + $bestMatchedUri = $base; + } + } + $siteLanguage = $baseUris[$bestMatchedUri] ?? null; + if ($siteLanguage instanceof Site) { + $siteLanguage = $siteLanguage->getLanguageById(0); + } + return $siteLanguage; + } + + /** + * Find a site by given identifier + * + * @param string $identifier + * @return Site + * @throws SiteNotFoundException + */ + public function getSiteByIdentifier(string $identifier): Site + { + if (isset($this->sites[$identifier])) { + return $this->sites[$identifier]; + } + throw new SiteNotFoundException('No site found for identifier ' . $identifier, 1521716628); + } + + /** + * Traverses the rootline of a page up until a Site was found. + * + * @param int $pageId + * @param array $alternativeRootLine + * @return Site + * @throws SiteNotFoundException + */ + public function getSiteByPageId(int $pageId, array $alternativeRootLine = null): Site + { + if (is_array($alternativeRootLine)) { + foreach ($alternativeRootLine as $pageInRootLine) { + if ($pageInRootLine['uid'] > 0) { + try { + return $this->getSiteByRootPageId((int)$pageInRootLine['uid']); + } catch (SiteNotFoundException $e) { + // continue looping + } + } + } + } + // Do your own root line traversing + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + $queryBuilder->select('pid')->from('pages'); + $rootLinePageId = $pageId; + while ($rootLinePageId > 0) { + try { + return $this->getSiteByRootPageId($rootLinePageId); + } catch (SiteNotFoundException $e) { + // get parent page ID + $queryBuilder->where( + $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($rootLinePageId)) + ); + $rootLinePageId = (int)$queryBuilder->execute()->fetchColumn(0); + } + } + throw new SiteNotFoundException('No site found in root line of page ' . $pageId, 1521716622); + } +} diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php index b9142a62e8d595a222795abf6ec318b06d8b59d6..f94d9fe03386d098c25ba49de58fda8bc7bfe852 100644 --- a/typo3/sysext/core/Configuration/DefaultConfiguration.php +++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php @@ -802,6 +802,226 @@ return [ ], ], ], + 'siteConfiguration' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class => [], + \TYPO3\CMS\Backend\Form\FormDataProvider\SiteDatabaseEditRow::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class, + ] + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseParentPageRow::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\SiteDatabaseEditRow::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseUserPermissionCheck::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseDefaultLanguagePageRow::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseParentPageRow::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseEffectivePid::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseParentPageRow::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseUserPermissionCheck::class + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabasePageRootline::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseEffectivePid::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\UserTsConfig::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabasePageRootline::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\PageTsConfig::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseEffectivePid::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\UserTsConfig::class + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\InlineOverrideChildTca::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\PageTsConfig::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\ParentPageTca::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\InlineOverrideChildTca::class + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowInitializeNew::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseUserPermissionCheck::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\UserTsConfig::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\PageTsConfig::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\ParentPageTca::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseUniqueUidNewRow::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowInitializeNew::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowDateTimeFields::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseUniqueUidNewRow::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowDefaultValues::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowInitializeNew::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowDateTimeFields::class + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRecordOverrideValues::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRowDefaultValues::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaGroup::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRecordOverrideValues::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseSystemLanguageRows::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaGroup::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRecordOverrideValues::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRecordTypeValue::class => [ + 'depends' => [ + // As the ctrl.type can hold a nested key we need to resolve all relations + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaGroup::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\PageTsConfigMerged::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\PageTsConfig::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRecordTypeValue::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsOverrides::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRecordTypeValue::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseEditRow::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsOverrides::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessCommon::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessRecordTitle::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessCommon::class + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessPlaceholders::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessRecordTitle::class + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessShowitem::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessPlaceholders::class + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsRemoveUnused::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessCommon::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessRecordTitle::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessPlaceholders::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\InlineOverrideChildTca::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessShowitem::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaTypesShowitem::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseRecordTypeValue::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseSystemLanguageRows::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsRemoveUnused::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessFieldLabels::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaTypesShowitem::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaText::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessFieldLabels::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaRadioItems::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaText::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaCheckboxItems::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaRadioItems::class + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\SiteTcaSelectItems::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaCheckboxItems::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\DatabasePageRootline::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\PageTsConfigMerged::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaTypesShowitem::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsRemoveUnused::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaCheckboxItems::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\SiteTcaSelectItems::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaSelectItems::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\SiteTcaInline::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInputPlaceholders::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaRecordTitle::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\SiteTcaInline::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInputPlaceholders::class, + ], + ], + \TYPO3\CMS\Backend\Form\FormDataProvider\EvaluateDisplayConditions::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaRecordTitle::class, + ], + ], + ], ], ], ], diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-84581-SiteHandling.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-84581-SiteHandling.rst new file mode 100644 index 0000000000000000000000000000000000000000..45c7222329cca3a44444e13c622af05d0169338e --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-84581-SiteHandling.rst @@ -0,0 +1,157 @@ +.. include:: ../../Includes.txt + +========================================= +Feature: #84581 - Introduce Site Handling +========================================= + +See :issue:`84581` + +Description +=========== + +Site Handling has been added to TYPO3. + +Its goal is to make managing multiple sites easier to understand and faster to do. Sites bring a variety of new +concepts to TYPO3 which we will explain below. + +Take your time and read through the entire document since some concepts rely on each other. + + +typo3conf/sites folder +---------------------- + +New sites will live in the folder `typo3conf/sites/`. In the first iteration this folder will contain a file called +`config.yaml` which holds all configuration for a given site. + +In the future this folder can (and should) be used for more files like Fluid templates, and Backend layouts. + + +config.yaml +----------- + +.. code:: + + site: + # the rootPage Id (see below) + rootPageId: 12 + # my base domain to run this site on. It either accepts a fully qualified URL or "/" to react to any domain name + base: 'https://www.example.com/' + # The language array + languages: + - + # the TYPO3 sys_language_uid as you know it since... ever + languageId: '0' + # The internal name for this language. Unused for now, but in the future this will affect display in the backend + title: English + # optional navigation title which is used in HMENU.special = language + navigationTitle: '' + # Language base. Accepts either a fully qualified URL or a path segment like "/en/". + base: / + # sets the locale during frontend rendering + locale: en_EN.UTF8 + # ??? + iso-639-1: en + # FE href language + hreflang: en-US + # FE text direction + direction: ltr + # Language Identifier to use in localLang XLIFF files + typo3Language: default + # Flag Identifier + flag: gb + - + languageId: '1' + title: 'danish' + navigationTitle: Dansk + base: /da/ + locale: dk_DK.UTF8 + iso-639-1: da + hreflang: dk-DK + direction: ltr + typo3Language: default + flag: dk + fallbackType: strict + - + languageId: '2' + title: Deutsch + navigationTitle: '' + base: 'https://www.beispiel.de' + locale: de_DE.UTF-8 + iso-639-1: de + hreflang: de-DE + direction: ltr + typo3Language: de + flag: de + # Enable content fallback + fallbackType: fallback + # Content fallback mode (order is important) + fallbacks: '2,1,0' + # Error Handling Array (order is important here) + # Error Handlers will check the given status code, but the special value "0" will react to any error not configured + # elsewhere in this configuration. + errorHandling: + - + # HTTP Status Code to react to + errorCode: '404' + # The used ErrorHandler. In this case, it's "Display content from Page". See examples below for available options. + errorHandler: Page + # href to the content source to display (accepts both fully qualified URLs as well as TYPO3 internal link syntax + errorContentSource: 't3://page?uid=8' + - + errorCode: '401' + errorHandler: Fluid + # Path to the Template File to show + errorFluidTemplate: 'EXT:my_extension/Resources/Private/Templates/ErrorPages/401.html' + # Optional Templates root path + errorFluidTemplatesRootPath: 'EXT:my_extension/Resources/Private/Templates/ErrorPages' + # Optional Layouts root path + errorFluidLayoutsRootPath: 'EXT:my_extension/Resources/Private/Layouts/ErrorPages' + # Optional Partials root path + errorFluidPartialsRootPath: 'EXT:my_extension/Resources/Private/Partials/ErrorPages' + - + errorCode: '0' + errorHandler: PHP + # Fully qualified class name to a class that implements PageErrorHandlerInterface + errorPhpClassFQCN: Vendor\ExtensionName\ErrorHandlers\GenericErrorhandler + + +All settings can also be edited via the backend module `Site Management > Configuration`. + +Keep in mind that due to the nature of the module, comments or additional values in your :file:`config.yaml` file +**will** get deleted on saving. + + +site identifier +--------------- + +The site identifier is the name of the folder within `typo3conf/sites/` that will hold your configuration file(s). When +choosing an identifier make sure to stick to ASCII but you may also use `-`, `_` and `.` for convenience. + + +rootPageId +---------- + +Root pages are identified by one of these two properties: + +* they are direct descendants of PID 0 (the root root page of TYPO3) +* they have the "Use as Root Page" property in `pages` set to true. + + +Impact +====== + +The following TypoScript settings will be set based on `config.yaml` rather than needing to have them in your TypoScript +template: + +* config.language +* config.htmlTag_dir +* config.htmlTag_langKey +* config.sys_language_uid +* config.sys_language_mode +* config.sys_language_isocode +* config.sys_language_isocode_default + + +Links to pages within a site can now be generated via **any** access of TYPO3, so in both BE and FE as well as CLI mode. + +.. index:: Backend, Frontend, TypoScript \ No newline at end of file diff --git a/typo3/sysext/frontend/Classes/Controller/ErrorController.php b/typo3/sysext/frontend/Classes/Controller/ErrorController.php index c6c816a828509c0dfab9b130cf1ab00f4fff3613..0c2b1152a4d05aacfd69b973f9ad3b8d8fd5170a 100644 --- a/typo3/sysext/frontend/Classes/Controller/ErrorController.php +++ b/typo3/sysext/frontend/Classes/Controller/ErrorController.php @@ -16,12 +16,15 @@ namespace TYPO3\CMS\Frontend\Controller; */ use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Controller\ErrorPageController; use TYPO3\CMS\Core\Error\Http\PageNotFoundException; use TYPO3\CMS\Core\Error\Http\ServiceUnavailableException; use TYPO3\CMS\Core\Http\HtmlResponse; use TYPO3\CMS\Core\Http\RedirectResponse; +use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\PageErrorHandler\PageErrorHandlerInterface; /** * Handles "Page Not Found" or "Page Unavailable" requests, @@ -33,16 +36,22 @@ class ErrorController * Used for creating a 500 response ("Page unavailable"), usually due some misconfiguration * but if configured, a RedirectResponse could be returned as well. * + * @param ServerRequestInterface $request * @param string $message * @param array $reasons * @return ResponseInterface * @throws ServiceUnavailableException */ - public function unavailableAction(string $message, array $reasons = []): ResponseInterface + public function unavailableAction(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface { if (!$this->isPageUnavailableHandlerConfigured()) { throw new ServiceUnavailableException($message, 1518472181); } + $errorHandler = $this->getErrorHandlerFromSite($request, 500); + if ($errorHandler instanceof PageErrorHandlerInterface) { + $response = $errorHandler->handlePageError($request, $message, $reasons); + return $response->withStatus(500, $message); + } return $this->handlePageError( $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'], $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling_statheader'], @@ -55,13 +64,19 @@ class ErrorController * Used for creating a 404 response ("Page Not Found"), * but if configured, a RedirectResponse could be returned as well. * + * @param ServerRequestInterface $request * @param string $message * @param array $reasons * @return ResponseInterface * @throws PageNotFoundException */ - public function pageNotFoundAction(string $message, array $reasons = []): ResponseInterface + public function pageNotFoundAction(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface { + $errorHandler = $this->getErrorHandlerFromSite($request, 404); + if ($errorHandler instanceof PageErrorHandlerInterface) { + $response = $errorHandler->handlePageError($request, $message, $reasons); + return $response->withStatus(404, $message); + } if (!$GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling']) { throw new PageNotFoundException($message, 1518472189); } @@ -77,12 +92,18 @@ class ErrorController * Used for creating a 403 response ("Access denied"), * but if configured, a RedirectResponse could be returned as well. * + * @param ServerRequestInterface $request * @param string $message * @param array $reasons * @return ResponseInterface */ - public function accessDeniedAction(string $message, array $reasons = []): ResponseInterface + public function accessDeniedAction(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface { + $errorHandler = $this->getErrorHandlerFromSite($request, 403); + if ($errorHandler instanceof PageErrorHandlerInterface) { + $response = $errorHandler->handlePageError($request, $message, $reasons); + return $response->withStatus(403, $message); + } return $this->handlePageError( $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'], $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling_accessdeniedheader'], @@ -250,7 +271,7 @@ class ErrorController /** * Headers which have been requested, will be added to the response object. - * If a header is part of the HTTP Repsonse code, the response object will be annotated as well. + * If a header is part of the HTTP Response code, the response object will be annotated as well. * * @param ResponseInterface $response * @param string $headers @@ -275,4 +296,17 @@ class ErrorController } return $response; } + + /** + * Checks if a site is configured, and an error handler is configured for this specific status code. + * + * @param ServerRequestInterface $request + * @param int $statusCode + * @return PageErrorHandlerInterface|null + */ + protected function getErrorHandlerFromSite(ServerRequestInterface $request, int $statusCode): ?PageErrorHandlerInterface + { + $site = $request->getAttribute('site'); + return $site instanceof Site ? $site->getErrorHandler($statusCode) : $site; + } } diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php index 03644ee55d69f4fc49c78a42177c82ecceb07d96..4bbc47fa40706c7b835c291ef0c82bc86b447101 100644 --- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php +++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php @@ -16,6 +16,7 @@ namespace TYPO3\CMS\Frontend\Controller; use Doctrine\DBAL\DBALException; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use TYPO3\CMS\Backend\FrontendBackendUserAuthentication; @@ -41,6 +42,7 @@ use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Core\Page\PageRenderer; use TYPO3\CMS\Core\Resource\StorageRepository; use TYPO3\CMS\Core\Service\DependencyOrderingService; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; use TYPO3\CMS\Core\TimeTracker\TimeTracker; use TYPO3\CMS\Core\Type\Bitmask\Permission; use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser; @@ -852,7 +854,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface ); $this->logger->emergency($message, ['exception' => $exception]); try { - $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($message); + $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], $message); $this->sendResponseAndExit($response); } catch (ServiceUnavailableException $e) { throw new ServiceUnavailableException($message, 1301648782); @@ -1275,7 +1277,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface // We find the first page belonging to the current domain $timeTracker->push('fetch_the_id domain/', ''); // The page_id of the current domain - $this->domainStartPage = $this->findDomainRecord($GLOBALS['TYPO3_CONF_VARS']['SYS']['recursiveDomainSearch']); + if ($this->getCurrentSiteLanguage()) { + $this->domainStartPage = $this->getCurrentSiteLanguage()->getSite()->getRootPageId(); + } else { + $this->domainStartPage = $this->findDomainRecord($GLOBALS['TYPO3_CONF_VARS']['SYS']['recursiveDomainSearch']); + } if (!$this->id) { if ($this->domainStartPage) { // If the id was not previously set, set it to the id of the domain. @@ -1289,7 +1295,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface $message = 'No pages are found on the rootlevel!'; $this->logger->alert($message); try { - $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($message); + $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], $message); $this->sendResponseAndExit($response); } catch (ServiceUnavailableException $e) { throw new ServiceUnavailableException($message, 1301648975); @@ -1318,9 +1324,9 @@ class TypoScriptFrontendController implements LoggerAwareInterface ]; $message = $pNotFoundMsg[$this->pageNotFound]; if ($this->pageNotFound === 1 || $this->pageNotFound === 2) { - $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction($message, $this->getPageAccessFailureReasons()); + $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction($GLOBALS['TYPO3_REQUEST'], $message, $this->getPageAccessFailureReasons()); } else { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($message, $this->getPageAccessFailureReasons()); + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], $message, $this->getPageAccessFailureReasons()); } $this->sendResponseAndExit($response); } @@ -1411,7 +1417,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface $message = 'The requested page does not exist!'; $this->logger->error($message); try { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($message, $this->getPageAccessFailureReasons()); + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], $message, $this->getPageAccessFailureReasons()); $this->sendResponseAndExit($response); } catch (PageNotFoundException $e) { throw new PageNotFoundException($message, 1301648780); @@ -1423,7 +1429,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface $message = 'The requested page does not exist!'; $this->logger->error($message); try { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($message, $this->getPageAccessFailureReasons()); + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], $message, $this->getPageAccessFailureReasons()); $this->sendResponseAndExit($response); } catch (PageNotFoundException $e) { throw new PageNotFoundException($message, 1301648781); @@ -1462,7 +1468,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface $message = 'The requested page didn\'t have a proper connection to the tree-root!'; $this->logger->error($message); try { - $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($message, $this->getPageAccessFailureReasons()); + $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], $message, $this->getPageAccessFailureReasons()); $this->sendResponseAndExit($response); } catch (ServiceUnavailableException $e) { throw new ServiceUnavailableException($message, 1301648167); @@ -1473,7 +1479,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface if (empty($this->rootLine)) { $message = 'The requested page was not accessible!'; try { - $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($message, $this->getPageAccessFailureReasons()); + $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], $message, $this->getPageAccessFailureReasons()); $this->sendResponseAndExit($response); } catch (ServiceUnavailableException $e) { $this->logger->warning($message); @@ -2163,7 +2169,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface $cHash_calc = $this->cacheHash->calculateCacheHash($this->cHash_array); if (!hash_equals($cHash_calc, $this->cHash)) { if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']) { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction('Request parameters could not be validated (&cHash comparison failed)'); + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], 'Request parameters could not be validated (&cHash comparison failed)'); $this->sendResponseAndExit($response); } else { $this->disableCache(); @@ -2191,7 +2197,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface if ($this->tempContent) { $this->clearPageCacheContent(); } - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction('Request parameters could not be validated (&cHash empty)'); + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], 'Request parameters could not be validated (&cHash empty)'); $this->sendResponseAndExit($response); } else { $this->disableCache(); @@ -2409,11 +2415,15 @@ class TypoScriptFrontendController implements LoggerAwareInterface */ protected function createHashBase($createLockHashBase = false) { + // Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering + // is not cached properly as we don't have any language-specific conditions anymore + $siteBase = $this->getCurrentSiteLanguage() ? $this->getCurrentSiteLanguage()->getBase() : ''; $hashParameters = [ 'id' => (int)$this->id, 'type' => (int)$this->type, 'gr_list' => (string)$this->gr_list, 'MP' => (string)$this->MP, + 'siteBase' => $siteBase, 'cHash' => $this->cHash_array, 'domainStartPage' => $this->domainStartPage ]; @@ -2457,7 +2467,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface $message = 'The page is not configured! [type=' . $this->type . '][' . $this->sPre . '].'; $this->logger->alert($message); try { - $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($message); + $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], $message); $this->sendResponseAndExit($response); } catch (ServiceUnavailableException $e) { $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.'; @@ -2506,7 +2516,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface $message = 'No TypoScript template found!'; $this->logger->alert($message); try { - $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($message); + $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], $message); $this->sendResponseAndExit($response); } catch (ServiceUnavailableException $e) { throw new ServiceUnavailableException($message, 1294587218); @@ -2525,6 +2535,12 @@ class TypoScriptFrontendController implements LoggerAwareInterface ArrayUtility::mergeRecursiveWithOverrule($modifiedGetVars, GeneralUtility::_GET()); GeneralUtility::_GETset($modifiedGetVars); } + + // Auto-configure settings when a site is configured + if ($this->getCurrentSiteLanguage()) { + $this->config['config']['absRefPrefix'] = $this->config['config']['absRefPrefix'] ?? 'auto'; + } + // Hook for postProcessing the configuration array $params = ['config' => &$this->config['config']]; foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] ?? [] as $funcRef) { @@ -2551,8 +2567,14 @@ class TypoScriptFrontendController implements LoggerAwareInterface GeneralUtility::callUserFunction($_funcRef, $_params, $this); } + $siteLanguage = $this->getCurrentSiteLanguage(); + // Initialize charset settings etc. - $languageKey = $this->config['config']['language'] ?? 'default'; + if ($siteLanguage) { + $languageKey = $siteLanguage->getTypo3Language(); + } else { + $languageKey = $this->config['config']['language'] ?? 'default'; + } $this->lang = $languageKey; $this->setOutputLanguage($languageKey); @@ -2561,11 +2583,27 @@ class TypoScriptFrontendController implements LoggerAwareInterface $this->metaCharset = $this->config['config']['metaCharset']; } - // Get values from TypoScript, if not set before - if ($this->sys_language_uid === 0) { - $this->sys_language_uid = ($this->sys_language_content = (int)$this->config['config']['sys_language_uid']); + // Get values from site language + if ($siteLanguage) { + $this->sys_language_uid = ($this->sys_language_content = $siteLanguage->getLanguageId()); + $this->sys_language_mode = $siteLanguage->getFallbackType(); + if ($this->sys_language_mode === 'fallback') { + $fallbackOrder = $siteLanguage->getFallbackLanguageIds(); + $fallbackOrder[] = 'pageNotFound'; + } + } else { + // Get values from TypoScript, if not set before + if ($this->sys_language_uid === 0) { + $this->sys_language_uid = ($this->sys_language_content = (int)$this->config['config']['sys_language_uid']); + } + list($this->sys_language_mode, $fallbackOrder) = GeneralUtility::trimExplode(';', $this->config['config']['sys_language_mode']); + if (!empty($fallbackOrder)) { + $fallBackOrder = GeneralUtility::trimExplode(',', $fallbackOrder); + } else { + $fallBackOrder = [0]; + } } - list($this->sys_language_mode, $sys_language_content) = GeneralUtility::trimExplode(';', $this->config['config']['sys_language_mode']); + $this->sys_language_contentOL = $this->config['config']['sys_language_overlay']; // If sys_language_uid is set to another language than default: if ($this->sys_language_uid > 0) { @@ -2580,20 +2618,20 @@ class TypoScriptFrontendController implements LoggerAwareInterface if ($this->sys_language_uid) { // If requested translation is not available: if (GeneralUtility::hideIfNotTranslated($this->page['l18n_cfg'])) { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction('Page is not available in the requested language.'); + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], 'Page is not available in the requested language.'); $this->sendResponseAndExit($response); } else { switch ((string)$this->sys_language_mode) { case 'strict': - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction('Page is not available in the requested language (strict).'); + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], 'Page is not available in the requested language (strict).'); $this->sendResponseAndExit($response); break; + case 'fallback': case 'content_fallback': // Setting content uid (but leaving the sys_language_uid) when a content_fallback // value was found. - $fallBackOrder = GeneralUtility::trimExplode(',', $sys_language_content); - foreach ($fallBackOrder as $orderValue) { - if ($orderValue === '0' || $orderValue === '') { + foreach ($fallBackOrder ?? [] as $orderValue) { + if ($orderValue === '0' || $orderValue === 0 || $orderValue === '') { $this->sys_language_content = 0; break; } @@ -2630,14 +2668,16 @@ class TypoScriptFrontendController implements LoggerAwareInterface if ((!$this->sys_language_uid || !$this->sys_language_content) && GeneralUtility::hideIfDefaultLanguage($this->page['l18n_cfg'])) { $message = 'Page is not available in default language.'; $this->logger->error($message); - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($message); + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], $message); $this->sendResponseAndExit($response); } $this->updateRootLinesWithTranslations(); // Finding the ISO code for the currently selected language // fetched by the sys_language record when not fetching content from the default language - if ($this->sys_language_content > 0) { + if ($siteLanguage = $this->getCurrentSiteLanguage()) { + $this->sys_language_isocode = $siteLanguage->getTwoLetterIsoCode(); + } elseif ($this->sys_language_content > 0) { // using sys_language_content because the ISO code only (currently) affect content selection from FlexForms - which should follow "sys_language_content" // Set the fourth parameter to TRUE in the next two getRawRecord() calls to // avoid versioning overlay to be applied as it generates an SQL error @@ -2682,8 +2722,13 @@ class TypoScriptFrontendController implements LoggerAwareInterface public function settingLocale() { // Setting locale - if ($this->config['config']['locale_all']) { - $availableLocales = GeneralUtility::trimExplode(',', $this->config['config']['locale_all'], true); + $locale = $this->config['config']['locale_all']; + $siteLanguage = $this->getCurrentSiteLanguage(); + if ($siteLanguage) { + $locale = $siteLanguage->getLocale(); + } + if ($locale) { + $availableLocales = GeneralUtility::trimExplode(',', $locale, true); // If LC_NUMERIC is set e.g. to 'de_DE' PHP parses float values locale-aware resulting in strings with comma // as decimal point which causes problems with value conversions - so we set all locale types except LC_NUMERIC // @see https://bugs.php.net/bug.php?id=53711 @@ -2692,13 +2737,13 @@ class TypoScriptFrontendController implements LoggerAwareInterface // As str_* methods are locale aware and turkish has no upper case I // Class autoloading and other checks depending on case changing break with turkish locale LC_CTYPE // @see http://bugs.php.net/bug.php?id=35050 - if (substr($this->config['config']['locale_all'], 0, 2) !== 'tr') { + if (substr($locale, 0, 2) !== 'tr') { setlocale(LC_CTYPE, ...$availableLocales); } setlocale(LC_MONETARY, ...$availableLocales); setlocale(LC_TIME, ...$availableLocales); } else { - $this->getTimeTracker()->setTSlogMessage('Locale "' . htmlspecialchars($this->config['config']['locale_all']) . '" not found.', 3); + $this->getTimeTracker()->setTSlogMessage('Locale "' . htmlspecialchars($locale) . '" not found.', 3); } } } @@ -4725,4 +4770,18 @@ class TypoScriptFrontendController implements LoggerAwareInterface { return GeneralUtility::makeInstance(TimeTracker::class); } + + /** + * Returns the currently configured "site language" if a site is configured (= resolved) in the current request. + * + * @internal + */ + protected function getCurrentSiteLanguage(): ?SiteLanguage + { + if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface + && $GLOBALS['TYPO3_REQUEST']->getAttribute('language') instanceof SiteLanguage) { + return $GLOBALS['TYPO3_REQUEST']->getAttribute('language'); + } + return null; + } } diff --git a/typo3/sysext/frontend/Classes/Middleware/MaintenanceMode.php b/typo3/sysext/frontend/Classes/Middleware/MaintenanceMode.php index 3709350dbd288120ff70010b4ff524451a614a86..6105a43c60ba5442f47f9950f510968b5437fe85 100644 --- a/typo3/sysext/frontend/Classes/Middleware/MaintenanceMode.php +++ b/typo3/sysext/frontend/Classes/Middleware/MaintenanceMode.php @@ -47,7 +47,7 @@ class MaintenanceMode implements MiddlewareInterface $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] ) ) { - return GeneralUtility::makeInstance(ErrorController::class)->unavailableAction('This page is temporarily unavailable.'); + return GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], 'This page is temporarily unavailable.'); } // Continue the regular stack if no maintenance mode is active return $handler->handle($request); diff --git a/typo3/sysext/frontend/Classes/Middleware/SiteResolver.php b/typo3/sysext/frontend/Classes/Middleware/SiteResolver.php new file mode 100644 index 0000000000000000000000000000000000000000..fc8368e19d0a863fe820a3aa406259dbcb87c2e4 --- /dev/null +++ b/typo3/sysext/frontend/Classes/Middleware/SiteResolver.php @@ -0,0 +1,88 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Frontend\Middleware; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use TYPO3\CMS\Core\Exception\SiteNotFoundException; +use TYPO3\CMS\Core\Site\Entity\Site; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; +use TYPO3\CMS\Core\Site\SiteFinder; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Identify the current request and resolve the site to it. + * After that middleware, TSFE should be populated with + * - language configuration + * - site configuration + * + * Properties like config.sys_language_uid and config.language is then set for TypoScript. + */ +class SiteResolver implements MiddlewareInterface +{ + /** + * Resolve the site/language information by checking the page ID or the URL. + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $finder = GeneralUtility::makeInstance(SiteFinder::class); + + $site = null; + $language = null; + + $pageId = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0; + $languageId = $request->getQueryParams()['L'] ?? $request->getParsedBody()['L'] ?? null; + + // 1. Check if we have a _GET/_POST parameter for "id", then a site information can be resolved based. + if ($pageId > 0 && $languageId !== null) { + // Loop over the whole rootline without permissions to get the actual site information + try { + $site = $finder->getSiteByPageId((int)$pageId); + $language = $site->getLanguageById($languageId); + } catch (SiteNotFoundException $e) { + } + } + if (!($language instanceof SiteLanguage)) { + // 2. Check if there is a site language, if not, just don't do anything + $language = $finder->getSiteLanguageByBase((string)$request->getUri()); + // @todo: use exception for getSiteLanguageByBase + if ($language) { + $site = $language->getSite(); + } + } + + // Add language+site information to the PSR-7 request object. + if ($language instanceof SiteLanguage && $site instanceof Site) { + $request = $request->withAttribute('site', $site); + $request = $request->withAttribute('language', $language); + $queryParams = $request->getQueryParams(); + // necessary to calculate the proper hash base + $queryParams['L'] = $language->getLanguageId(); + $request = $request->withQueryParams($queryParams); + $_GET['L'] = $queryParams['L']; + // At this point, we later get further route modifiers + // for bw-compat we update $GLOBALS[TYPO3_REQUEST] to be used later in TSFE. + $GLOBALS['TYPO3_REQUEST'] = $request; + } + return $handler->handle($request); + } +} diff --git a/typo3/sysext/frontend/Classes/Page/PageGenerator.php b/typo3/sysext/frontend/Classes/Page/PageGenerator.php index bae36916de61a90044b653762c6ceb35ff75ff9a..835fceb88bcc1ecd4d8dd402f51a739b935493fe 100644 --- a/typo3/sysext/frontend/Classes/Page/PageGenerator.php +++ b/typo3/sysext/frontend/Classes/Page/PageGenerator.php @@ -14,7 +14,9 @@ namespace TYPO3\CMS\Frontend\Page; * The TYPO3 project - inspiring people to share! */ +use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Page\PageRenderer; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; use TYPO3\CMS\Core\TimeTracker\TimeTracker; use TYPO3\CMS\Core\Type\File\ImageInfo; use TYPO3\CMS\Core\TypoScript\TypoScriptService; @@ -103,9 +105,14 @@ class PageGenerator $tsfe->content = ''; $htmlTagAttributes = []; $htmlLang = $tsfe->config['config']['htmlTag_langKey'] ?: ($tsfe->sys_language_isocode ?: 'en'); - // Set content direction: (More info: http://www.tau.ac.il/~danon/Hebrew/HTML_and_Hebrew.html) - if ($tsfe->config['config']['htmlTag_dir']) { - $htmlTagAttributes['dir'] = htmlspecialchars($tsfe->config['config']['htmlTag_dir']); + // Set content direction + // More info: http://www.tau.ac.il/~danon/Hebrew/HTML_and_Hebrew.html) + $direction = $tsfe->config['config']['htmlTag_dir']; + if (self::getCurrentSiteLanguage()) { + $direction = self::getCurrentSiteLanguage()->getDirection(); + } + if ($direction) { + $htmlTagAttributes['dir'] = htmlspecialchars($direction); } // Setting document type: $docTypeParts = []; @@ -930,4 +937,18 @@ class PageGenerator ); } } + + /** + * Returns the currently configured "site language" if a site is configured (= resolved) in the current request. + * + * @internal + */ + protected static function getCurrentSiteLanguage(): ?SiteLanguage + { + if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface + && $GLOBALS['TYPO3_REQUEST']->getAttribute('language') instanceof SiteLanguage) { + return $GLOBALS['TYPO3_REQUEST']->getAttribute('language'); + } + return null; + } } diff --git a/typo3/sysext/frontend/Classes/PageErrorHandler/DefaultPHPErrorHandler.php b/typo3/sysext/frontend/Classes/PageErrorHandler/DefaultPHPErrorHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..9c6b37aa751a0701768925791af276cddbafea26 --- /dev/null +++ b/typo3/sysext/frontend/Classes/PageErrorHandler/DefaultPHPErrorHandler.php @@ -0,0 +1,81 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Frontend\PageErrorHandler; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Http\HtmlResponse; + +class DefaultPHPErrorHandler implements PageErrorHandlerInterface +{ + + /* + /$$$$$$$ +| $$__ $$ +| $$ \ $$ /$$$$$$ /$$$$$$/$$$$ /$$$$$$ /$$ /$$ /$$$$$$ +| $$$$$$$/ /$$__ $$| $$_ $$_ $$ /$$__ $$| $$ /$$//$$__ $$ +| $$__ $$| $$$$$$$$| $$ \ $$ \ $$| $$ \ $$ \ $$/$$/| $$$$$$$$ +| $$ \ $$| $$_____/| $$ | $$ | $$| $$ | $$ \ $$$/ | $$_____/ +| $$ | $$| $$$$$$$| $$ | $$ | $$| $$$$$$/ \ $/ | $$$$$$$ +|__/ |__/ \_______/|__/ |__/ |__/ \______/ \_/ \_______/ + + + + /$$ + | $$ + /$$ /$$ /$$| $$$$$$$ /$$$$$$ /$$$$$$$ +| $$ | $$ | $$| $$__ $$ /$$__ $$| $$__ $$ +| $$ | $$ | $$| $$ \ $$| $$$$$$$$| $$ \ $$ +| $$ | $$ | $$| $$ | $$| $$_____/| $$ | $$ +| $$$$$/$$$$/| $$ | $$| $$$$$$$| $$ | $$ + \_____/\___/ |__/ |__/ \_______/|__/ |__/ + + + + /$$ + | $$ + /$$$$$$/$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ +| $$_ $$_ $$ /$$__ $$ /$$__ $$ /$$__ $$ /$$__ $$ /$$__ $$ +| $$ \ $$ \ $$| $$$$$$$$| $$ \__/| $$ \ $$| $$$$$$$$| $$ | $$ +| $$ | $$ | $$| $$_____/| $$ | $$ | $$| $$_____/| $$ | $$ +| $$ | $$ | $$| $$$$$$$| $$ | $$$$$$$| $$$$$$$| $$$$$$$ +|__/ |__/ |__/ \_______/|__/ \____ $$ \_______/ \_______/ + /$$ \ $$ + | $$$$$$/ + \______/ + */ + + /** + * @var int + */ + protected $statusCode; + + public function __construct(int $statusCode, array $configuration) + { + $this->statusCode = $statusCode; + } + + /** + * @param ServerRequestInterface $request + * @param string $message + * @param array $reasons + * @return ResponseInterface + */ + public function handlePageError(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface + { + return new HtmlResponse('go away', $this->statusCode); + } +} diff --git a/typo3/sysext/frontend/Classes/PageErrorHandler/FluidPageErrorHandler.php b/typo3/sysext/frontend/Classes/PageErrorHandler/FluidPageErrorHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..1734ac7eefa412e43f3df19ac5e4a2efbe9e2593 --- /dev/null +++ b/typo3/sysext/frontend/Classes/PageErrorHandler/FluidPageErrorHandler.php @@ -0,0 +1,77 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Frontend\PageErrorHandler; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Http\HtmlResponse; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Fluid\View\TemplateView; +use TYPO3Fluid\Fluid\View\ViewInterface; + +/** + * An error handler that renders a fluid template. + * This is typically configured via the "Sites configuration" module in the backend. + */ +class FluidPageErrorHandler implements PageErrorHandlerInterface +{ + /** + * @var ViewInterface + */ + protected $view; + + /** + * @var int + */ + protected $statusCode; + + /** + * FluidPageErrorHandler constructor. + * @param int $statusCode + * @param array $configuration + */ + public function __construct(int $statusCode, array $configuration) + { + $this->statusCode = $statusCode; + $this->view = GeneralUtility::makeInstance(TemplateView::class); + if (!empty($configuration['errorFluidTemplatesRootPath'])) { + $this->view->setTemplateRootPaths([$configuration['errorFluidTemplatesRootPath']]); + } + if (!empty($configuration['errorFluidLayoutsRootPath'])) { + $this->view->setLayoutRootPaths([$configuration['errorFluidLayoutsRootPath']]); + } + if (!empty($configuration['errorFluidPartialsRootPath'])) { + $this->view->setPartialRootPaths([$configuration['errorFluidPartialsRootPath']]); + } + $this->view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($configuration['errorFluidTemplate'])); + } + + /** + * @param ServerRequestInterface $request + * @param string $message + * @param array $reasons + * @return ResponseInterface + */ + public function handlePageError(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface + { + $this->view->assignMultiple([ + 'request' => $request, + 'message' => $message, + 'reasons' => $reasons + ]); + return new HtmlResponse($this->view->render()); + } +} diff --git a/typo3/sysext/frontend/Classes/PageErrorHandler/PageContentErrorHandler.php b/typo3/sysext/frontend/Classes/PageErrorHandler/PageContentErrorHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..7e06cc89c4c8f9b35fb953c3d112747e894657a1 --- /dev/null +++ b/typo3/sysext/frontend/Classes/PageErrorHandler/PageContentErrorHandler.php @@ -0,0 +1,105 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Frontend\PageErrorHandler; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Backend\Routing\PageUriBuilder; +use TYPO3\CMS\Core\Http\HtmlResponse; +use TYPO3\CMS\Core\LinkHandling\LinkService; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Install\FolderStructure\Exception\InvalidArgumentException; + +/** + * Renders the content of a page to be displayed (also in relation to language etc) + * This is typically configured via the "Sites configuration" module in the backend. + */ +class PageContentErrorHandler implements PageErrorHandlerInterface +{ + + /** + * @var int + */ + protected $statusCode; + + /** + * @var array + */ + protected $errorHandlerConfiguration; + + /** + * PageContentErrorHandler constructor. + * @param int $statusCode + * @param array $configuration + * @throws InvalidArgumentException + */ + public function __construct(int $statusCode, array $configuration) + { + $this->statusCode = $statusCode; + if (empty($configuration['errorContentSource'])) { + throw new InvalidArgumentException('PageContentErrorHandler needs to have a proper link set.', 1522826413); + } + $this->errorHandlerConfiguration = $configuration; + } + + /** + * @param ServerRequestInterface $request + * @param string $message + * @param array $reasons + * @return ResponseInterface + */ + public function handlePageError(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface + { + $resolvedUrl = $this->resolveUrl($request, $this->errorHandlerConfiguration['errorContentSource']); + $content = GeneralUtility::getUrl($resolvedUrl); + return new HtmlResponse($content, $this->statusCode); + } + + /** + * Resolve the URL (currently only page and external URL are supported) + * + * @param ServerRequestInterface $request + * @param string $typoLinkUrl + * @return string + */ + protected function resolveUrl(ServerRequestInterface $request, string $typoLinkUrl): string + { + $linkService = GeneralUtility::makeInstance(LinkService::class); + $urlParams = $linkService->resolve($typoLinkUrl); + if ($urlParams['type'] !== 'page' && $urlParams['type'] !== 'url') { + throw new \InvalidArgumentException('PageContentErrorHandler can only handle TYPO3 urls of types "page" or "url"', 1522826609); + } + if ($urlParams['type'] === 'url') { + return $urlParams['url']; + } + + // Build Url + $languageUid = null; + $siteLanguage = $request->getAttribute('language'); + if ($siteLanguage instanceof SiteLanguage) { + $languageUid = $siteLanguage->getLanguageId(); + } + $uriBuilder = GeneralUtility::makeInstance(PageUriBuilder::class); + return (string)$uriBuilder->buildUri( + (int)$urlParams['pageuid'], + [], + null, + ['language' => $languageUid], + PageUriBuilder::ABSOLUTE_URL + ); + } +} diff --git a/typo3/sysext/frontend/Classes/PageErrorHandler/PageErrorHandlerInterface.php b/typo3/sysext/frontend/Classes/PageErrorHandler/PageErrorHandlerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..82d52a2841a71956735766dd2581a914aaf6de49 --- /dev/null +++ b/typo3/sysext/frontend/Classes/PageErrorHandler/PageErrorHandlerInterface.php @@ -0,0 +1,28 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Frontend\PageErrorHandler; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Page error handler interface + * Should be implemented by all custom PHP-related Page Error Handlers. + */ +interface PageErrorHandlerInterface +{ + public function handlePageError(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface; +} diff --git a/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php index cc7376dec4f7282096ab1a25ca97317303a6debc..363c31eefb436157abf99974fa8b78da9e919596 100644 --- a/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php +++ b/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php @@ -15,8 +15,11 @@ namespace TYPO3\CMS\Frontend\Typolink; * The TYPO3 project - inspiring people to share! */ +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Backend\Routing\PageUriBuilder; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; use TYPO3\CMS\Frontend\ContentObject\TypolinkModifyLinkConfigForPageLinksHookInterface; @@ -480,6 +483,7 @@ class PageLinkBuilder extends AbstractTypolinkBuilder */ protected function createTotalUrlAndLinkData($page, $target, $no_cache, $addParams = '', $typeOverride = '', $targetDomain = '') { + $allQueryParameters = []; $LD = []; // Adding Mount Points, "&MP=", parameter for the current page if any is set // but non other set explicitly @@ -507,25 +511,67 @@ class PageLinkBuilder extends AbstractTypolinkBuilder // Override... if ($typeNum) { $LD['type'] = '&type=' . (int)$typeNum; + $allQueryParameters['type'] = (int)$typeNum; } else { $LD['type'] = ''; } // Preserving the type number. $LD['orig_type'] = $LD['type']; // noCache - $LD['no_cache'] = $no_cache ? '&no_cache=1' : ''; - // linkVars - if ($addParams) { - $LD['linkVars'] = GeneralUtility::implodeArrayForUrl('', GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams), '', false, true); + if ($no_cache) { + $LD['no_cache'] = '&no_cache=1'; + $allQueryParameters['no_cache'] = 1; } else { - $LD['linkVars'] = $this->getTypoScriptFrontendController()->linkVars; + $LD['no_cache'] = ''; + } + // linkVars + $queryParameters = GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams); + if (!empty($queryParameters)) { + $allQueryParameters = array_replace_recursive($queryParameters, $allQueryParameters); + $LD['linkVars'] = GeneralUtility::implodeArrayForUrl('', $queryParameters, '', false, true); } // Add absRefPrefix if exists. $LD['url'] = $this->getTypoScriptFrontendController()->absRefPrefix . $LD['url']; // If the special key 'sectionIndex_uid' (added 'manually' in tslib/menu.php to the page-record) is set, then the link jumps directly to a section on the page. $LD['sectionIndex'] = $page['sectionIndex_uid'] ? '#c' . $page['sectionIndex_uid'] : ''; - // Compile the normal total url - $LD['totalURL'] = rtrim($LD['url'] . $LD['type'] . $LD['no_cache'] . $LD['linkVars'] . $this->getTypoScriptFrontendController()->getMethodUrlIdToken, '?') . $LD['sectionIndex']; + + // Compile the total url + $urlParts = parse_url($LD['url']); + + // Now see if the URL can be replaced by a URL generated by the Site-based Page Builder, + // but first find out if a language has been set explicitly + if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) { + $currentSiteLanguage = $GLOBALS['TYPO3_REQUEST']->getAttribute('language'); + if ($currentSiteLanguage instanceof SiteLanguage) { + $languageId = $currentSiteLanguage->getLanguageId(); + } + } + $absRefPrefix = $this->getTypoScriptFrontendController()->absRefPrefix; + $languageId = $queryParameters['L'] ?? $languageId ?? null; + $totalUrl = (string)GeneralUtility::makeInstance(PageUriBuilder::class)->buildUri( + (int)$page['uid'], + $allQueryParameters, + $LD['sectionIndex'], + ['language' => $languageId, 'alternativePageId' => $page['alias'] ?: $page['uid'], 'legacyUrlPrefix' => $absRefPrefix], + (!$urlParts['scheme'] && !$urlParts['host']) ? PageUriBuilder::ABSOLUTE_PATH : PageUriBuilder::ABSOLUTE_URL + ); + + // $totalUri contains /index.php for legacy URLs, as previously "it was index.php" + // In case an URI has is prefixed with "/" which is not the absRefPrefix, remove it. + // this might change in the future + if (strpos($totalUrl, '/index.php') === 0 && strpos($totalUrl, $absRefPrefix) !== 0) { + $totalUrl = substr($totalUrl, 1); + } + + // Add the method url id token later-on + if ($this->getTypoScriptFrontendController()->getMethodUrlIdToken) { + if (strpos($totalUrl, '#') !== false) { + $totalUrl = str_replace('#', $this->getTypoScriptFrontendController()->getMethodUrlIdToken . '#', $totalUrl); + } else { + $totalUrl .= $this->getTypoScriptFrontendController()->getMethodUrlIdToken; + } + } + $LD['totalURL'] = $totalUrl; // Call post processing function for link rendering: $_params = [ 'LD' => &$LD, diff --git a/typo3/sysext/frontend/Configuration/RequestMiddlewares.php b/typo3/sysext/frontend/Configuration/RequestMiddlewares.php index f5367b67e5c33cbdec5219a6f718acdf1a566d7c..cc67614ff023814d78c2dba86e8ddb3760f0f076 100644 --- a/typo3/sysext/frontend/Configuration/RequestMiddlewares.php +++ b/typo3/sysext/frontend/Configuration/RequestMiddlewares.php @@ -69,12 +69,25 @@ return [ 'typo3/cms-frontend/tsfe', ] ], + 'typo3/cms-frontend/site' => [ + 'target' => \TYPO3\CMS\Frontend\Middleware\SiteResolver::class, + 'after' => [ + 'typo3/cms-core/normalized-params-attribute', + 'typo3/cms-frontend/tsfe', + 'typo3/cms-frontend/authentication', + 'typo3/cms-frontend/backend-user-authentication', + ], + 'before' => [ + 'typo3/cms-frontend/page-resolver' + ] + ], 'typo3/cms-frontend/page-resolver' => [ 'target' => \TYPO3\CMS\Frontend\Middleware\PageResolver::class, 'after' => [ 'typo3/cms-frontend/tsfe', 'typo3/cms-frontend/authentication', 'typo3/cms-frontend/backend-user-authentication', + 'typo3/cms-frontend/site', ] ], ] diff --git a/typo3/sysext/frontend/Tests/Unit/Controller/ErrorControllerTest.php b/typo3/sysext/frontend/Tests/Unit/Controller/ErrorControllerTest.php index 793fb35a75931f5552d2427fe8976b136bf292bd..b1814887ac68edf50f45bc6b53c9aba8cff024eb 100644 --- a/typo3/sysext/frontend/Tests/Unit/Controller/ErrorControllerTest.php +++ b/typo3/sysext/frontend/Tests/Unit/Controller/ErrorControllerTest.php @@ -22,6 +22,7 @@ use TYPO3\CMS\Core\Http\HtmlResponse; use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Http\RequestFactory; use TYPO3\CMS\Core\Http\Response; +use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\Controller\ErrorController; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; @@ -63,8 +64,9 @@ class ErrorControllerTest extends UnitTestCase $this->expectExceptionMessage('This test page was not found!'); $this->expectExceptionCode(1518472189); $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'] = false; + $GLOBALS['TYPO3_REQUEST'] = []; $subject = new ErrorController(); - $subject->pageNotFoundAction('This test page was not found!'); + $subject->pageNotFoundAction(new ServerRequest(), 'This test page was not found!'); } /** @@ -177,11 +179,12 @@ X-TYPO3-Additional-Header: Banana Stand', $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['HTTP_HOST'] = 'localhost'; $_SERVER['SSL_SESSION_ID'] = true; + $GLOBALS['TYPO3_REQUEST'] = []; $this->prophesizeErrorPageController(); $subject = new ErrorController(); - $response = $subject->pageNotFoundAction($message); + $response = $subject->pageNotFoundAction(new ServerRequest(), $message); if (is_array($expectedResponseDetails)) { $this->assertInstanceOf($expectedResponseDetails['type'], $response); $this->assertEquals($expectedResponseDetails['statusCode'], $response->getStatusCode()); @@ -206,11 +209,12 @@ X-TYPO3-Additional-Header: Banana Stand'; $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['HTTP_HOST'] = 'localhost'; $_SERVER['SSL_SESSION_ID'] = true; + $GLOBALS['TYPO3_REQUEST'] = []; $this->prophesizeErrorPageController(); $subject = new ErrorController(); $this->prophesizeGetUrl(); - $response = $subject->pageNotFoundAction('Custom message'); + $response = $subject->pageNotFoundAction(new ServerRequest(), 'Custom message'); $expectedResponseDetails = [ 'type' => HtmlResponse::class, @@ -268,8 +272,9 @@ X-TYPO3-Additional-Header: Banana Stand'; $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['HTTP_HOST'] = 'localhost'; $_SERVER['SSL_SESSION_ID'] = true; + $GLOBALS['TYPO3_REQUEST'] = []; $subject = new ErrorController(); - $response = $subject->accessDeniedAction($message); + $response = $subject->accessDeniedAction(new ServerRequest(), $message); if (is_array($expectedResponseDetails)) { $this->assertInstanceOf($expectedResponseDetails['type'], $response); $this->assertEquals($expectedResponseDetails['statusCode'], $response->getStatusCode()); @@ -293,7 +298,7 @@ X-TYPO3-Additional-Header: Banana Stand'; $this->expectExceptionCode(1518472181); $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'] = false; $subject = new ErrorController(); - $subject->unavailableAction('All your system are belong to us!'); + $subject->unavailableAction(new ServerRequest(), 'All your system are belong to us!'); } /** @@ -308,7 +313,7 @@ X-TYPO3-Additional-Header: Banana Stand'; $this->expectExceptionMessage('All your system are belong to us!'); $this->expectExceptionCode(1518472181); $subject = new ErrorController(); - $subject->unavailableAction('All your system are belong to us!'); + $subject->unavailableAction(new ServerRequest(), 'All your system are belong to us!'); } /** @@ -422,10 +427,11 @@ X-TYPO3-Additional-Header: Banana Stand', $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['HTTP_HOST'] = 'localhost'; $_SERVER['SSL_SESSION_ID'] = true; + $GLOBALS['TYPO3_REQUEST'] = []; $this->prophesizeGetUrl(); $this->prophesizeErrorPageController(); $subject = new ErrorController(); - $response = $subject->unavailableAction($message); + $response = $subject->unavailableAction(new ServerRequest(), $message); if (is_array($expectedResponseDetails)) { $this->assertInstanceOf($expectedResponseDetails['type'], $response); $this->assertEquals($expectedResponseDetails['statusCode'], $response->getStatusCode()); @@ -451,10 +457,11 @@ X-TYPO3-Additional-Header: Banana Stand'; $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['HTTP_HOST'] = 'localhost'; $_SERVER['SSL_SESSION_ID'] = true; + $GLOBALS['TYPO3_REQUEST'] = []; $this->prophesizeErrorPageController(); $this->prophesizeGetUrl(); $subject = new ErrorController(); - $response = $subject->unavailableAction('custom message'); + $response = $subject->unavailableAction(new ServerRequest(), 'custom message'); $expectedResponseDetails = [ 'type' => HtmlResponse::class,