From 0824e6e84b05d10fb431d9de308f37dbdce198b4 Mon Sep 17 00:00:00 2001 From: Christian Kuhn <lolli@schwarzbu.ch> Date: Sun, 1 Apr 2018 13:15:55 +0200 Subject: [PATCH] [FEATURE] Introduce Site Handling TYPO3 is famously known for the "multi-site" functionality, allowing multiple websites running within one TYPO3 instance. However, configuring a multi-site had various downsides, mostly regarding to domain/entrypoint handling for a site, and if lots of languages were in place. Concepts like "absRefPrefix", "baseURL", various language related TypoScript settings, and the infamous "L" GET parameter can now be seen obsolete. Also, handling page-not-found or access-denied errors have never been easier, as every admin/integrator is able to configure this. What TYPO3 calls a "site" is a entrypoint / pagetree, and contains both configuration values relevant for Backend and Frontend. A site configuration has a unique (human-readable) "site identifier" and the following additional values: * Root page ID This is a page on the root level (pid=0) or having "is_siteroot" checked. * The base path / base URL This HTTP entry point e.g. https://www.mydomain.com/ ("Base URL" / HTTP entry point, like https://www.mydomain.com/) -- This allows to fully identify a pagetree with an entrypoint without having to guess during "runtime". * The definition of all available languages for this pagetree, including the default language for this specific pagetree. -- This includes both values for backend-related as well as information, previously only settable via TypoScript. This way, it is possible to have a TYPO3 installation with 20 languages, but only using 5 languages within one pagetree (site), using 15 different languages in another site, while also giving meaning for all records within one site. A site configuration can be added or edited within the TYPO3 Backend in a new backend module for admins, and is then persisted in "typo3conf/sites/site-identifier/config.yaml". The configuration format yaml is chosen as it minimizes the risk of doing hacks, but the concept of a SiteConfiguration can be adapted / exchanged to be overloaded or found in various other places in the future. Adding a site configuration for a project has various benefits: - Configuration is in one place, stored in the file system thus, is deployable. - Configuration can be done by an integrator/admin without any programming skills => in one place. - The necessity to query sys_language is only needed when configuring a site. - No need to configure TSconfig options like "previewDomains" and "defaultLanguageLabel" are gone. is - No need to configure any TypoScript conditions, or even TypoScript settings related to language handling. - It is possible to configure error handling on a per-site level, even language-dependant, without having to code or configure anything. However, if no site is configured for a pagetree, the previous behaviour is still in place, allowing to migrate slowly to the sites handling, as some key functionality like URL path handling for speaking URLs is not in place yet. It is important to understand that adding a site configuration comes with various restrictions: - "sys_domain" handling is not necessary anymore, as a page is resolved via the domain+base URL given in the configuration - Any previously configured TypoScript conditions based on L parameter do not apply anymore if a site is configured - This also applies to any config.*language* related TypoScript setting. - It is necessary to use a URI with scheme and path to configure a site, where as previously, TYPO3 would work without a base URL. - mod_rewrite or something similar is a requirement for sites to work properly. Further improvements not yet implemented: - Ensure backend modules like Web->Page, Web->View, Web->List and Web->Info only show records in the configured site languages. - Enable the possibility to handle "domain entry aliases", also for multi-server setups with different domain names. - Ensure the new Site module can handle "read-only" / deployable site configurations. - Allow to activate a language for a site to be editable in the Backend but not be available in the Frontend for everybody. - Handling "Storage Folder" on the top level with different language entries. - Improve URL generation in frontend to skip sys_domain resolving. - TypoScript conditions for [site = my-identifier] and [siteLanguage = dk]. - Improve proper caching for rootline resolving of pages without restrictions. - Improve resolving of siteLanguage from the current request. - Linking from one site to a page of a different site. - Centralizing access to sys_language and sys_domain. - Handle copying/moving of records to a different site with different languages and language settings. - Handle configuration change (like deleting a language in the configuration - what should happen to the translated records?) Next up for 9.3: - Adding "Routers" on top of sites for URL resolving of pages and records. - Handle Storage Folders on top level rootline - Handle Mount Points New API: - New Entity classes "Site" and "SiteLanguage" are resolved as part of a PSR-15 middleware and available for pages in FE and BE when possible. - A SiteFinder object is used to query Site and SiteLanguage objects and used in various places. - The new PageUriBuilder allows to create links to pages without any relation to the current request, and are already in use in Frontend links and Backend preview links. - A PageErrorHandlerInterface allows to custom error handlers to be introduced by extensions and configured on any site. Site handling is considered still "under development" until TYPO3 9 LTS, and implementation as well as the configuration format might change in the next sprint releases, in order to gather feedback on what is missing in the implementation. As a site configuration will be mandatory for TYPO3 v10.0, some changes regarding sys_language and sys_domain will follow. Beware: - Due to the definition of every record of default language (language=0), it might be possible to switch to locales for languages and get rid of the language ID. - sys_domain won't make it any much longer. Resolves: #84581 Releases: master Change-Id: Iabeeb6835a98c8f5a71d502379ed63a68dfad6dd Reviewed-on: https://review.typo3.org/56505 Tested-by: TYPO3com <no-reply@typo3.com> Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de> Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de> Reviewed-by: Georg Ringer <georg.ringer@gmail.com> Tested-by: Georg Ringer <georg.ringer@gmail.com> Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de> Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de> --- .../Configuration/SiteTcaConfiguration.php | 72 ++ .../SiteConfigurationController.php | 645 ++++++++++++++++++ .../Controller/SiteInlineAjaxController.php | 405 +++++++++++ .../SiteValidationErrorException.php | 25 + .../FieldInformation/SiteConfiguration.php | 51 ++ .../SiteConfigurationDataGroup.php | 47 ++ .../FormDataProvider/SiteDatabaseEditRow.php | 72 ++ .../Form/FormDataProvider/SiteTcaInline.php | 309 +++++++++ .../FormDataProvider/SiteTcaSelectItems.php | 63 ++ .../Classes/Middleware/SiteResolver.php | 61 ++ .../Classes/Routing/PageUriBuilder.php | 118 ++++ .../Classes/Utility/BackendUtility.php | 18 +- .../Configuration/Backend/AjaxRoutes.php | 12 + .../Configuration/RequestMiddlewares.php | 6 + .../SiteConfigurationTCA/sys_site.php | 91 +++ .../sys_site_errorhandling.php | 157 +++++ .../sys_site_language.php | 456 +++++++++++++ .../Language/locallang_siteconfiguration.xlf | 59 ++ .../locallang_siteconfiguration_module.xlf | 17 + .../locallang_siteconfiguration_tca.xlf | 121 ++++ .../siteconfiguration_fieldinformation.xlf | 32 + .../Templates/SiteConfiguration/Edit.html | 18 + .../Templates/SiteConfiguration/Overview.html | 79 +++ .../Public/Icons/module-contentelements.svg | 1 + .../Resources/Public/Icons/module-sites.svg | 1 + .../Public/Icons/module-templates.svg | 1 + .../Resources/Public/Icons/module-urls.svg | 1 + .../Public/JavaScript/SiteInlineActions.js | 21 + .../FormDataGroup/SiteConfigurationTest.php | 103 +++ .../SiteDatabaseEditRowTest.php | 196 ++++++ typo3/sysext/backend/ext_localconf.php | 7 + typo3/sysext/backend/ext_tables.php | 14 + .../Configuration/SiteConfiguration.php | 170 +++++ .../Exception/SiteNotFoundException.php | 26 + .../sysext/core/Classes/Site/Entity/Site.php | 223 ++++++ .../core/Classes/Site/Entity/SiteLanguage.php | 289 ++++++++ typo3/sysext/core/Classes/Site/SiteFinder.php | 178 +++++ .../Configuration/DefaultConfiguration.php | 220 ++++++ .../master/Feature-84581-SiteHandling.rst | 157 +++++ .../Classes/Controller/ErrorController.php | 42 +- .../TypoScriptFrontendController.php | 117 +++- .../Classes/Middleware/MaintenanceMode.php | 2 +- .../Classes/Middleware/SiteResolver.php | 88 +++ .../frontend/Classes/Page/PageGenerator.php | 27 +- .../DefaultPHPErrorHandler.php | 81 +++ .../FluidPageErrorHandler.php | 77 +++ .../PageContentErrorHandler.php | 105 +++ .../PageErrorHandlerInterface.php | 28 + .../Classes/Typolink/PageLinkBuilder.php | 60 +- .../Configuration/RequestMiddlewares.php | 13 + .../Unit/Controller/ErrorControllerTest.php | 23 +- 51 files changed, 5152 insertions(+), 53 deletions(-) create mode 100644 typo3/sysext/backend/Classes/Configuration/SiteTcaConfiguration.php create mode 100644 typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php create mode 100644 typo3/sysext/backend/Classes/Controller/SiteInlineAjaxController.php create mode 100644 typo3/sysext/backend/Classes/Exception/SiteValidationErrorException.php create mode 100644 typo3/sysext/backend/Classes/Form/FieldInformation/SiteConfiguration.php create mode 100644 typo3/sysext/backend/Classes/Form/FormDataGroup/SiteConfigurationDataGroup.php create mode 100644 typo3/sysext/backend/Classes/Form/FormDataProvider/SiteDatabaseEditRow.php create mode 100644 typo3/sysext/backend/Classes/Form/FormDataProvider/SiteTcaInline.php create mode 100644 typo3/sysext/backend/Classes/Form/FormDataProvider/SiteTcaSelectItems.php create mode 100644 typo3/sysext/backend/Classes/Middleware/SiteResolver.php create mode 100644 typo3/sysext/backend/Classes/Routing/PageUriBuilder.php create mode 100644 typo3/sysext/backend/Configuration/SiteConfigurationTCA/sys_site.php create mode 100644 typo3/sysext/backend/Configuration/SiteConfigurationTCA/sys_site_errorhandling.php create mode 100644 typo3/sysext/backend/Configuration/SiteConfigurationTCA/sys_site_language.php create mode 100644 typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration.xlf create mode 100644 typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf create mode 100644 typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf create mode 100644 typo3/sysext/backend/Resources/Private/Language/siteconfiguration_fieldinformation.xlf create mode 100644 typo3/sysext/backend/Resources/Private/Templates/SiteConfiguration/Edit.html create mode 100644 typo3/sysext/backend/Resources/Private/Templates/SiteConfiguration/Overview.html create mode 100644 typo3/sysext/backend/Resources/Public/Icons/module-contentelements.svg create mode 100644 typo3/sysext/backend/Resources/Public/Icons/module-sites.svg create mode 100644 typo3/sysext/backend/Resources/Public/Icons/module-templates.svg create mode 100644 typo3/sysext/backend/Resources/Public/Icons/module-urls.svg create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/SiteInlineActions.js create mode 100644 typo3/sysext/backend/Tests/Unit/Form/FormDataGroup/SiteConfigurationTest.php create mode 100644 typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/SiteDatabaseEditRowTest.php create mode 100644 typo3/sysext/core/Classes/Configuration/SiteConfiguration.php create mode 100644 typo3/sysext/core/Classes/Exception/SiteNotFoundException.php create mode 100644 typo3/sysext/core/Classes/Site/Entity/Site.php create mode 100644 typo3/sysext/core/Classes/Site/Entity/SiteLanguage.php create mode 100644 typo3/sysext/core/Classes/Site/SiteFinder.php create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-84581-SiteHandling.rst create mode 100644 typo3/sysext/frontend/Classes/Middleware/SiteResolver.php create mode 100644 typo3/sysext/frontend/Classes/PageErrorHandler/DefaultPHPErrorHandler.php create mode 100644 typo3/sysext/frontend/Classes/PageErrorHandler/FluidPageErrorHandler.php create mode 100644 typo3/sysext/frontend/Classes/PageErrorHandler/PageContentErrorHandler.php create mode 100644 typo3/sysext/frontend/Classes/PageErrorHandler/PageErrorHandlerInterface.php diff --git a/typo3/sysext/backend/Classes/Configuration/SiteTcaConfiguration.php b/typo3/sysext/backend/Classes/Configuration/SiteTcaConfiguration.php new file mode 100644 index 000000000000..9347d2907d4b --- /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 000000000000..de8ea402b253 --- /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 000000000000..d732aeeeed95 --- /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 000000000000..e7a84fa399f1 --- /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 000000000000..648f22ff1e7d --- /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 000000000000..f5b47e9b3cf7 --- /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 000000000000..b4f1a60558f2 --- /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 000000000000..9b6e5400f2c0 --- /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 000000000000..958c92953776 --- /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 000000000000..f28021cb48c5 --- /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 000000000000..13e5cca310bb --- /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 f22e7e6a2f19..076cdd3bce01 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 5f1383e6dfdc..002a36add171 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 d29b921f8b75..bb8048e441a8 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 000000000000..834cfb3f6019 --- /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 000000000000..c90677310a2e --- /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 000000000000..389ac30b3875 --- /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 000000000000..cd16846f2189 --- /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 000000000000..d8edf48ac3e7 --- /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 000000000000..f3a5fd2a068d --- /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 000000000000..5051ff417db1 --- /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 000000000000..eca5d6776513 --- /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 000000000000..ebf3726439a2 --- /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 000000000000..342c843d5d79 --- /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 000000000000..fe87ff9fcfde --- /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 000000000000..a8156e7e687f --- /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 000000000000..b0abf121da52 --- /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 000000000000..08db9e8947be --- /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 000000000000..079b94d815e6 --- /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 000000000000..b01f82973bda --- /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 9b3f52749249..5c9d69973f56 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 c23146803e9c..ac86abf7869d 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 000000000000..f444f40f5ae1 --- /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 000000000000..42ae05205cf6 --- /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 000000000000..908009a9d929 --- /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 000000000000..2fc49c87b3b5 --- /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 000000000000..3c92524cde13 --- /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 b9142a62e8d5..f94d9fe03386 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 000000000000..45c7222329cc --- /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 c6c816a82850..0c2b1152a4d0 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 03644ee55d69..4bbc47fa4070 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 3709350dbd28..6105a43c60ba 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 000000000000..fc8368e19d0a --- /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 bae36916de61..835fceb88bcc 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 000000000000..9c6b37aa751a --- /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 000000000000..1734ac7eefa4 --- /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 000000000000..7e06cc89c4c8 --- /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 000000000000..82d52a2841a7 --- /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 cc7376dec4f7..363c31eefb43 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 f5367b67e5c3..cc67614ff023 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 793fb35a7593..b1814887ac68 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, -- GitLab