From a94b7b5fc58adbff72a1a6fcfb982026db155c5e Mon Sep 17 00:00:00 2001 From: Benjamin Franzke Date: Mon, 22 Jan 2024 11:34:53 +0100 Subject: [PATCH] [FEATURE] Make backend URL configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TYPO3 Backend URL is made configurable in order to enable optional protection against application admin interface infrastructure enumeration (WSTG-CONF-05). Both, frontend and backend requests are now handled by the PHP script `/index.php` to enable virtual admin interface URLs. The default TYPO3 Backend entrypoint path `/typo3` can be changed by specifying a custom URL path or domain name in `$GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint']`. This change requires web server adaption. A silent migration and according documentation for custom web server configurations is added. A deprecation layer (for non-adapted systems) is in place that rewrites the server environment variables passed to `/typo3/index.php` as if `/index.php` was used directly. This layer will be removed in TYPO3 v14. This change does not take assets into account, only routing is adapted. That means composer mode will use assets provided via _assets as before and TYPO3 classic mode will serve backend assets from /typo3/* even if another backend URL is used and configured. In composer mode there is an additional opt-out for the installation of the legacy entrypoint for that can be defined in composer.json: "extra": { "typo3/cms": { "install-deprecated-typo3-index-php": false } } The application flow is slightly adapted by moving common middlewares into a separate core middleware chain. This chain is dispatched by a distinct core HTTP application (which is invoked by index.php). These middlewares are suitable for proxy determination or generic access control – basically everything not needed for subrequests. The core HTTP request handler then decides whether the request is to be routed to the frontend or backend application. Frontend and backend appplications are still designed to work independently with a plain PSR-7 Server Request in order for sub requests from backend to frontend (or vice versa) to work. The following diagram outlines the new application workflow including flow of possible sub requests (not yet used from backend to frontend, but it shows how they are intended to be invoked): +-------------------+ | | | Core HTTP | | Application | | | +---------+---------+ | | v +---------+---------+ | | | Core HTTP | | Middlewares | | | +---------+---------+ | | v +---------+---------+ | | | Core HTTP | +-------------+ Request Handler +--------------+ | | | | | +-------------------+ | | | v v +--------+----------+ +---------+---------+ | | (Sub Request) | | | Frontend HTTP +<-------------+ | Backend HTTP | | Application +<-----------+ | | Application | | | | | | | +---------+---------+ | | +---------+---------+ | | | | | | | | v | | v +---------+---------+ | | +---------+---------+ | | | | | | | Frontend HTTP | | | | Backend HTTP | | Middlewares | | | | Middlewares | | | | | | | +---------+---------+ | | +---------+---------+ | | | | | | | | v | | v +---------+---------+ | | +---------+---------+ | | | | | | | Frontend HTTP | | | | Backend HTTP | | Request Handler | | | | Request Handler | | | | | | | +---------+---------+ | | +---------+---------+ | | | | | | | | v | | v +---------+---------+ | | +---------+---------+ | | | | | | | TypoScript | | | | Backend Route | | Frontend +------------+ | | Dispatcher | | Controller | | | | | | | +---------+---------+ +-------------------+ | | | | | v | +---------+---------+ | | | | | Backend | +------------+ Controller | | | +-------------------+ Commands executed: # For changed in https://github.com/TYPO3/testing-framework/pull/533 composer req --dev "typo3/testing-framework":"dev-main" Resolves: #87889 Releases: main Change-Id: I3c96d4d7c58f08ed302ee35eb75d28afbf77686a Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/74366 Reviewed-by: Oliver Hader Reviewed-by: Stefan Bürk Tested-by: core-ci Tested-by: Stefan Bürk Tested-by: Christian Kuhn Reviewed-by: Christian Kuhn Tested-by: Oliver Hader Reviewed-by: Benni Mack Tested-by: Benni Mack --- .../TypeScript/backend/module/router.ts | 3 +- composer.lock | 8 +- .../Classes/Composer/InstallerScripts.php | 7 +- .../Classes/Controller/BackendController.php | 3 + .../backend/Classes/Http/Application.php | 23 +--- .../sysext/backend/Classes/Routing/Router.php | 12 +- .../backend/Classes/ServiceProvider.php | 8 +- .../Configuration/RequestMiddlewares.php | 7 -- .../backend/Resources/Private/Php/backend.php | 21 ---- .../Resources/Private/Php/legacy-backend.php | 48 +++++++ .../Private/Templates/Backend/Main.html | 2 +- .../Public/JavaScript/module/router.js | 4 +- .../Tests/Functional/Routing/RouterTest.php | 31 +++-- .../Buttons/Action/ShortcutButtonTest.php | 2 +- .../Tests/Unit/Routing/UriBuilderTest.php | 10 +- .../Classes/Composer/InstallerScripts.php | 11 +- .../Classes/Core/SystemEnvironmentBuilder.php | 11 +- .../sysext/core/Classes/Http/Application.php | 59 +++++++++ .../core/Classes/Http/RequestHandler.php | 47 +++++++ .../Middleware/NormalizedParamsAttribute.php | 5 +- .../Routing/BackendEntryPointResolver.php | 100 +++++++++++++-- .../ContentSecurityPolicy/PolicyProvider.php | 8 +- typo3/sysext/core/Classes/ServiceProvider.php | 28 +++++ .../Configuration/DefaultConfiguration.php | 1 + .../DefaultConfigurationDescription.yaml | 3 + .../core/Configuration/RequestMiddlewares.php | 33 +++++ ...TYPO3BackendEntrypointScriptDeprecated.rst | 117 ++++++++++++++++++ ...ture-87889-ConfigurableTYPO3BackendURL.rst | 92 ++++++++++++++ .../Resources/Private/Php/index.php} | 4 +- .../Application/Login/BackendLoginCest.php | 6 +- .../Extension/ApplicationEnvironment.php | 4 +- .../Tests/Unit/Utility/GeneralUtilityTest.php | 20 +-- .../Unit/Mvc/Web/Routing/UriBuilderTest.php | 5 +- .../ViewHelpers/Be/LinkViewHelperTest.php | 12 +- .../ViewHelpers/Be/UriViewHelperTest.php | 12 +- .../ViewHelpers/Link/PageViewHelperTest.php | 2 +- .../ViewHelpers/Uri/PageViewHelperTest.php | 2 +- .../Classes/Composer/InstallerScripts.php | 41 ------ .../frontend/Classes/Http/Application.php | 18 --- .../frontend/Classes/ServiceProvider.php | 4 - .../Configuration/RequestMiddlewares.php | 9 +- .../Unit/Resource/FilePathSanitizerTest.php | 8 +- .../indexed_search/Tests/Unit/IndexerTest.php | 12 -- .../Classes/Controller/LayoutController.php | 5 +- .../WebServerConfigurationFileService.php | 106 ++++++---------- .../install/Classes/ServiceProvider.php | 4 +- .../root-htaccess | 8 +- .../root-web-config | 5 +- .../install/Resources/Private/Php/install.php | 2 +- .../Private/Templates/Layout/MainLayout.html | 2 +- .../Fixtures/.htaccess_already_updated | 6 +- .../Fixtures/.htaccess_custom_config | 7 -- .../Functional/Fixtures/.htaccess_outdated | 13 ++ .../Tests/Functional/Fixtures/.htaccess_valid | 7 -- .../Functional/Fixtures/.htaccess_wrong_order | 7 -- .../Fixtures/web.config_already_updated | 3 +- .../Fixtures/web.config_custom_config | 12 -- .../Functional/Fixtures/web.config_outdated | 41 ++++++ .../Functional/Fixtures/web.config_valid | 12 -- .../Fixtures/web.config_wrong_order | 12 -- .../WebServerConfigurationFileServiceTest.php | 39 ++---- 61 files changed, 767 insertions(+), 387 deletions(-) delete mode 100644 typo3/sysext/backend/Resources/Private/Php/backend.php create mode 100644 typo3/sysext/backend/Resources/Private/Php/legacy-backend.php create mode 100644 typo3/sysext/core/Classes/Http/Application.php create mode 100644 typo3/sysext/core/Classes/Http/RequestHandler.php create mode 100644 typo3/sysext/core/Configuration/RequestMiddlewares.php create mode 100644 typo3/sysext/core/Documentation/Changelog/13.0/Deprecation-87889-TYPO3BackendEntrypointScriptDeprecated.rst create mode 100644 typo3/sysext/core/Documentation/Changelog/13.0/Feature-87889-ConfigurableTYPO3BackendURL.rst rename typo3/sysext/{frontend/Resources/Private/Php/frontend.php => core/Resources/Private/Php/index.php} (79%) delete mode 100644 typo3/sysext/frontend/Classes/Composer/InstallerScripts.php delete mode 100644 typo3/sysext/install/Tests/Functional/Fixtures/.htaccess_custom_config create mode 100644 typo3/sysext/install/Tests/Functional/Fixtures/.htaccess_outdated delete mode 100644 typo3/sysext/install/Tests/Functional/Fixtures/.htaccess_valid delete mode 100644 typo3/sysext/install/Tests/Functional/Fixtures/.htaccess_wrong_order delete mode 100644 typo3/sysext/install/Tests/Functional/Fixtures/web.config_custom_config create mode 100644 typo3/sysext/install/Tests/Functional/Fixtures/web.config_outdated delete mode 100644 typo3/sysext/install/Tests/Functional/Fixtures/web.config_valid delete mode 100644 typo3/sysext/install/Tests/Functional/Fixtures/web.config_wrong_order diff --git a/Build/Sources/TypeScript/backend/module/router.ts b/Build/Sources/TypeScript/backend/module/router.ts index bfcea526cc61..bcaab54e2d42 100644 --- a/Build/Sources/TypeScript/backend/module/router.ts +++ b/Build/Sources/TypeScript/backend/module/router.ts @@ -50,6 +50,7 @@ export class ModuleRouter extends LitElement { @property({ type: String, attribute: 'state-tracker' }) stateTrackerUrl: string; @property({ type: String, attribute: 'sitename' }) sitename: string; @property({ type: Boolean, attribute: 'sitename-first' }) sitenameFirst: boolean; + @property({ type: String, attribute: 'entry-point' }) entryPoint: string; @query('slot', true) slotElement: HTMLSlotElement; constructor() { @@ -212,7 +213,7 @@ export class ModuleRouter extends LitElement { const controller = params.get('install[controller]'); params.delete('install[controller]'); params.delete('install[context]'); - url.pathname = url.pathname.replace('/typo3/install.php', '/typo3/module/tools/' + controller); + url.pathname = url.pathname.replace('/typo3/install.php', this.entryPoint + 'module/tools/' + controller); } else { // non token-urls cannot be mapped by // the main backend controller right now diff --git a/composer.lock b/composer.lock index 9c9f011c4cc2..341df46b7855 100644 --- a/composer.lock +++ b/composer.lock @@ -8736,12 +8736,12 @@ "source": { "type": "git", "url": "https://github.com/TYPO3/testing-framework.git", - "reference": "ba5af3f321cfcf9bb3ed055562297cb262ee9063" + "reference": "9750f4a9f17552cbe38865ed5f7c0d9162c694d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/ba5af3f321cfcf9bb3ed055562297cb262ee9063", - "reference": "ba5af3f321cfcf9bb3ed055562297cb262ee9063", + "url": "https://api.github.com/repos/TYPO3/testing-framework/zipball/9750f4a9f17552cbe38865ed5f7c0d9162c694d7", + "reference": "9750f4a9f17552cbe38865ed5f7c0d9162c694d7", "shasum": "" }, "require": { @@ -8802,7 +8802,7 @@ "issues": "https://github.com/TYPO3/testing-framework/issues", "source": "https://github.com/TYPO3/testing-framework/tree/main" }, - "time": "2024-01-17T18:52:07+00:00" + "time": "2024-01-23T10:07:54+00:00" } ], "aliases": [], diff --git a/typo3/sysext/backend/Classes/Composer/InstallerScripts.php b/typo3/sysext/backend/Classes/Composer/InstallerScripts.php index e63a05df994b..a49c70e71bb0 100644 --- a/typo3/sysext/backend/Classes/Composer/InstallerScripts.php +++ b/typo3/sysext/backend/Classes/Composer/InstallerScripts.php @@ -31,9 +31,14 @@ class InstallerScripts implements InstallerScriptsRegistration { public static function register(Event $event, ScriptDispatcher $scriptDispatcher) { + $extra = $event->getComposer()->getPackage()->getExtra(); + $installDeprecatedTypo3IndexPhp = $extra['typo3/cms']['install-deprecated-typo3-index-php'] ?? true; + if (!$installDeprecatedTypo3IndexPhp) { + return; + } $scriptDispatcher->addInstallerScript( new EntryPoint( - dirname(__DIR__, 2) . '/Resources/Private/Php/backend.php', + dirname(__DIR__, 2) . '/Resources/Private/Php/legacy-backend.php', 'typo3/index.php' ) ); diff --git a/typo3/sysext/backend/Classes/Controller/BackendController.php b/typo3/sysext/backend/Classes/Controller/BackendController.php index 0e9a29c94baf..f6d4e434708f 100644 --- a/typo3/sysext/backend/Classes/Controller/BackendController.php +++ b/typo3/sysext/backend/Classes/Controller/BackendController.php @@ -44,6 +44,7 @@ use TYPO3\CMS\Core\Messaging\FlashMessageService; use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction; use TYPO3\CMS\Core\Page\PageRenderer; +use TYPO3\CMS\Core\Routing\BackendEntryPointResolver; use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; use TYPO3\CMS\Core\Type\File\ImageInfo; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -75,6 +76,7 @@ public function __construct( protected readonly BackendViewFactory $viewFactory, protected readonly EventDispatcherInterface $eventDispatcher, protected readonly FlashMessageService $flashMessageService, + protected readonly BackendEntryPointResolver $backendEntryPointResolver, ) { $this->modules = $this->moduleProvider->getModulesForModuleMenu($this->getBackendUser()); } @@ -157,6 +159,7 @@ public function mainAction(ServerRequestInterface $request): ResponseInterface 'modulesCollapsed' => $this->getCollapseStateOfMenu(), 'modulesInformation' => GeneralUtility::jsonEncodeForHtmlAttribute($this->getModulesInformation(), false), 'startupModule' => $this->getStartupModule($request), + 'entryPoint' => $this->backendEntryPointResolver->getPathFromRequest($request), 'stateTracker' => (string)$this->uriBuilder->buildUriFromRoute('state-tracker'), 'sitename' => $title, 'sitenameFirstInBackendTitle' => ($backendUser->uc['backendTitleFormat'] ?? '') === 'sitenameFirst', diff --git a/typo3/sysext/backend/Classes/Http/Application.php b/typo3/sysext/backend/Classes/Http/Application.php index c9a20f2d9b24..3bcc28fd968f 100644 --- a/typo3/sysext/backend/Classes/Http/Application.php +++ b/typo3/sysext/backend/Classes/Http/Application.php @@ -20,15 +20,11 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -use TYPO3\CMS\Core\Configuration\ConfigurationManager; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\DateTimeAspect; use TYPO3\CMS\Core\Context\VisibilityAspect; -use TYPO3\CMS\Core\Core\Bootstrap; use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; use TYPO3\CMS\Core\Http\AbstractApplication; -use TYPO3\CMS\Core\Http\RedirectResponse; -use TYPO3\CMS\Core\Routing\BackendEntryPointResolver; /** * Entry point for the TYPO3 Backend (HTTP requests) @@ -37,35 +33,24 @@ class Application extends AbstractApplication { public function __construct( RequestHandlerInterface $requestHandler, - protected readonly ConfigurationManager $configurationManager, protected readonly Context $context, - protected readonly BackendEntryPointResolver $backendEntryPointResolver ) { $this->requestHandler = $requestHandler; } public function handle(ServerRequestInterface $request): ResponseInterface { - if (!Bootstrap::checkIfEssentialConfigurationExists($this->configurationManager)) { - return $this->installToolRedirect($request); - } - - // Add applicationType attribute to request: This is backend and maybe backend ajax. $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + if (isset($_SERVER['TYPO3_DEPRECATED_ENTRYPOINT'])) { + trigger_error('/typo3/index.php entrypoint is deprecated and will be removed in TYPO3 v14. Adapt your webserver config to route all requests via /index.php', E_USER_DEPRECATED); + } + // Set up the initial context $this->initializeContext(); return parent::handle($request); } - /** - * Create a PSR-7 Response that redirects to the install tool - */ - protected function installToolRedirect(ServerRequestInterface $request): ResponseInterface - { - return new RedirectResponse($this->backendEntryPointResolver->getPathFromRequest($request) . 'install.php', 302); - } - /** * Initializes the Context used for accessing data and finding out the current state of the application */ diff --git a/typo3/sysext/backend/Classes/Routing/Router.php b/typo3/sysext/backend/Classes/Routing/Router.php index 5b49354abef1..7b600add3e0a 100644 --- a/typo3/sysext/backend/Classes/Routing/Router.php +++ b/typo3/sysext/backend/Classes/Routing/Router.php @@ -20,6 +20,7 @@ use Symfony\Component\Routing\Route as SymfonyRoute; use TYPO3\CMS\Backend\Routing\Exception\MethodNotAllowedException; use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException; +use TYPO3\CMS\Core\Routing\BackendEntryPointResolver; use TYPO3\CMS\Core\Routing\RequestContextFactory; use TYPO3\CMS\Core\Routing\RouteCollection; use TYPO3\CMS\Core\SingletonInterface; @@ -43,7 +44,8 @@ class Router implements SingletonInterface protected RouteCollection $routeCollection; public function __construct( - protected readonly RequestContextFactory $requestContextFactory + protected readonly RequestContextFactory $requestContextFactory, + protected readonly BackendEntryPointResolver $backendEntryPointResolver, ) { $this->routeCollection = new RouteCollection(); } @@ -127,11 +129,11 @@ public function match($pathInfo): Route */ public function matchResult(ServerRequestInterface $request): RouteResult { - $path = $request->getUri()->getPath(); - if (($normalizedParams = $request->getAttribute('normalizedParams')) !== null) { - // Remove the directory name of the script from the path. This will usually be `/typo3` in this context. - $path = substr($path, strlen(dirname($normalizedParams->getScriptName()))); + $path = $this->backendEntryPointResolver->getBackendRoutePath($request); + if ($path === null) { + throw new ResourceNotFoundException('The requested resource "' . $request->getUri()->getPath() . '" does not contain a known backend route prefix.', 1704787661); } + if ($path === '' || $path === '/' || $path === '/index.php') { // Allow the login page to be displayed if routing is not used and on index.php // (consolidate RouteDispatcher::evaluateReferrer() when changing 'login' to something different) diff --git a/typo3/sysext/backend/Classes/ServiceProvider.php b/typo3/sysext/backend/Classes/ServiceProvider.php index f87c5320afc6..11de5b32a271 100644 --- a/typo3/sysext/backend/Classes/ServiceProvider.php +++ b/typo3/sysext/backend/Classes/ServiceProvider.php @@ -32,7 +32,6 @@ use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessStorage; use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent; use TYPO3\CMS\Core\Cache\Exception\InvalidDataException; -use TYPO3\CMS\Core\Configuration\ConfigurationManager; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\EventDispatcher\ListenerProvider; use TYPO3\CMS\Core\Exception as CoreException; @@ -95,9 +94,7 @@ public static function getApplication(ContainerInterface $container): Applicatio ); return new Application( $requestHandler, - $container->get(ConfigurationManager::class), $container->get(Context::class), - $container->get(BackendEntryPointResolver::class) ); } @@ -174,7 +171,10 @@ public static function getBackendMiddlewares(ContainerInterface $container): \Ar public static function configureBackendRouter(ContainerInterface $container, Router $router = null): Router { - $router = $router ?? self::new($container, Router::class, [$container->get(RequestContextFactory::class)]); + $router = $router ?? self::new($container, Router::class, [ + $container->get(RequestContextFactory::class), + $container->get(BackendEntryPointResolver::class), + ]); $cache = $container->get('cache.core'); $cacheIdentifier = $container->get(PackageDependentCacheIdentifier::class)->withPrefix('BackendRoutes')->toString(); diff --git a/typo3/sysext/backend/Configuration/RequestMiddlewares.php b/typo3/sysext/backend/Configuration/RequestMiddlewares.php index cd8daa7f35d2..c9ea5b3cb180 100644 --- a/typo3/sysext/backend/Configuration/RequestMiddlewares.php +++ b/typo3/sysext/backend/Configuration/RequestMiddlewares.php @@ -13,16 +13,9 @@ */ return [ 'backend' => [ - /** internal: do not use or reference this middleware in your own code */ - 'typo3/cms-core/verify-host-header' => [ - 'target' => \TYPO3\CMS\Core\Middleware\VerifyHostHeader::class, - ], /** internal: do not use or reference this middleware in your own code */ 'typo3/cms-core/normalized-params-attribute' => [ 'target' => \TYPO3\CMS\Core\Middleware\NormalizedParamsAttribute::class, - 'after' => [ - 'typo3/cms-core/verify-host-header', - ], ], 'typo3/cms-backend/locked-backend' => [ 'target' => \TYPO3\CMS\Backend\Middleware\LockedBackendGuard::class, diff --git a/typo3/sysext/backend/Resources/Private/Php/backend.php b/typo3/sysext/backend/Resources/Private/Php/backend.php deleted file mode 100644 index 2a56a3d0727d..000000000000 --- a/typo3/sysext/backend/Resources/Private/Php/backend.php +++ /dev/null @@ -1,21 +0,0 @@ -get(\TYPO3\CMS\Backend\Http\Application::class)->run(); -}); diff --git a/typo3/sysext/backend/Resources/Private/Php/legacy-backend.php b/typo3/sysext/backend/Resources/Private/Php/legacy-backend.php new file mode 100644 index 000000000000..2cbf62b7b42c --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Php/legacy-backend.php @@ -0,0 +1,48 @@ +
- +
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/module/router.js b/typo3/sysext/backend/Resources/Public/JavaScript/module/router.js index 023b9aeddec9..7fa735f9a9a5 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/module/router.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/module/router.js @@ -10,7 +10,7 @@ * * The TYPO3 project - inspiring people to share! */ -var __decorate=function(t,e,o,r){var i,l=arguments.length,n=l<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,o,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(l<3?i(n):l>3?i(e,o,n):i(e,o))||n);return l>3&&n&&Object.defineProperty(e,o,n),n};import{html,css,LitElement}from"lit";import{customElement,property,query}from"lit/decorators.js";import{ModuleUtility}from"@typo3/backend/module.js";const IFRAME_COMPONENT="@typo3/backend/module/iframe",alwaysUpdate=()=>!0;let ModuleRouter=class extends LitElement{constructor(){super(),this.module="",this.endpoint="",this.addEventListener("typo3-module-load",(({target:t,detail:e})=>{const o=t.getAttribute("slot");this.pushState({slotName:o,detail:e})})),this.addEventListener("typo3-module-loaded",(({detail:t})=>{this.updateBrowserState(t)})),this.addEventListener("typo3-iframe-load",(({detail:t})=>{let e={slotName:IFRAME_COMPONENT,detail:t};if(e.detail.url.includes(this.stateTrackerUrl+"?state=")){const t=e.detail.url.split("?state=");e=JSON.parse(decodeURIComponent(t[1]||"{}"))}this.slotElement.getAttribute("name")!==e.slotName&&this.slotElement.setAttribute("name",e.slotName),this.markActive(e.slotName,this.slotElement.getAttribute("name")===IFRAME_COMPONENT?null:e.detail.url,!1),this.updateBrowserState(e.detail),this.parentElement.dispatchEvent(new CustomEvent("typo3-module-load",{bubbles:!0,composed:!0,detail:e.detail}))})),this.addEventListener("typo3-iframe-loaded",(({detail:t})=>{this.updateBrowserState(t),this.parentElement.dispatchEvent(new CustomEvent("typo3-module-loaded",{bubbles:!0,composed:!0,detail:t}))}))}render(){const t=ModuleUtility.getFromName(this.module).component||IFRAME_COMPONENT;return html``}updated(){const t=ModuleUtility.getFromName(this.module).component||IFRAME_COMPONENT;this.markActive(t,this.endpoint)}async markActive(t,e,o=!0){const r=await this.getModuleElement(t);e&&(o||r.getAttribute("endpoint")!==e)&&r.setAttribute("endpoint",e),r.hasAttribute("active")||r.setAttribute("active","");for(let t=r.previousElementSibling;null!==t;t=t.previousElementSibling)t.removeAttribute("active");for(let t=r.nextElementSibling;null!==t;t=t.nextElementSibling)t.removeAttribute("active")}async getModuleElement(t){let e=this.querySelector(`*[slot="${t}"]`);if(null!==e)return e;try{const o=await import(t+".js");if(e=this.querySelector(`*[slot="${t}"]`),null!==e)return e;if(!("componentName"in o))throw new Error(`module ${t} is missing the "componentName" export`);e=document.createElement(o.componentName)}catch(e){throw console.error({msg:`Error importing ${t} as backend module`,err:e}),e}return e.setAttribute("slot",t),this.appendChild(e),e}async pushState(t){const e=this.stateTrackerUrl+"?state="+encodeURIComponent(JSON.stringify(t));(await this.getModuleElement(IFRAME_COMPONENT)).setAttribute("endpoint",e)}updateBrowserState(t){const e=new URL(t.url||"",window.location.origin),o=new URLSearchParams(e.search),r="title"in t?t.title:"";if(null!==r){const t=[this.sitename];""!==r&&t.unshift(r),this.sitenameFirst&&t.reverse(),document.title=t.join(" · ")}if(!o.has("token")){if(!o.has("install[controller]"))return;{const t=o.get("install[controller]");o.delete("install[controller]"),o.delete("install[context]"),e.pathname=e.pathname.replace("/typo3/install.php","/typo3/module/tools/"+t)}}o.delete("token"),e.search=o.toString();const i=e.toString();window.history.replaceState(t,"",i)}};ModuleRouter.styles=css` +var __decorate=function(t,e,o,r){var i,l=arguments.length,n=l<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,o,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(l<3?i(n):l>3?i(e,o,n):i(e,o))||n);return l>3&&n&&Object.defineProperty(e,o,n),n};import{html,css,LitElement}from"lit";import{customElement,property,query}from"lit/decorators.js";import{ModuleUtility}from"@typo3/backend/module.js";const IFRAME_COMPONENT="@typo3/backend/module/iframe",alwaysUpdate=()=>!0;let ModuleRouter=class extends LitElement{constructor(){super(),this.module="",this.endpoint="",this.addEventListener("typo3-module-load",(({target:t,detail:e})=>{const o=t.getAttribute("slot");this.pushState({slotName:o,detail:e})})),this.addEventListener("typo3-module-loaded",(({detail:t})=>{this.updateBrowserState(t)})),this.addEventListener("typo3-iframe-load",(({detail:t})=>{let e={slotName:IFRAME_COMPONENT,detail:t};if(e.detail.url.includes(this.stateTrackerUrl+"?state=")){const t=e.detail.url.split("?state=");e=JSON.parse(decodeURIComponent(t[1]||"{}"))}this.slotElement.getAttribute("name")!==e.slotName&&this.slotElement.setAttribute("name",e.slotName),this.markActive(e.slotName,this.slotElement.getAttribute("name")===IFRAME_COMPONENT?null:e.detail.url,!1),this.updateBrowserState(e.detail),this.parentElement.dispatchEvent(new CustomEvent("typo3-module-load",{bubbles:!0,composed:!0,detail:e.detail}))})),this.addEventListener("typo3-iframe-loaded",(({detail:t})=>{this.updateBrowserState(t),this.parentElement.dispatchEvent(new CustomEvent("typo3-module-loaded",{bubbles:!0,composed:!0,detail:t}))}))}render(){const t=ModuleUtility.getFromName(this.module).component||IFRAME_COMPONENT;return html``}updated(){const t=ModuleUtility.getFromName(this.module).component||IFRAME_COMPONENT;this.markActive(t,this.endpoint)}async markActive(t,e,o=!0){const r=await this.getModuleElement(t);e&&(o||r.getAttribute("endpoint")!==e)&&r.setAttribute("endpoint",e),r.hasAttribute("active")||r.setAttribute("active","");for(let t=r.previousElementSibling;null!==t;t=t.previousElementSibling)t.removeAttribute("active");for(let t=r.nextElementSibling;null!==t;t=t.nextElementSibling)t.removeAttribute("active")}async getModuleElement(t){let e=this.querySelector(`*[slot="${t}"]`);if(null!==e)return e;try{const o=await import(t+".js");if(e=this.querySelector(`*[slot="${t}"]`),null!==e)return e;if(!("componentName"in o))throw new Error(`module ${t} is missing the "componentName" export`);e=document.createElement(o.componentName)}catch(e){throw console.error({msg:`Error importing ${t} as backend module`,err:e}),e}return e.setAttribute("slot",t),this.appendChild(e),e}async pushState(t){const e=this.stateTrackerUrl+"?state="+encodeURIComponent(JSON.stringify(t));(await this.getModuleElement(IFRAME_COMPONENT)).setAttribute("endpoint",e)}updateBrowserState(t){const e=new URL(t.url||"",window.location.origin),o=new URLSearchParams(e.search),r="title"in t?t.title:"";if(null!==r){const t=[this.sitename];""!==r&&t.unshift(r),this.sitenameFirst&&t.reverse(),document.title=t.join(" · ")}if(!o.has("token")){if(!o.has("install[controller]"))return;{const t=o.get("install[controller]");o.delete("install[controller]"),o.delete("install[context]"),e.pathname=e.pathname.replace("/typo3/install.php",this.entryPoint+"module/tools/"+t)}}o.delete("token"),e.search=o.toString();const i=e.toString();window.history.replaceState(t,"",i)}};ModuleRouter.styles=css` :host { width: 100%; min-height: 100%; @@ -22,4 +22,4 @@ var __decorate=function(t,e,o,r){var i,l=arguments.length,n=l<3?e:null===r?r=Obj min-height: 100%; width: 100%; } - `,__decorate([property({type:String,hasChanged:alwaysUpdate})],ModuleRouter.prototype,"module",void 0),__decorate([property({type:String,hasChanged:alwaysUpdate})],ModuleRouter.prototype,"endpoint",void 0),__decorate([property({type:String,attribute:"state-tracker"})],ModuleRouter.prototype,"stateTrackerUrl",void 0),__decorate([property({type:String,attribute:"sitename"})],ModuleRouter.prototype,"sitename",void 0),__decorate([property({type:Boolean,attribute:"sitename-first"})],ModuleRouter.prototype,"sitenameFirst",void 0),__decorate([query("slot",!0)],ModuleRouter.prototype,"slotElement",void 0),ModuleRouter=__decorate([customElement("typo3-backend-module-router")],ModuleRouter);export{ModuleRouter}; \ No newline at end of file + `,__decorate([property({type:String,hasChanged:alwaysUpdate})],ModuleRouter.prototype,"module",void 0),__decorate([property({type:String,hasChanged:alwaysUpdate})],ModuleRouter.prototype,"endpoint",void 0),__decorate([property({type:String,attribute:"state-tracker"})],ModuleRouter.prototype,"stateTrackerUrl",void 0),__decorate([property({type:String,attribute:"sitename"})],ModuleRouter.prototype,"sitename",void 0),__decorate([property({type:Boolean,attribute:"sitename-first"})],ModuleRouter.prototype,"sitenameFirst",void 0),__decorate([property({type:String,attribute:"entry-point"})],ModuleRouter.prototype,"entryPoint",void 0),__decorate([query("slot",!0)],ModuleRouter.prototype,"slotElement",void 0),ModuleRouter=__decorate([customElement("typo3-backend-module-router")],ModuleRouter);export{ModuleRouter}; \ No newline at end of file diff --git a/typo3/sysext/backend/Tests/Functional/Routing/RouterTest.php b/typo3/sysext/backend/Tests/Functional/Routing/RouterTest.php index 189b562f646e..53f21064d25d 100644 --- a/typo3/sysext/backend/Tests/Functional/Routing/RouterTest.php +++ b/typo3/sysext/backend/Tests/Functional/Routing/RouterTest.php @@ -52,7 +52,8 @@ public function routerReturnsRouteForAlias(): void public function matchResultFindsProperRoute(): void { $subject = $this->get(Router::class); - $request = new ServerRequest('https://example.com/login', 'GET'); + $serverParams = array_replace($_SERVER, ['HTTP_HOST' => 'example.com', 'HTTPS' => 'on', 'SCRIPT_NAME' => '/index.php']); + $request = new ServerRequest('https://example.com/typo3/login', 'GET', null, [], $serverParams); $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); $result = $subject->matchResult($request); self::assertEquals('/login', $result->getRoute()->getPath()); @@ -64,7 +65,8 @@ public function matchResultFindsProperRoute(): void public function matchResultThrowsExceptionOnInvalidRoute(): void { $subject = $this->get(Router::class); - $request = new ServerRequest('https://example.com/this-path/does-not-exist', 'GET'); + $serverParams = array_replace($_SERVER, ['HTTP_HOST' => 'example.com', 'HTTPS' => 'on', 'SCRIPT_NAME' => '/index.php']); + $request = new ServerRequest('https://example.com/typo3/this-path/does-not-exist', 'GET', null, [], $serverParams); $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); $this->expectException(ResourceNotFoundException::class); $subject->matchResult($request); @@ -76,7 +78,8 @@ public function matchResultThrowsExceptionOnInvalidRoute(): void public function matchResultThrowsInvalidMethodForValidRoute(): void { $subject = $this->get(Router::class); - $request = new ServerRequest('https://example.com/login/password-reset/initiate-reset', 'GET'); + $serverParams = array_replace($_SERVER, ['HTTP_HOST' => 'example.com', 'HTTPS' => 'on', 'SCRIPT_NAME' => '/index.php']); + $request = new ServerRequest('https://example.com/typo3/login/password-reset/initiate-reset', 'GET', null, [], $serverParams); $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); $this->expectException(MethodNotAllowedException::class); $subject->matchResult($request); @@ -88,7 +91,8 @@ public function matchResultThrowsInvalidMethodForValidRoute(): void public function matchResultReturnsRouteWithMethodLimitation(): void { $subject = $this->get(Router::class); - $request = new ServerRequest('https://example.com/login/password-reset/initiate-reset', 'POST'); + $serverParams = array_replace($_SERVER, ['HTTP_HOST' => 'example.com', 'HTTPS' => 'on', 'SCRIPT_NAME' => '/index.php']); + $request = new ServerRequest('https://example.com/typo3/login/password-reset/initiate-reset', 'POST', null, [], $serverParams); $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); $result = $subject->matchResult($request); self::assertEquals('/login/password-reset/initiate-reset', $result->getRoute()->getPath()); @@ -100,7 +104,8 @@ public function matchResultReturnsRouteWithMethodLimitation(): void public function matchResultReturnsRouteForBackendModuleWithMethodLimitation(): void { $subject = $this->get(Router::class); - $request = new ServerRequest('https://example.com/module/site/configuration/delete', 'POST'); + $serverParams = array_replace($_SERVER, ['HTTP_HOST' => 'example.com', 'HTTPS' => 'on', 'SCRIPT_NAME' => '/index.php']); + $request = new ServerRequest('https://example.com/typo3/module/site/configuration/delete', 'POST', null, [], $serverParams); $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); $result = $subject->matchResult($request); self::assertEquals('/module/site/configuration/delete', $result->getRoute()->getPath()); @@ -116,7 +121,8 @@ public function matchResultThrowsExceptionForWrongHttpMethod(): void $this->expectExceptionCode(1612649842); $subject = $this->get(Router::class); - $request = new ServerRequest('https://example.com/module/site/configuration/delete', 'GET'); + $serverParams = array_replace($_SERVER, ['HTTP_HOST' => 'example.com', 'HTTPS' => 'on', 'SCRIPT_NAME' => '/index.php']); + $request = new ServerRequest('https://example.com/typo3/module/site/configuration/delete', 'GET', null, [], $serverParams); $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); $subject->matchResult($request); } @@ -128,7 +134,8 @@ public function matchResultReturnsRouteWithPlaceholderAndMethodLimitation(): voi { $subject = $this->get(Router::class); $subject->addRoute('custom-route', (new Route('/my-path/{identifier}', []))->setMethods(['POST'])); - $request = new ServerRequest('https://example.com/my-path/my-identifier', 'POST'); + $serverParams = array_replace($_SERVER, ['HTTP_HOST' => 'example.com', 'HTTPS' => 'on', 'SCRIPT_NAME' => '/index.php']); + $request = new ServerRequest('https://example.com/typo3/my-path/my-identifier', 'POST', null, [], $serverParams); $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); $result = $subject->matchResult($request); self::assertEquals('custom-route', $result->getRouteName()); @@ -148,11 +155,17 @@ public function matchResultReturnsRouteForSubRoute(): void $routeCollection->addPrefix('/module/main/module'); $subject->addRouteCollection($routeCollection); - $resultMainModule = $subject->matchResult(new ServerRequest('/module/main/module')); + $serverParams = array_replace($_SERVER, ['HTTP_HOST' => 'example.com', 'HTTPS' => 'on', 'SCRIPT_NAME' => '/index.php']); + + $request = new ServerRequest('/typo3/module/main/module', 'GET', null, [], $serverParams); + $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); + $resultMainModule = $subject->matchResult($request); self::assertEquals('main_module', $resultMainModule->getRouteName()); self::assertEquals('/module/main/module', $resultMainModule->getRoute()->getPath()); - $resultSubRoute = $subject->matchResult(new ServerRequest('/module/main/module/subroute')); + $request = new ServerRequest('/typo3/module/main/module/subroute', 'GET', null, [], $serverParams); + $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); + $resultSubRoute = $subject->matchResult($request); self::assertEquals('main_module.subroute', $resultSubRoute->getRouteName()); self::assertEquals('/module/main/module/subroute', $resultSubRoute->getRoute()->getPath()); } diff --git a/typo3/sysext/backend/Tests/Functional/Template/Components/Buttons/Action/ShortcutButtonTest.php b/typo3/sysext/backend/Tests/Functional/Template/Components/Buttons/Action/ShortcutButtonTest.php index 3aff9cf64822..2e7b24144679 100644 --- a/typo3/sysext/backend/Tests/Functional/Template/Components/Buttons/Action/ShortcutButtonTest.php +++ b/typo3/sysext/backend/Tests/Functional/Template/Components/Buttons/Action/ShortcutButtonTest.php @@ -61,7 +61,7 @@ public function rendersCorrectMarkup(ShortcutButton $button, string $expectedMar $this->importCSVDataSet(__DIR__ . '/../../../../Fixtures/be_users.csv'); $backendUser = $this->setUpBackendUser(1); $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); - $serverParams = array_replace($_SERVER, ['HTTP_HOST' => 'example.com', 'SCRIPT_NAME' => '/typo3/index.php']); + $serverParams = array_replace($_SERVER, ['HTTP_HOST' => 'example.com', 'SCRIPT_NAME' => '/index.php']); $request = new ServerRequest('http://example.com/typo3/index.php', 'GET', null, $serverParams); $GLOBALS['TYPO3_REQUEST'] = $request ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE) diff --git a/typo3/sysext/backend/Tests/Unit/Routing/UriBuilderTest.php b/typo3/sysext/backend/Tests/Unit/Routing/UriBuilderTest.php index b459f926a618..d82a5ccbca83 100644 --- a/typo3/sysext/backend/Tests/Unit/Routing/UriBuilderTest.php +++ b/typo3/sysext/backend/Tests/Unit/Routing/UriBuilderTest.php @@ -82,8 +82,9 @@ public function validRoutesAreBuilt( array $routeParameters, string $expectation ): void { - $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $router = new Router($requestContextFactory); + $backendEntryPointResolver = new BackendEntryPointResolver(); + $requestContextFactory = new RequestContextFactory($backendEntryPointResolver); + $router = new Router($requestContextFactory, $backendEntryPointResolver); foreach ($routes as $nameRoute => $route) { $router->addRoute($nameRoute, $route); } @@ -108,8 +109,9 @@ public function nonExistingRouteThrowsException(): void $this->expectException(RouteNotFoundException::class); $this->expectExceptionCode(1476050190); - $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $subject = new UriBuilder(new Router($requestContextFactory), $formProtectionFactory, $requestContextFactory); + $backendEntryPointResolver = new BackendEntryPointResolver(); + $requestContextFactory = new RequestContextFactory($backendEntryPointResolver); + $subject = new UriBuilder(new Router($requestContextFactory, $backendEntryPointResolver), $formProtectionFactory, $requestContextFactory); $subject->buildUriFromRoute(StringUtility::getUniqueId('any')); } } diff --git a/typo3/sysext/core/Classes/Composer/InstallerScripts.php b/typo3/sysext/core/Classes/Composer/InstallerScripts.php index 0cd7495a5152..8a3fbedf41e7 100644 --- a/typo3/sysext/core/Classes/Composer/InstallerScripts.php +++ b/typo3/sysext/core/Classes/Composer/InstallerScripts.php @@ -18,6 +18,7 @@ namespace TYPO3\CMS\Core\Composer; use Composer\Script\Event; +use TYPO3\CMS\Composer\Plugin\Core\InstallerScripts\EntryPoint; use TYPO3\CMS\Composer\Plugin\Core\InstallerScriptsRegistration; use TYPO3\CMS\Composer\Plugin\Core\ScriptDispatcher; @@ -29,13 +30,19 @@ class InstallerScripts implements InstallerScriptsRegistration { public static function register(Event $event, ScriptDispatcher $scriptDispatcher) { + $scriptDispatcher->addInstallerScript( + new EntryPoint( + dirname(__DIR__, 2) . '/Resources/Private/Php/index.php', + 'index.php' + ) + ); if ($event->getComposer()->getPackage()->getName() === 'typo3/cms') { - // We don't need the binary in Composer mode (as we have typo3/cms-cli providing it) + // We only need to provide the binary in monorepo classic mode (regular Composer mode receives it via typo3/cms-cli) $source = dirname(__DIR__, 2) . '/Resources/Private/Php/cli.php'; $target = 'typo3/sysext/core/bin/typo3'; $scriptDispatcher->addInstallerScript(new CliEntryPoint($source, $target)); } else { - // We don't need package artifact creation for our dev package/ TYPO3 classic mode + // Provide package artifact in regular composer mode (not needed for monorepo classic mode) $scriptDispatcher->addInstallerScript(new PackageArtifactBuilder()); } } diff --git a/typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php b/typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php index 3aadfabd040d..7a9b2c916bf0 100644 --- a/typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php +++ b/typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php @@ -57,7 +57,7 @@ class SystemEnvironmentBuilder * @internal This method should not be used by 3rd party code. It will change without further notice. * @param int $entryPointLevel Number of subdirectories where the entry script is located under the document root */ - public static function run(int $entryPointLevel = 0, int $requestType = self::REQUESTTYPE_FE) + public static function run(int $entryPointLevel = 0, int $requestType = 0) { self::defineBaseConstants(); $scriptPath = self::calculateScriptPath($entryPointLevel, $requestType); @@ -100,8 +100,8 @@ protected static function defineBaseConstants() /** * Calculate script path. This is the absolute path to the entry script. - * Can be something like '.../public/index.php' or '.../public/typo3/index.php' for - * web calls, or '.../bin/typo3' or similar for cli calls. + * Can be something like '.../public/index.php' for web calls, or + * '.../bin/typo3' or similar for cli calls. * * @param int $entryPointLevel Number of subdirectories where the entry script is located under the document root * @return string Absolute path to entry script @@ -308,8 +308,7 @@ protected static function getPathThisScriptCli() * - Directly called documentRoot/index.php (-> FE call or eiD include): index.php is located in the same directory * as the main project. The document root is identical to the directory the script is located at. * - The install tool, located under typo3/install.php. - * - A Backend script: This is the case for the typo3/index.php dispatcher and other entry scripts like 'typo3/sysext/core/bin/typo3' - * or 'typo3/index.php' that are located inside typo3/ directly. + * - The CLI script 'typo3/sysext/core/bin/typo3' which is located inside typo3/ directly. * * @param string $scriptPath Calculated path to the entry script * @param int $entryPointLevel Number of subdirectories where the entry script is located under the document root @@ -338,7 +337,7 @@ protected static function usesComposerClassLoading(): bool protected static function isCliRequestType(?int $requestType): bool { if ($requestType === null) { - $requestType = PHP_SAPI === 'cli' ? self::REQUESTTYPE_CLI : self::REQUESTTYPE_FE; + return PHP_SAPI === 'cli'; } return ($requestType & self::REQUESTTYPE_CLI) === self::REQUESTTYPE_CLI; diff --git a/typo3/sysext/core/Classes/Http/Application.php b/typo3/sysext/core/Classes/Http/Application.php new file mode 100644 index 000000000000..38b16c4c9187 --- /dev/null +++ b/typo3/sysext/core/Classes/Http/Application.php @@ -0,0 +1,59 @@ +requestHandler = $requestHandler; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + if (!Bootstrap::checkIfEssentialConfigurationExists($this->configurationManager)) { + return $this->installToolRedirect($request); + } + + return parent::handle($request); + } + + protected function installToolRedirect(ServerRequestInterface $request): ResponseInterface + { + // /typo3/install.php is currently physically and statically installed to typo3/install.php + // so we must not use BackendEntryPointResolver which is targeted towards virtual backend paths. + // @todo: Move /typo3/install.php to /install.php? + return new RedirectResponse($this->getNormalizedParams($request)->getSitePath() . 'typo3/install.php', 302); + } + + protected function getNormalizedParams(ServerRequestInterface $request): NormalizedParams + { + return NormalizedParams::createFromRequest($request); + } +} diff --git a/typo3/sysext/core/Classes/Http/RequestHandler.php b/typo3/sysext/core/Classes/Http/RequestHandler.php new file mode 100644 index 000000000000..eaca2b3f6164 --- /dev/null +++ b/typo3/sysext/core/Classes/Http/RequestHandler.php @@ -0,0 +1,47 @@ +container->has(BackendApplication::class) && $this->backendEntryPointResolver->isBackendRoute($request)) { + return $this->container->get(BackendApplication::class)->handle($request); + } + + if ($this->container->has(FrontendApplication::class)) { + return $this->container->get(FrontendApplication::class)->handle($request); + } + + throw new \Exception('TYPO3 is not configured. Please install typo3/cms-backend and/or typo3/cms-frontend', 1704788092); + } +} diff --git a/typo3/sysext/core/Classes/Middleware/NormalizedParamsAttribute.php b/typo3/sysext/core/Classes/Middleware/NormalizedParamsAttribute.php index c8619074e68d..17f04fa4e4e1 100644 --- a/typo3/sysext/core/Classes/Middleware/NormalizedParamsAttribute.php +++ b/typo3/sysext/core/Classes/Middleware/NormalizedParamsAttribute.php @@ -37,7 +37,10 @@ class NormalizedParamsAttribute implements MiddlewareInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); + $normalizedParams = $request->getAttribute('normalizedParams', null); + if ($normalizedParams === null) { + $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); + } return $handler->handle($request); } } diff --git a/typo3/sysext/core/Classes/Routing/BackendEntryPointResolver.php b/typo3/sysext/core/Classes/Routing/BackendEntryPointResolver.php index 6166269683c4..c658dc9335a4 100644 --- a/typo3/sysext/core/Classes/Routing/BackendEntryPointResolver.php +++ b/typo3/sysext/core/Classes/Routing/BackendEntryPointResolver.php @@ -23,35 +23,121 @@ use TYPO3\CMS\Core\Http\Uri; /** - * This class helps to resolve all kinds of paths to "/typo3/" - the main entry point to the TYPO3 Backend. + * This class helps to resolve the virtual path to the main entry point of the TYPO3 Backend. */ class BackendEntryPointResolver { - protected string $path = 'typo3/'; + protected string $entryPoint = '/typo3'; /** - * Returns a prefix such as /typo3/ or /mysubdir/typo3/ to the TYPO3 Backend. + * Returns a prefix such as /typo3/ or /mysubdir/typo3/ to the TYPO3 Backend with trailing slash. */ public function getPathFromRequest(ServerRequestInterface $request): string { + $entryPoint = $this->getEntryPoint($request); + if (str_contains($entryPoint, '//')) { + $entryPointParts = parse_url($entryPoint); + /* Remove trailing slash unless, the string is '/' itself */ + $entryPoint = rtrim('/' . trim($entryPointParts['path'] ?? '', '/'), '/'); + } + return $entryPoint . '/'; + } + + /** + * Returns a full URL to the main URL of the TYPO3 Backend. + */ + public function getUriFromRequest(ServerRequestInterface $request, string $additionalPathPart = ''): UriInterface + { + if (str_starts_with(ltrim($additionalPathPart, '/'), 'install.php')) { + // install entrypoint not configurable yet, therefore it's essential to return the static `typo3/install.php` + // uri - otherwise wrong redirect would happen and leading to nested backend for admin area displayed. + if ($request->getAttribute('normalizedParams') instanceof NormalizedParams) { + $normalizedParams = $request->getAttribute('normalizedParams'); + } else { + $normalizedParams = NormalizedParams::createFromRequest($request); + } + return new Uri($normalizedParams->getSiteUrl() . 'typo3/install.php'); + } + $entryPoint = $this->getEntryPointConfiguration(); + if (str_starts_with($entryPoint, 'https://') || str_starts_with($entryPoint, 'http://')) { + // fqdn, early return as all required information are available. + return new Uri($entryPoint . '/' . ltrim($additionalPathPart, '/')); + } if ($request->getAttribute('normalizedParams') instanceof NormalizedParams) { $normalizedParams = $request->getAttribute('normalizedParams'); } else { $normalizedParams = NormalizedParams::createFromRequest($request); } - return $normalizedParams->getSitePath() . $this->path; + if (str_starts_with($entryPoint, '//')) { + // Browser supports uri starting with `//` and uses the current request scheme for the link. Do avoid issue + // for example checking the url at some point we prefix it with the current request protocol. + return new Uri(($normalizedParams->isHttps() ? 'https:' : 'http:') . $entryPoint . '/' . ltrim($additionalPathPart, '/')); + } + return new Uri($normalizedParams->getSiteUrl() . $entryPoint . '/' . ltrim($additionalPathPart, '/')); + } + + public function isBackendRoute(ServerRequestInterface $request): bool + { + return $this->getBackendRoutePath($request) !== null; + } + + public function getBackendRoutePath(ServerRequestInterface $request): ?string + { + $uri = $request->getUri(); + $path = $uri->getPath(); + $entryPoint = $this->getEntryPoint($request); + + if (str_contains($entryPoint, '//')) { + $entryPointParts = parse_url($entryPoint); + if ($uri->getHost() !== $entryPointParts['host']) { + return null; + } + /* Remove trailing slash unless, the string is '/' itself */ + $entryPoint = rtrim('/' . trim($entryPointParts['path'] ?? '', '/'), '/'); + } + + if ($path === $entryPoint) { + return ''; + } + if (str_starts_with($path, $entryPoint . '/')) { + return substr($path, strlen($entryPoint)); + } + return null; } /** - * Returns a full URL to the main URL of the TYPO3 Backend. + * Returns a prefix such as /typo3 or /mysubdir/typo3 to the TYPO3 Backend *without* trailing slash. */ - public function getUriFromRequest(ServerRequestInterface $request, string $additionalPathPart = ''): UriInterface + protected function getEntryPoint(ServerRequestInterface $request): string { + $entryPoint = $this->getEntryPointConfiguration(); + if (str_contains($entryPoint, '//')) { + return $entryPoint; + } if ($request->getAttribute('normalizedParams') instanceof NormalizedParams) { $normalizedParams = $request->getAttribute('normalizedParams'); } else { $normalizedParams = NormalizedParams::createFromRequest($request); } - return new Uri($normalizedParams->getSiteUrl() . $this->path . ltrim($additionalPathPart, '/')); + return $normalizedParams->getSitePath() . $entryPoint; + } + + protected function getEntryPointConfiguration(): string + { + $entryPoint = $GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint'] ?? $this->entryPoint; + if (str_starts_with($entryPoint, 'https://') + || str_starts_with($entryPoint, 'http://') + || str_starts_with($entryPoint, '//') + ) { + $uri = new Uri(rtrim($entryPoint, '/')); + $uri = $uri->withPath($this->removeMultipleSlashes($uri->getPath())); + return (string)$uri; + } + return $this->removeMultipleSlashes(trim($entryPoint, '/')); + } + + private function removeMultipleSlashes(string $value): string + { + return preg_replace('/(\/+)/', '/', $value); } } diff --git a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/PolicyProvider.php b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/PolicyProvider.php index a72a459cb18a..228b7017b244 100644 --- a/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/PolicyProvider.php +++ b/typo3/sysext/core/Classes/Security/ContentSecurityPolicy/PolicyProvider.php @@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Core\RequestId; use TYPO3\CMS\Core\Http\NormalizedParams; use TYPO3\CMS\Core\Http\Uri; +use TYPO3\CMS\Core\Routing\BackendEntryPointResolver; use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyMutatedEvent; use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\Site\Entity\SiteLanguage; @@ -42,7 +43,8 @@ public function __construct( private readonly SiteFinder $siteFinder, private readonly PolicyRegistry $policyRegistry, private readonly EventDispatcherInterface $eventDispatcher, - protected readonly MutationRepository $mutationRepository, + private readonly MutationRepository $mutationRepository, + private readonly BackendEntryPointResolver $backendEntryPointResolver, ) {} /** @@ -102,9 +104,9 @@ public function getDefaultReportingUriBase(Scope $scope, ServerRequestInterface } else { $uri = new Uri($normalizedParams->getSitePath()); } - // add `typo3/` path in backend scope + // add backend entryPoint route prefix in backend scope if ($scope->type->isBackend()) { - $uri = $uri->withPath($uri->getPath() . 'typo3/'); + $uri = $this->backendEntryPointResolver->getUriFromRequest($request); } // prefix current require scheme, host, port in case it's not given if ($absolute && ($uri->getScheme() === '' || $uri->getHost() === '')) { diff --git a/typo3/sysext/core/Classes/ServiceProvider.php b/typo3/sysext/core/Classes/ServiceProvider.php index c8bbabd7d06c..f69217843a78 100644 --- a/typo3/sysext/core/Classes/ServiceProvider.php +++ b/typo3/sysext/core/Classes/ServiceProvider.php @@ -73,6 +73,8 @@ public function getFactories(): array EventDispatcher\EventDispatcher::class => [ static::class, 'getEventDispatcher' ], EventDispatcher\ListenerProvider::class => [ static::class, 'getEventListenerProvider' ], FormProtection\FormProtectionFactory::class => [ static::class, 'getFormProtectionFactory' ], + Http\Application::class => [ static::class, 'getHttpApplication' ], + Http\RequestHandler::class => [ static::class, 'getHttpRequestHandler' ], Http\Client\GuzzleClientFactory::class => [ static::class, 'getGuzzleClientFactory' ], Http\MiddlewareStackResolver::class => [ static::class, 'getMiddlewareStackResolver' ], Http\RequestFactory::class => [ static::class, 'getRequestFactory' ], @@ -107,6 +109,7 @@ public function getFactories(): array TypoScript\Tokenizer\LosslessTokenizer::class => [ static::class, 'getLosslessTokenizer'], 'icons' => [ static::class, 'getIcons' ], 'middlewares' => [ static::class, 'getMiddlewares' ], + 'core.middlewares' => [ static::class, 'getCoreMiddlewares' ], 'content.security.policies' => [ static::class, 'getContentSecurityPolicies' ], ]; } @@ -514,6 +517,26 @@ public static function getBackendEntryPointResolver(ContainerInterface $containe return self::new($container, Routing\BackendEntryPointResolver::class); } + public static function getHttpApplication(ContainerInterface $container): Http\Application + { + $requestHandler = new Http\MiddlewareDispatcher( + $container->get(Http\RequestHandler::class), + $container->get('core.middlewares'), + ); + return new Http\Application( + $requestHandler, + $container->get(Configuration\ConfigurationManager::class), + ); + } + + public static function getHttpRequestHandler(ContainerInterface $container): Http\RequestHandler + { + return new Http\RequestHandler( + $container, + $container->get(Routing\BackendEntryPointResolver::class), + ); + } + public static function getRequestContextFactory(ContainerInterface $container): Routing\RequestContextFactory { return self::new($container, Routing\RequestContextFactory::class, [ @@ -567,6 +590,11 @@ public static function getContentSecurityPolicies(ContainerInterface $container) return new Map(); } + public static function getCoreMiddlewares(ContainerInterface $container): \ArrayObject + { + return new \ArrayObject($container->get(Http\MiddlewareStackResolver::class)->resolve('core')); + } + public static function provideFallbackEventDispatcher( ContainerInterface $container, EventDispatcherInterface $eventDispatcher = null diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php index 7b1d3b8a56d7..b085b5cd34e8 100644 --- a/typo3/sysext/core/Configuration/DefaultConfiguration.php +++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php @@ -1294,6 +1294,7 @@ ], 'BE' => [ // Backend Configuration. + 'entryPoint' => '/typo3', 'fileadminDir' => 'fileadmin/', 'lockRootPath' => '', 'userHomePath' => '', diff --git a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml index ba9bce29e958..207b07467332 100644 --- a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml +++ b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml @@ -219,6 +219,9 @@ BE: type: container description: 'Backend' items: + entryPoint: + type: text + description: 'URL slug for the TYPO3 backend. Defaults to "typo3".' fileadminDir: type: text description: 'Path to the primary directory of files for editors. This is relative to the public web dir, DefaultStorage will be created with that configuration, do not access manually but via \TYPO3\CMS\Core\Resource\ResourceFactory::getDefaultStorage().' diff --git a/typo3/sysext/core/Configuration/RequestMiddlewares.php b/typo3/sysext/core/Configuration/RequestMiddlewares.php new file mode 100644 index 000000000000..e7d00af4129b --- /dev/null +++ b/typo3/sysext/core/Configuration/RequestMiddlewares.php @@ -0,0 +1,33 @@ + [ + * 'middleware-identifier' => [ + * 'target' => classname or callable + * 'before/after' => array of dependencies + * ] + * ] + */ +return [ + 'core' => [ + /** internal: do not use or reference this middleware in your own code */ + 'typo3/cms-core/verify-host-header' => [ + 'target' => \TYPO3\CMS\Core\Middleware\VerifyHostHeader::class, + ], + /** internal: do not use or reference this middleware in your own code */ + 'typo3/cms-core/normalized-params-attribute' => [ + 'target' => \TYPO3\CMS\Core\Middleware\NormalizedParamsAttribute::class, + 'after' => [ + 'typo3/cms-core/verify-host-header', + ], + ], + /** internal: do not use or reference this middleware in your own code */ + 'typo3/cms-core/response-propagation' => [ + 'target' => \TYPO3\CMS\Core\Middleware\ResponsePropagation::class, + 'after' => [ + 'typo3/cms-core/verify-host-header', + ], + ], + ], +]; diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Deprecation-87889-TYPO3BackendEntrypointScriptDeprecated.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Deprecation-87889-TYPO3BackendEntrypointScriptDeprecated.rst new file mode 100644 index 000000000000..7bb2b72c5b6c --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/13.0/Deprecation-87889-TYPO3BackendEntrypointScriptDeprecated.rst @@ -0,0 +1,117 @@ +.. include:: /Includes.rst.txt + +.. _deprecation-87889-1705928143: + +================================================================ +Deprecation: #87889 - TYPO3 Backend entrypoint script deprecated +================================================================ + +See :issue:`87889` + +Description +=========== + +The TYPO3 Backend entrypoint script `/typo3/index.php` is no longer needed and +deprecated in favor of handling all backend and frontend requests with `/index.php`. +It is still in place in case webserver configuration has not been adapted yet. + +Note that the maintenance tool is still available via `/typo3/install.php`. + + +Impact +====== + +The TYPO3 Backend route path is made configurable in order to protected against +application admin interface infrastructure enumeration (WSTG-CONF-05_). +Therefore all requests are handled by the PHP script `/index.php` in order to +allow for variable admin interface URLs. +(via :php:`$GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint']`). + + +Affected installations +====================== + +All installations using the TYPO3 backend `/typo3`. + + +Migration +========= + +There is a silent update in place which automatically updates the +webserver configuration file when accessing the install tool, at +least for Apache and Microsoft IIS webservers. + +Note: This does not work if you are not using the default configuration, +which is shipped with Core and automatically applied during the TYPO3 +installation process, as basis. + +If you however use a custom web server configuration you may adapt as follows: + + +Apache Configuration +~~~~~~~~~~~~~~~~~~~~ + +It is most important to rewrite all `typo3/*` requests to `/index.php`, but also +`RewriteCond %{REQUEST_FILENAME} !-d` should be removed in order for a request +to `/typo3/` to be directly served via `/index.php` instead of the deprecated +entrpoint `/typo3/index.php`. + +Apache Configuration before: + +.. code-block:: none + + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-l + RewriteRule ^typo3/(.*)$ %{ENV:CWD}typo3/index.php [QSA,L] + + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-l + RewriteRule ^.*$ %{ENV:CWD}index.php [QSA,L] + + +Apache Configuration after: + +.. code-block:: none + + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^typo3/(.*)$ %{ENV:CWD}index.php [QSA,L] + + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-l + RewriteRule ^.*$ %{ENV:CWD}index.php [QSA,L] + + +Apache Configuration +~~~~~~~~~~~~~~~~~~~~ + + +Nginx Config before: + +.. code-block:: none + + location /typo3/ { + absolute_redirect off; + try_files $uri /typo3/index.php$is_args$args; + } + +Nginx Config after: + +.. code-block:: none + + location /typo3/ { + absolute_redirect off; + try_files $uri /index.php$is_args$args; + } + + +Related +======= + +- :ref:`changelog-Feature-87889-ConfigurableTYPO3BackendURL` + +.. _WSTG-CONF-05: https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/05-Enumerate_Infrastructure_and_Application_Admin_Interfaces + +.. index:: Backend, NotScanned, ext:backend diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Feature-87889-ConfigurableTYPO3BackendURL.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Feature-87889-ConfigurableTYPO3BackendURL.rst new file mode 100644 index 000000000000..19d62c352025 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/13.0/Feature-87889-ConfigurableTYPO3BackendURL.rst @@ -0,0 +1,92 @@ +.. include:: /Includes.rst.txt + +.. _feature-87889-1705931337: + +================================================ +Feature: #87889 - Configurable TYPO3 Backend URL +================================================ + +See :issue:`87889` + +Description +=========== + + +The TYPO3 Backend URL is made configurable in order to enable optional +protection against application admin interface infrastructure +enumeration (WSTG-CONF-05). Both, frontend and backend requests are +now handled by the PHP script `/index.php` to enable virtual admin +interface URLs. + +The default TYPO3 Backend entrypoint path `/typo3` can be changed by +specifying a custom URL path or domain name in +:php:`$GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint']`. + +This change requires web server adaption. A silent migration and +according documentation for custom web server configurations is added. +A deprecation layer (for non-adapted systems) is in place that rewrites +the server environment variables passed to `/typo3/index.php` as if +`/index.php` was used directly. This layer will be removed in TYPO3 v14. + +This change does not take assets into account, only routing is adapted. +That means composer mode will use assets provided via _assets as before +and TYPO3 classic mode will serve backend assets from /typo3/* even if +another backend URL is used and configured. + + +Configure to a specific path +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + $GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint'] = '/admin'; + +Now point your browser to `https://example.com/admin` to log into the TYPO3 +Backend. + + +Configure to use a distinct (sub)domain +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + $GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint'] = 'https://backend.example.com'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'] = '.example.com'; + + +Now point your browser to `https://backend.example.com/` to log into the TYPO3 +Backend. + +Legacy-Free installation +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The legacy entrypoint `/typo3/index.php` is no longer needed and deprecated in +favor of handling all backend and frontend requests with `/index.php`. The +entrypoint is still in place in case webserver configuration has not been adapted +yet and the maintenance and emergency tool is still available via +`/typo3/install.php` in order to work in edge cases like broken web server +routing. + +In composer mode there is an additional opt-out for the installation of the +legacy entrypoint for that can be defined in composer.json: + +.. code-block:: json + + "extra": { + "typo3/cms": { + "install-deprecated-typo3-index-php": false + } + } + + +Impact +====== + +The TYPO3 Backend route path is made configurable in order to protected against +application admin interface infrastructure enumeration (WSTG-CONF-05_). +Therefore all requests are handled by the PHP script `/index.php` in order to +allow for variable admin interface URLs. + +.. _WSTG-CONF-05: https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/05-Enumerate_Infrastructure_and_Application_Admin_Interfaces + +.. index:: Backend, ext:backend diff --git a/typo3/sysext/frontend/Resources/Private/Php/frontend.php b/typo3/sysext/core/Resources/Private/Php/index.php similarity index 79% rename from typo3/sysext/frontend/Resources/Private/Php/frontend.php rename to typo3/sysext/core/Resources/Private/Php/index.php index 728fa4f91a69..2fa0600eb299 100644 --- a/typo3/sysext/frontend/Resources/Private/Php/frontend.php +++ b/typo3/sysext/core/Resources/Private/Php/index.php @@ -16,6 +16,6 @@ // Set up the application for the frontend call_user_func(static function () { $classLoader = require __DIR__ . '/../../../../../../vendor/autoload.php'; - \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::run(0, \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_FE); - \TYPO3\CMS\Core\Core\Bootstrap::init($classLoader)->get(\TYPO3\CMS\Frontend\Http\Application::class)->run(); + \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::run(); + \TYPO3\CMS\Core\Core\Bootstrap::init($classLoader)->get(\TYPO3\CMS\Core\Http\Application::class)->run(); }); diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Login/BackendLoginCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Login/BackendLoginCest.php index 1fdd41da03f8..6e19b84a388a 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Login/BackendLoginCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Login/BackendLoginCest.php @@ -32,7 +32,7 @@ final class BackendLoginCest public function loginButtonMouseOver(ApplicationTester $I): void { $I->wantTo('check login functions'); - $I->amOnPage('/typo3/index.php'); + $I->amOnPage('/typo3'); $I->waitForElement('#t3-username', 10); $I->wantTo('mouse over css change login button'); @@ -57,7 +57,7 @@ public function loginButtonMouseOver(ApplicationTester $I): void public function loginDeniedWithInvalidCredentials(ApplicationTester $I): void { $I->wantTo('check login functions'); - $I->amOnPage('/typo3/index.php'); + $I->amOnPage('/typo3'); $I->waitForElement('#t3-username'); $I->wantTo('check empty credentials'); @@ -122,7 +122,7 @@ public function loginWorksAsEditorUser(ApplicationTester $I): void private function login(ApplicationTester $I, string $username, string $password): void { $I->amGoingTo('Step\Application\Login username: ' . $username); - $I->amOnPage('/typo3/index.php'); + $I->amOnPage('/typo3'); $I->waitForElement('#t3-username'); $I->fillField('#t3-username', $username); $I->fillField('#t3-password', $password); diff --git a/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php b/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php index 9b3bad9b54b3..dfd3c3330998 100644 --- a/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php +++ b/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php @@ -166,8 +166,8 @@ private function createServerRequest(string $url, string $method = 'GET'): Serve 'SERVER_NAME' => $requestUrlParts['host'] ?? 'localhost', 'SERVER_ADDR' => '127.0.0.1', 'REMOTE_ADDR' => '127.0.0.1', - 'SCRIPT_NAME' => '/typo3/index.php', - 'PHP_SELF' => '/typo3/index.php', + 'SCRIPT_NAME' => '/index.php', + 'PHP_SELF' => '/index.php', 'SCRIPT_FILENAME' => $docRoot . '/index.php', 'QUERY_STRING' => $requestUrlParts['query'] ?? '', 'REQUEST_URI' => $requestUrlParts['path'] . (isset($requestUrlParts['query']) ? '?' . $requestUrlParts['query'] : ''), diff --git a/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php b/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php index 5fc7bbf36c8f..b35d054eb7df 100644 --- a/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php +++ b/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php @@ -1354,9 +1354,11 @@ public function sanitizeLocalUrlAcceptsNotEncodedValidPaths(string $path): void Environment::getVarPath(), Environment::getConfigPath(), // needs to be a subpath in order to validate ".." references - Environment::getPublicPath() . '/typo3/index.php', + Environment::getPublicPath() . '/subdir/index.php', Environment::isWindows() ? 'WINDOWS' : 'UNIX' ); + $_SERVER['HTTP_HOST'] = 'localhost'; + $_SERVER['SCRIPT_NAME'] = '/subdir/index.php'; self::assertEquals($path, GeneralUtility::sanitizeLocalUrl($path)); } @@ -1375,9 +1377,11 @@ public function sanitizeLocalUrlAcceptsEncodedValidPaths(string $path): void Environment::getVarPath(), Environment::getConfigPath(), // needs to be a subpath in order to validate ".." references - Environment::getPublicPath() . '/typo3/index.php', + Environment::getPublicPath() . '/subdir/index.php', Environment::isWindows() ? 'WINDOWS' : 'UNIX' ); + $_SERVER['HTTP_HOST'] = 'localhost'; + $_SERVER['SCRIPT_NAME'] = '/subdir/index.php'; self::assertEquals(rawurlencode($path), GeneralUtility::sanitizeLocalUrl(rawurlencode($path))); } @@ -1426,12 +1430,11 @@ public function sanitizeLocalUrlAcceptsNotEncodedValidUrls(string $url, string $ Environment::getPublicPath(), Environment::getVarPath(), Environment::getConfigPath(), - // needs to be a subpath in order to validate ".." references - Environment::getPublicPath() . '/typo3/index.php', + Environment::getPublicPath() . '/index.php', Environment::isWindows() ? 'WINDOWS' : 'UNIX' ); $_SERVER['HTTP_HOST'] = $host; - $_SERVER['SCRIPT_NAME'] = $subDirectory . 'typo3/index.php'; + $_SERVER['SCRIPT_NAME'] = $subDirectory . 'index.php'; self::assertEquals($url, GeneralUtility::sanitizeLocalUrl($url)); } @@ -1449,12 +1452,11 @@ public function sanitizeLocalUrlAcceptsEncodedValidUrls(string $url, string $hos Environment::getPublicPath(), Environment::getVarPath(), Environment::getConfigPath(), - // needs to be a subpath in order to validate ".." references - Environment::getPublicPath() . '/typo3/index.php', + Environment::getPublicPath() . '/index.php', Environment::isWindows() ? 'WINDOWS' : 'UNIX' ); $_SERVER['HTTP_HOST'] = $host; - $_SERVER['SCRIPT_NAME'] = $subDirectory . 'typo3/index.php'; + $_SERVER['SCRIPT_NAME'] = $subDirectory . 'index.php'; self::assertEquals(rawurlencode($url), GeneralUtility::sanitizeLocalUrl(rawurlencode($url))); } @@ -1517,7 +1519,7 @@ public function sanitizeLocalUrlDeniesPlainInvalidUrlsInFrontendContext(string $ Environment::isWindows() ? 'WINDOWS' : 'UNIX' ); $_SERVER['HTTP_HOST'] = 'localhost'; - $_SERVER['SCRIPT_NAME'] = 'index.php'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; self::assertEquals('', GeneralUtility::sanitizeLocalUrl($url)); } diff --git a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php index 4962311a094f..1d688ab13319 100644 --- a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php @@ -51,8 +51,9 @@ final class UriBuilderTest extends UnitTestCase protected function setUp(): void { parent::setUp(); - $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $router = new Router($requestContextFactory); + $backendEntryPointResolver = new BackendEntryPointResolver(); + $requestContextFactory = new RequestContextFactory($backendEntryPointResolver); + $router = new Router($requestContextFactory, $backendEntryPointResolver); $router->addRoute('module_key', new Route('/test/Path', [])); $router->addRoute('module_key.controller_action', new Route('/test/Path/Controller/action', [])); $router->addRoute('module_key.controller2_action2', new Route('/test/Path/Controller2/action2', [])); diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/LinkViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/LinkViewHelperTest.php index 7edffdcd5b9b..a9c30d607880 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/LinkViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/LinkViewHelperTest.php @@ -38,8 +38,10 @@ public function renderRendersTagWithHrefFromRoute(): void { // Mock Uribuilder in this functional test so we don't have to work with existing routes $formProtectionFactoryMock = $this->createMock(FormProtectionFactory::class); - $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router($requestContextFactory), $formProtectionFactoryMock, $requestContextFactory])->getMock(); + $backendEntryPointResolver = new BackendEntryPointResolver(); + $requestContextFactory = new RequestContextFactory($backendEntryPointResolver); + $router = new Router($requestContextFactory, $backendEntryPointResolver); + $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([$router, $formProtectionFactoryMock, $requestContextFactory])->getMock(); $uriBuilderMock->expects(self::once())->method('buildUriFromRoute') ->with('theRouteArgument', ['parameter' => 'to pass'], 'theReferenceTypeArgument')->willReturn('theUri'); GeneralUtility::setSingletonInstance(UriBuilder::class, $uriBuilderMock); @@ -58,8 +60,10 @@ public function renderPassesEmptyArrayToUriBuilderForNoParameters(): void { // Mock Uribuilder in this functional test so we don't have to work with existing routes $formProtectionFactoryMock = $this->createMock(FormProtectionFactory::class); - $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router($requestContextFactory), $formProtectionFactoryMock, $requestContextFactory])->getMock(); + $backendEntryPointResolver = new BackendEntryPointResolver(); + $requestContextFactory = new RequestContextFactory($backendEntryPointResolver); + $router = new Router($requestContextFactory, $backendEntryPointResolver); + $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([$router, $formProtectionFactoryMock, $requestContextFactory])->getMock(); $uriBuilderMock->expects(self::once())->method('buildUriFromRoute') ->with('theRouteArgument', [], 'theReferenceTypeArgument')->willReturn('theUri'); GeneralUtility::setSingletonInstance(UriBuilder::class, $uriBuilderMock); diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/UriViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/UriViewHelperTest.php index 87540eb4395c..85688b9e875d 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/UriViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Be/UriViewHelperTest.php @@ -38,8 +38,10 @@ public function renderRendersTagWithHrefFromRoute(): void { // Mock Uribuilder in this functional test so we don't have to work with existing routes $formProtectionFactoryMock = $this->createMock(FormProtectionFactory::class); - $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router($requestContextFactory), $formProtectionFactoryMock, $requestContextFactory])->getMock(); + $backendEntryPointResolver = new BackendEntryPointResolver(); + $requestContextFactory = new RequestContextFactory($backendEntryPointResolver); + $router = new Router($requestContextFactory, $backendEntryPointResolver); + $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([$router, $formProtectionFactoryMock, $requestContextFactory])->getMock(); $uriBuilderMock->expects(self::once())->method('buildUriFromRoute') ->with('theRouteArgument', ['parameter' => 'to pass'], 'theReferenceTypeArgument')->willReturn('theUri'); GeneralUtility::setSingletonInstance(UriBuilder::class, $uriBuilderMock); @@ -58,8 +60,10 @@ public function renderPassesEmptyArrayToUriBuilderForNoParameters(): void { // Mock Uribuilder in this functional test so we don't have to work with existing routes $formProtectionFactoryMock = $this->createMock(FormProtectionFactory::class); - $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([new Router($requestContextFactory), $formProtectionFactoryMock, $requestContextFactory])->getMock(); + $backendEntryPointResolver = new BackendEntryPointResolver(); + $requestContextFactory = new RequestContextFactory($backendEntryPointResolver); + $router = new Router($requestContextFactory, $backendEntryPointResolver); + $uriBuilderMock = $this->getMockBuilder(UriBuilder::class)->setConstructorArgs([$router, $formProtectionFactoryMock, $requestContextFactory])->getMock(); $uriBuilderMock->expects(self::once())->method('buildUriFromRoute') ->with('theRouteArgument', [], 'theReferenceTypeArgument')->willReturn('theUri'); GeneralUtility::setSingletonInstance(UriBuilder::class, $uriBuilderMock); diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php index 04b03ec57fa3..ed33c74d985f 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php @@ -141,7 +141,7 @@ public function renderInBackendCoreContextAddsSection(): void */ public function renderInBackendCoreContextCreatesAbsoluteLink(): void { - $request = new ServerRequest('http://localhost/typo3/', null, 'php://input', [], ['HTTP_HOST' => 'localhost', 'SCRIPT_NAME' => 'typo3/index.php']); + $request = new ServerRequest('http://localhost/typo3', null, 'php://input', [], ['HTTP_HOST' => 'localhost', 'SCRIPT_NAME' => '/index.php']); $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); $request = $request->withAttribute('route', new Route('dummy', ['_identifier' => 'web_layout'])); $GLOBALS['TYPO3_REQUEST'] = $request; diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/PageViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/PageViewHelperTest.php index 1ccdc101ca24..72edc7b6a319 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/PageViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/PageViewHelperTest.php @@ -139,7 +139,7 @@ public function renderInBackendCoreContextAddsSection(): void */ public function renderInBackendCoreContextCreatesAbsoluteUri(): void { - $request = new ServerRequest('http://localhost/typo3/', null, 'php://input', [], ['HTTP_HOST' => 'localhost', 'SCRIPT_NAME' => 'typo3/index.php']); + $request = new ServerRequest('http://localhost/typo3/', null, 'php://input', [], ['HTTP_HOST' => 'localhost', 'SCRIPT_NAME' => '/index.php']); $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); $request = $request->withAttribute('route', new Route('dummy', ['_identifier' => 'web_layout'])); $GLOBALS['TYPO3_REQUEST'] = $request; diff --git a/typo3/sysext/frontend/Classes/Composer/InstallerScripts.php b/typo3/sysext/frontend/Classes/Composer/InstallerScripts.php deleted file mode 100644 index 0fa73052e145..000000000000 --- a/typo3/sysext/frontend/Classes/Composer/InstallerScripts.php +++ /dev/null @@ -1,41 +0,0 @@ -addInstallerScript( - new EntryPoint( - dirname(__DIR__, 2) . '/Resources/Private/Php/frontend.php', - 'index.php' - ) - ); - } -} diff --git a/typo3/sysext/frontend/Classes/Http/Application.php b/typo3/sysext/frontend/Classes/Http/Application.php index 7aeea5007edc..b6f38e2efafb 100644 --- a/typo3/sysext/frontend/Classes/Http/Application.php +++ b/typo3/sysext/frontend/Classes/Http/Application.php @@ -20,17 +20,13 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -use TYPO3\CMS\Core\Configuration\ConfigurationManager; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\DateTimeAspect; use TYPO3\CMS\Core\Context\UserAspect; use TYPO3\CMS\Core\Context\VisibilityAspect; use TYPO3\CMS\Core\Context\WorkspaceAspect; -use TYPO3\CMS\Core\Core\Bootstrap; use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; use TYPO3\CMS\Core\Http\AbstractApplication; -use TYPO3\CMS\Core\Http\RedirectResponse; -use TYPO3\CMS\Core\Routing\BackendEntryPointResolver; /** * Entry point for the TYPO3 Frontend @@ -39,19 +35,13 @@ class Application extends AbstractApplication { public function __construct( RequestHandlerInterface $requestHandler, - protected readonly ConfigurationManager $configurationManager, protected readonly Context $context, - protected readonly BackendEntryPointResolver $backendEntryPointResolver ) { $this->requestHandler = $requestHandler; } public function handle(ServerRequestInterface $request): ResponseInterface { - if (!Bootstrap::checkIfEssentialConfigurationExists($this->configurationManager)) { - return $this->installToolRedirect($request); - } - // Create new request object having applicationType "I am a frontend request" attribute. $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); @@ -59,14 +49,6 @@ public function handle(ServerRequestInterface $request): ResponseInterface return parent::handle($request); } - /** - * Create a PSR-7 Response that redirects to the install tool - */ - protected function installToolRedirect(ServerRequestInterface $request): ResponseInterface - { - return new RedirectResponse($this->backendEntryPointResolver->getPathFromRequest($request) . 'install.php', 302); - } - /** * Initializes the Context used for accessing data and finding out the current state of the application */ diff --git a/typo3/sysext/frontend/Classes/ServiceProvider.php b/typo3/sysext/frontend/Classes/ServiceProvider.php index 02ac1d1d0fc8..492c0b2000c6 100644 --- a/typo3/sysext/frontend/Classes/ServiceProvider.php +++ b/typo3/sysext/frontend/Classes/ServiceProvider.php @@ -22,13 +22,11 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use TYPO3\CMS\Core\Cache\Exception\InvalidDataException; -use TYPO3\CMS\Core\Configuration\ConfigurationManager; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Exception as CoreException; use TYPO3\CMS\Core\Http\MiddlewareDispatcher; use TYPO3\CMS\Core\Http\MiddlewareStackResolver; use TYPO3\CMS\Core\Package\AbstractServiceProvider; -use TYPO3\CMS\Core\Routing\BackendEntryPointResolver; /** * @internal @@ -69,9 +67,7 @@ public static function getApplication(ContainerInterface $container): Http\Appli ); return new Http\Application( $requestHandler, - $container->get(ConfigurationManager::class), $container->get(Context::class), - $container->get(BackendEntryPointResolver::class) ); } diff --git a/typo3/sysext/frontend/Configuration/RequestMiddlewares.php b/typo3/sysext/frontend/Configuration/RequestMiddlewares.php index e78bf4b06df9..992515c51688 100644 --- a/typo3/sysext/frontend/Configuration/RequestMiddlewares.php +++ b/typo3/sysext/frontend/Configuration/RequestMiddlewares.php @@ -18,17 +18,10 @@ 'target' => \TYPO3\CMS\Frontend\Middleware\TimeTrackerInitialization::class, ], /** internal: do not use or reference this middleware in your own code */ - 'typo3/cms-core/verify-host-header' => [ - 'target' => \TYPO3\CMS\Core\Middleware\VerifyHostHeader::class, - 'after' => [ - 'typo3/cms-frontend/timetracker', - ], - ], - /** internal: do not use or reference this middleware in your own code */ 'typo3/cms-core/normalized-params-attribute' => [ 'target' => \TYPO3\CMS\Core\Middleware\NormalizedParamsAttribute::class, 'after' => [ - 'typo3/cms-core/verify-host-header', + 'typo3/cms-frontend/timetracker', ], ], /** internal: do not use or reference this middleware in your own code, as this will be possibly be removed */ diff --git a/typo3/sysext/frontend/Tests/Unit/Resource/FilePathSanitizerTest.php b/typo3/sysext/frontend/Tests/Unit/Resource/FilePathSanitizerTest.php index 728ce5a57175..96aa1182af98 100644 --- a/typo3/sysext/frontend/Tests/Unit/Resource/FilePathSanitizerTest.php +++ b/typo3/sysext/frontend/Tests/Unit/Resource/FilePathSanitizerTest.php @@ -151,12 +151,12 @@ public static function sanitizeCorrectlyResolvesPathsAndUrlsDataProvider(): arra true, ], 'relative input within existing public path' => [ - 'typo3/index.php', - 'typo3/index.php', + 'typo3/install.php', + 'typo3/install.php', ], 'spaces are trimmed from input' => [ - ' typo3/index.php ', - 'typo3/index.php', + ' typo3/install.php ', + 'typo3/install.php', ], 'extension paths are resolved as is, when second argument is true' => [ 'EXT:frontend/Resources/Private/Templates/MainPage.html', diff --git a/typo3/sysext/indexed_search/Tests/Unit/IndexerTest.php b/typo3/sysext/indexed_search/Tests/Unit/IndexerTest.php index bdc69dff987c..85906edd01e7 100644 --- a/typo3/sysext/indexed_search/Tests/Unit/IndexerTest.php +++ b/typo3/sysext/indexed_search/Tests/Unit/IndexerTest.php @@ -75,18 +75,6 @@ public function extractHyperLinksFindsCorrectPathWithAbsolutePath(): void self::assertEquals(Environment::getPublicPath() . '/index.php', $result[0]['localPath']); } - /** - * @test - */ - public function extractHyperLinksFindsCorrectPathForPathWithinTypo3Directory(): void - { - $html = 'test test test'; - $subject = $this->getMockBuilder(Indexer::class)->disableOriginalConstructor()->addMethods(['dummy'])->getMock(); - $result = $subject->extractHyperLinks($html); - self::assertCount(1, $result); - self::assertEquals(Environment::getPublicPath() . '/typo3/index.php', $result[0]['localPath']); - } - /** * @test */ diff --git a/typo3/sysext/install/Classes/Controller/LayoutController.php b/typo3/sysext/install/Classes/Controller/LayoutController.php index 31f8df4cf418..d603e9494882 100644 --- a/typo3/sysext/install/Classes/Controller/LayoutController.php +++ b/typo3/sysext/install/Classes/Controller/LayoutController.php @@ -27,6 +27,7 @@ use TYPO3\CMS\Core\Information\Typo3Version; use TYPO3\CMS\Core\Package\FailsafePackageManager; use TYPO3\CMS\Core\Page\ImportMap; +use TYPO3\CMS\Core\Routing\BackendEntryPointResolver; use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException; @@ -50,7 +51,8 @@ class LayoutController extends AbstractController public function __construct( private readonly FailsafePackageManager $packageManager, private readonly SilentConfigurationUpgradeService $silentConfigurationUpgradeService, - private readonly SilentTemplateFileUpgradeService $silentTemplateFileUpgradeService + private readonly SilentTemplateFileUpgradeService $silentTemplateFileUpgradeService, + private readonly BackendEntryPointResolver $backendEntryPointResolver, ) {} /** @@ -102,6 +104,7 @@ public function mainLayoutAction(ServerRequestInterface $request): ResponseInter { $view = $this->initializeView($request); $view->assign('moduleName', 'tools_tools' . ($request->getQueryParams()['install']['module'] ?? 'layout')); + $view->assign('backendUrl', (string)$this->backendEntryPointResolver->getUriFromRequest($request)); return new JsonResponse([ 'success' => true, 'html' => $view->render('Layout/MainLayout'), diff --git a/typo3/sysext/install/Classes/Service/WebServerConfigurationFileService.php b/typo3/sysext/install/Classes/Service/WebServerConfigurationFileService.php index 2d2e49c62f1f..051d3ded0e13 100644 --- a/typo3/sysext/install/Classes/Service/WebServerConfigurationFileService.php +++ b/typo3/sysext/install/Classes/Service/WebServerConfigurationFileService.php @@ -62,27 +62,34 @@ protected function addApacheBackendRoutingRewriteRules(): bool return false; } - $newRewriteRule = PHP_EOL . ' - ### BEGIN: TYPO3 automated migration - # If the file/symlink/directory does not exist but is below /typo3/, redirect to the TYPO3 Backend entry point. - RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - RewriteCond %{REQUEST_FILENAME} !-l - RewriteRule ^typo3/(.*)$ %{ENV:CWD}typo3/index.php [QSA,L] - ### END: TYPO3 automated migration'; + $configurationFileContent = preg_replace( + sprintf('/%s/', implode('\s*', array_map( + static fn($s) => preg_quote($s, '/'), + [ + 'RewriteCond %{REQUEST_FILENAME} !-d', + 'RewriteCond %{REQUEST_FILENAME} !-l', + 'RewriteRule ^typo3/(.*)$ %{ENV:CWD}typo3/index.php [QSA,L]', + ] + ))), + 'RewriteRule ^typo3/(.*)$ %{ENV:CWD}index.php [QSA,L]', + $configurationFileContent, + ); - return (bool)file_put_contents( - $configurationFilename, - str_replace( - '# Stop rewrite processing, if we are in the typo3/ directory or any other known directory', + $configurationFileContent = str_replace( + [ '# Stop rewrite processing, if we are in any other known directory', - $this->performBackendRoutingRewriteRulesUpdate( - '/(RewriteRule\s\^\(\?\:(typo3\/\|).*\s\[L\])(.*RewriteRule\s\^\.\*\$\s%{ENV:CWD}index\.php\s\[QSA,L\])/s', - $newRewriteRule, - $configurationFileContent, - ) - ) + '# Stop rewrite processing, if we are in the typo3/ directory or any other known directory', // v10 style comment + '# If the file does not exist but is below /typo3/, redirect to the TYPO3 Backend entry point.', + ], + [ + '# Stop rewrite processing, if we are in any known directory', + '# Stop rewrite processing, if we are in any known directory', + '# If the file does not exist but is below /typo3/, rewrite to the main TYPO3 entry point.', + ], + $configurationFileContent, ); + + return (bool)file_put_contents($configurationFilename, $configurationFileContent); } protected function addMicrosoftIisBackendRoutingRewriteRules(): bool @@ -94,24 +101,17 @@ protected function addMicrosoftIisBackendRoutingRewriteRules(): bool return false; } - $newRewriteRule = ' - - - - - - - - - - - '; - return (bool)file_put_contents( $configurationFilename, - $this->performBackendRoutingRewriteRulesUpdate( - '/(.+?<\/rule>)(.*', + '', + ], + [ + '', + '', + ], $configurationFileContent ) ); @@ -145,48 +145,14 @@ protected function getConfigurationFileContent(string $filename): string protected function updateNecessary(string $configurationFileContent): bool { if ($this->isApache()) { - return (bool)preg_match('/RewriteRule\s\^\(\?\:typo3\/\|.*\s\[L\].*RewriteRule\s\^\.\*\$\s%{ENV:CWD}index\.php\s\[QSA,L\]/s', $configurationFileContent) - && !str_contains($configurationFileContent, 'RewriteRule ^typo3/(.*)$ %{ENV:CWD}typo3/index.php [QSA,L]'); + return str_contains($configurationFileContent, 'RewriteRule ^typo3/(.*)$ %{ENV:CWD}typo3/index.php [QSA,L]'); } if ($this->isMicrosoftIis()) { - return (bool)preg_match('/.*/s', $configurationFileContent) - && !str_contains($configurationFileContent, ''); + return str_contains($configurationFileContent, ''); } return false; } - /** - * Removes the 'typo3' directory from the existing "known directory" rewrite rule and - * adds the new backend rewrite rule between this rule and the frontend rewrite rule. - * - * Pattern must contain three capturing groups: - * 1: The "known directory" rule from which "typo3" should be removed - * 2: The "typo3" string to be removed - * 3: The subsequent part including the frontend rewrite rule - * - * The new rule will then be added between group 1 and group 3. - * - * @param string $pattern - * @param string $newRewriteRule - * @param string $configurationFileContent - * - * @return string The updated webserver configuration - */ - protected function performBackendRoutingRewriteRulesUpdate( - string $pattern, - string $newRewriteRule, - string $configurationFileContent - ): string { - return (string)preg_replace_callback( - $pattern, - static function ($matches) use ($newRewriteRule) { - return str_replace($matches[2], '', ($matches[1] . $newRewriteRule)) . $matches[3]; - }, - $configurationFileContent, - 1 - ); - } - protected function isApache(): bool { return str_starts_with($this->webServer, 'Apache'); diff --git a/typo3/sysext/install/Classes/ServiceProvider.php b/typo3/sysext/install/Classes/ServiceProvider.php index edb1d022b715..7459c0010c7c 100644 --- a/typo3/sysext/install/Classes/ServiceProvider.php +++ b/typo3/sysext/install/Classes/ServiceProvider.php @@ -42,6 +42,7 @@ use TYPO3\CMS\Core\Package\FailsafePackageManager; use TYPO3\CMS\Core\Package\PackageManager; use TYPO3\CMS\Core\Registry; +use TYPO3\CMS\Core\Routing\BackendEntryPointResolver; use TYPO3\CMS\Core\TypoScript\AST\CommentAwareAstBuilder; use TYPO3\CMS\Core\TypoScript\AST\Traverser\AstTraverser; use TYPO3\CMS\Core\TypoScript\Tokenizer\LosslessTokenizer; @@ -285,7 +286,8 @@ public static function getLayoutController(ContainerInterface $container): Contr return new Controller\LayoutController( $container->get(FailsafePackageManager::class), $container->get(Service\SilentConfigurationUpgradeService::class), - $container->get(Service\SilentTemplateFileUpgradeService::class) + $container->get(Service\SilentTemplateFileUpgradeService::class), + $container->get(BackendEntryPointResolver::class), ); } diff --git a/typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-htaccess b/typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-htaccess index d39e2af8bc7f..0e1f313e76b3 100644 --- a/typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-htaccess +++ b/typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-htaccess @@ -318,15 +318,13 @@ AddDefaultCharset utf-8 RewriteCond %{SCRIPT_FILENAME} -f RewriteRule (?:^|/)\. - [F] - # Stop rewrite processing, if we are in any other known directory + # Stop rewrite processing, if we are in any known directory # NOTE: Add your additional local storages here RewriteRule ^(?:fileadmin/|typo3conf/|typo3temp/|uploads/) - [L] - # If the file/symlink/directory does not exist but is below /typo3/, redirect to the TYPO3 Backend entry point. + # If the file/symlink/directory does not exist but is below /typo3/, redirect to the main TYPO3 entry point. RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - RewriteCond %{REQUEST_FILENAME} !-l - RewriteRule ^typo3/(.*)$ %{ENV:CWD}typo3/index.php [QSA,L] + RewriteRule ^typo3/(.*)$ %{ENV:CWD}index.php [QSA,L] # If the file/symlink/directory does not exist => Redirect to index.php. # For httpd.conf, you need to prefix each '%{REQUEST_FILENAME}' with '%{DOCUMENT_ROOT}'. diff --git a/typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-web-config b/typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-web-config index 1f7ec26bf777..ba59759aac47 100644 --- a/typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-web-config +++ b/typo3/sysext/install/Resources/Private/FolderStructureTemplateFiles/root-web-config @@ -51,14 +51,13 @@ - + - - + diff --git a/typo3/sysext/install/Resources/Private/Php/install.php b/typo3/sysext/install/Resources/Private/Php/install.php index 8e58c44a36e5..e427d3787d7f 100644 --- a/typo3/sysext/install/Resources/Private/Php/install.php +++ b/typo3/sysext/install/Resources/Private/Php/install.php @@ -15,6 +15,6 @@ call_user_func(static function () { $classLoader = require __DIR__ . '/../../../../../../vendor/autoload.php'; - \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::run(1, \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_INSTALL); + \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::run(1); \TYPO3\CMS\Core\Core\Bootstrap::init($classLoader, true)->get(\TYPO3\CMS\Install\Http\Application::class)->run(); }); diff --git a/typo3/sysext/install/Resources/Private/Templates/Layout/MainLayout.html b/typo3/sysext/install/Resources/Private/Templates/Layout/MainLayout.html index c08943601937..c5a18e1dfbfe 100644 --- a/typo3/sysext/install/Resources/Private/Templates/Layout/MainLayout.html +++ b/typo3/sysext/install/Resources/Private/Templates/Layout/MainLayout.html @@ -118,7 +118,7 @@

Loading cards

  • - - + diff --git a/typo3/sysext/install/Tests/Functional/Fixtures/web.config_custom_config b/typo3/sysext/install/Tests/Functional/Fixtures/web.config_custom_config deleted file mode 100644 index d57b84a5add6..000000000000 --- a/typo3/sysext/install/Tests/Functional/Fixtures/web.config_custom_config +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/typo3/sysext/install/Tests/Functional/Fixtures/web.config_outdated b/typo3/sysext/install/Tests/Functional/Fixtures/web.config_outdated new file mode 100644 index 000000000000..5f35df86a880 --- /dev/null +++ b/typo3/sysext/install/Tests/Functional/Fixtures/web.config_outdated @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/typo3/sysext/install/Tests/Functional/Fixtures/web.config_valid b/typo3/sysext/install/Tests/Functional/Fixtures/web.config_valid deleted file mode 100644 index c7bda81dcf49..000000000000 --- a/typo3/sysext/install/Tests/Functional/Fixtures/web.config_valid +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/typo3/sysext/install/Tests/Functional/Fixtures/web.config_wrong_order b/typo3/sysext/install/Tests/Functional/Fixtures/web.config_wrong_order deleted file mode 100644 index b73c602671b8..000000000000 --- a/typo3/sysext/install/Tests/Functional/Fixtures/web.config_wrong_order +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/typo3/sysext/install/Tests/Functional/Service/WebServerConfigurationFileServiceTest.php b/typo3/sysext/install/Tests/Functional/Service/WebServerConfigurationFileServiceTest.php index d8d463df6a53..ddf3e804605b 100644 --- a/typo3/sysext/install/Tests/Functional/Service/WebServerConfigurationFileServiceTest.php +++ b/typo3/sysext/install/Tests/Functional/Service/WebServerConfigurationFileServiceTest.php @@ -68,55 +68,36 @@ public function addWebServerSpecificBackendRoutingRewriteRulesTest( public static function webServerConfigurationIsChangedDataProvider(): \Generator { - yield '.htaccess with custom configuration - will not be changed' => [ - 'Apache', - '.htaccess_custom_config', - ]; - yield '.htaccess with wrong order - will not be changed' => [ - 'Apache', - '.htaccess_wrong_order', - ]; yield '.htaccess already updated - will not be changed' => [ 'Apache', '.htaccess_already_updated', ]; - yield '.htaccess without custom configuration - will be changed' => [ + yield 'outdated .htaccess - will be changed' => [ 'Apache', - '.htaccess_valid', + '.htaccess_outdated', true, [ - 'TYPO3 automated migration', - '# If the file/symlink/directory does not exist but is below /typo3/, redirect to the TYPO3 Backend entry point.', - 'RewriteRule ^typo3/(.*)$ %{ENV:CWD}typo3/index.php [QSA,L]', + '# Stop rewrite processing, if we are in any known directory', + 'RewriteRule ^typo3/(.*)$ %{ENV:CWD}index.php [QSA,L]', ], [ - 'Stop rewrite processing, if we are in the typo3/ directory', - 'RewriteRule ^(?:typo3/|', + '# Stop rewrite processing, if we are in any other known directory', + 'RewriteRule ^typo3/(.*)$ %{ENV:CWD}typo3/index.php [QSA,L]', ], ]; - yield 'web.config with custom configuration - will not be changed' => [ - 'Microsoft-IIS', - 'web.config_custom_config', - ]; - yield 'web.config with wrong order - will not be changed' => [ - 'Microsoft-IIS', - 'web.config_wrong_order', - ]; yield 'web.config already updated - will not be changed' => [ 'Microsoft-IIS', 'web.config_already_updated', ]; - yield 'web.config without custom configuration - will be changed' => [ + yield 'outdated web.config - will be changed' => [ 'Microsoft-IIS', - 'web.config_valid', + 'web.config_outdated', true, [ - 'TYPO3 automated migration', - 'TYPO3 - If the file/directory does not exist but is below /typo3/, redirect to the TYPO3 Backend entry point.', - '', + '', ], [ - '', ], ]; }