From 3b1fe2d527d5ea990fef23daf68e33ca6b096368 Mon Sep 17 00:00:00 2001
From: Andreas Fernandez <a.fernandez@scripting-base.de>
Date: Mon, 24 Apr 2023 11:19:41 +0200
Subject: [PATCH] [BUGFIX] Check used webserver during CLI setup
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

When TYPO3 is installed via CLI, the setup command now asks for the
webserver being used to be able to copy necessary webserver files, e.g.
`.htaccess` for Apache or `web.config` for IIS.

Resolves: #100719
Releases: main, 12.4
Change-Id: Ibb97c4cf92b29ceeda85ae848ceeae16afb73359
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/80772
Tested-by: Stefan B�rk <stefan@buerk.tech>
Reviewed-by: Stefan B�rk <stefan@buerk.tech>
Tested-by: core-ci <typo3@b13.com>
---
 ...eature-99221-AddCliInstallSetupCommand.rst |  1 +
 .../install/Classes/Command/SetupCommand.php  | 35 +++++++++-
 .../Controller/EnvironmentController.php      |  7 +-
 .../Controller/InstallerController.php        |  9 +--
 .../Classes/Controller/UpgradeController.php  |  6 +-
 .../FolderStructure/DefaultFactory.php        | 25 +++----
 .../Classes/Service/CoreUpdateService.php     |  6 +-
 .../sysext/install/Classes/WebserverType.php  | 69 +++++++++++++++++++
 .../FolderStructure/DefaultFactoryTest.php    |  4 +-
 9 files changed, 133 insertions(+), 29 deletions(-)
 create mode 100644 typo3/sysext/install/Classes/WebserverType.php

diff --git a/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99221-AddCliInstallSetupCommand.rst b/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99221-AddCliInstallSetupCommand.rst
index 62ad6ec4ef6e..a9368a6ec931 100644
--- a/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99221-AddCliInstallSetupCommand.rst
+++ b/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99221-AddCliInstallSetupCommand.rst
@@ -43,6 +43,7 @@ Automated setup:
     TYPO3_SETUP_ADMIN_USERNAME=admin \
     TYPO3_SETUP_CREATE_SITE="https://your-typo3-site.com/" \
     TYPO3_PROJECT_NAME="Automated Setup" \
+    TYPO3_SERVER_TYPE="apache" \
     ./bin/typo3 setup --force
 
 ..  warning::
diff --git a/typo3/sysext/install/Classes/Command/SetupCommand.php b/typo3/sysext/install/Classes/Command/SetupCommand.php
index b4cdac4c24bf..30e03a309e0b 100644
--- a/typo3/sysext/install/Classes/Command/SetupCommand.php
+++ b/typo3/sysext/install/Classes/Command/SetupCommand.php
@@ -35,6 +35,7 @@ use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
 use TYPO3\CMS\Install\Service\LateBootService;
 use TYPO3\CMS\Install\Service\SetupDatabaseService;
 use TYPO3\CMS\Install\Service\SetupService;
+use TYPO3\CMS\Install\WebserverType;
 
 /**
  * CLI command for setting up TYPO3 via CLI
@@ -58,6 +59,7 @@ class SetupCommand extends Command
         private readonly LateBootService $lateBootService
     ) {
         parent::__construct($name);
+
     }
 
     protected function configure()
@@ -139,6 +141,13 @@ class SetupCommand extends Command
                 'Create a basic site setup (root page and site configuration) with the given domain',
                 false
             )
+            ->addOption(
+                'server-type',
+                null,
+                InputOption::VALUE_OPTIONAL,
+                'Define the web server the TYPO3 installation will be running on',
+                'other'
+            )
             ->addOption(
                 'force',
                 null,
@@ -171,6 +180,7 @@ TYPO3_SETUP_ADMIN_EMAIL=admin@example.com \
 TYPO3_SETUP_ADMIN_USERNAME=admin \
 TYPO3_SETUP_CREATE_SITE="https://your-typo3-site.com/" \
 TYPO3_PROJECT_NAME="Automated Setup" \
+TYPO3_SERVER_TYPE="apache" \
 ./bin/typo3 setup --force
 ---------------------------------
 
@@ -197,8 +207,9 @@ EOT
         $questionHelper = $this->getHelper('question');
 
         // Ensure all required files and folders exist
+        $serverType = $this->getServerType($questionHelper, $input, $output);
         $folderStructureFactory = GeneralUtility::makeInstance(DefaultFactory::class);
-        $folderStructureFactory->getStructure()->fix();
+        $folderStructureFactory->getStructure($serverType)->fix();
 
         try {
             $force = $input->getOption('force');
@@ -479,6 +490,28 @@ EOT
         return $databaseConnectionOptions;
     }
 
+    protected function getServerType(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): WebserverType
+    {
+        $serverTypeValidator = function (string $serverType): WebserverType {
+            if (!array_key_exists($serverType, WebserverType::getDescriptions())) {
+                throw new \RuntimeException(
+                    'Webserver must be any of ' . implode(', ', array_keys(WebserverType::getDescriptions())),
+                    1682329380,
+                );
+            }
+
+            return WebserverType::from($serverType);
+        };
+        $serverTypeFromCli = $this->getFallbackValueEnvOrOption($input, 'server-type', 'TYPO3_SERVER_TYPE');
+        if ($serverTypeFromCli === false && $input->isInteractive()) {
+            $questionServerType = new ChoiceQuestion('Which web server is used?', WebserverType::getDescriptions());
+            $questionServerType->setValidator($serverTypeValidator);
+            return $questionHelper->ask($input, $output, $questionServerType);
+        }
+
+        return $serverTypeValidator($serverTypeFromCli);
+    }
+
     protected function getAdminUserName(QuestionHelper $questionHelper, InputInterface $input, OutputInterface $output): string
     {
         $usernameValidator = static function ($username) {
diff --git a/typo3/sysext/install/Classes/Controller/EnvironmentController.php b/typo3/sysext/install/Classes/Controller/EnvironmentController.php
index 90e21a358123..3bf451be2c08 100644
--- a/typo3/sysext/install/Classes/Controller/EnvironmentController.php
+++ b/typo3/sysext/install/Classes/Controller/EnvironmentController.php
@@ -44,6 +44,7 @@ use TYPO3\CMS\Install\SystemEnvironment\Check;
 use TYPO3\CMS\Install\SystemEnvironment\DatabaseCheck;
 use TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck;
 use TYPO3\CMS\Install\SystemEnvironment\SetupCheck;
+use TYPO3\CMS\Install\WebserverType;
 
 /**
  * Environment controller
@@ -153,7 +154,7 @@ class EnvironmentController extends AbstractController
     {
         $view = $this->initializeView($request);
         $folderStructureFactory = GeneralUtility::makeInstance(DefaultFactory::class);
-        $structureFacade = $folderStructureFactory->getStructure();
+        $structureFacade = $folderStructureFactory->getStructure(WebserverType::fromRequest($request));
 
         $structureMessages = $structureFacade->getStatus();
         $errorQueue = new FlashMessageQueue('install');
@@ -194,10 +195,10 @@ class EnvironmentController extends AbstractController
     /**
      * Try to fix folder structure errors
      */
-    public function folderStructureFixAction(): ResponseInterface
+    public function folderStructureFixAction(ServerRequestInterface $request): ResponseInterface
     {
         $folderStructureFactory = GeneralUtility::makeInstance(DefaultFactory::class);
-        $structureFacade = $folderStructureFactory->getStructure();
+        $structureFacade = $folderStructureFactory->getStructure(WebserverType::fromRequest($request));
         $fixedStatusObjects = $structureFacade->fix();
         return new JsonResponse([
             'success' => true,
diff --git a/typo3/sysext/install/Classes/Controller/InstallerController.php b/typo3/sysext/install/Classes/Controller/InstallerController.php
index 9d45993c2858..cc03442b39d6 100644
--- a/typo3/sysext/install/Classes/Controller/InstallerController.php
+++ b/typo3/sysext/install/Classes/Controller/InstallerController.php
@@ -55,6 +55,7 @@ use TYPO3\CMS\Install\Service\SilentConfigurationUpgradeService;
 use TYPO3\CMS\Install\Service\SilentTemplateFileUpgradeService;
 use TYPO3\CMS\Install\SystemEnvironment\Check;
 use TYPO3\CMS\Install\SystemEnvironment\SetupCheck;
+use TYPO3\CMS\Install\WebserverType;
 use TYPO3Fluid\Fluid\View\TemplateView as FluidTemplateView;
 
 /**
@@ -151,7 +152,7 @@ final class InstallerController
     /**
      * Render "environment and folders"
      */
-    public function showEnvironmentAndFoldersAction(): ResponseInterface
+    public function showEnvironmentAndFoldersAction(ServerRequestInterface $request): ResponseInterface
     {
         $view = $this->initializeView();
         $systemCheckMessageQueue = new FlashMessageQueue('install');
@@ -164,7 +165,7 @@ final class InstallerController
             $systemCheckMessageQueue->enqueue($message);
         }
         $folderStructureFactory = GeneralUtility::makeInstance(DefaultFactory::class);
-        $structureFacade = $folderStructureFactory->getStructure();
+        $structureFacade = $folderStructureFactory->getStructure(WebserverType::fromRequest($request));
         $structureMessageQueue = $structureFacade->getStatus();
         return new JsonResponse([
             'success' => true,
@@ -178,10 +179,10 @@ final class InstallerController
     /**
      * Create main folder layout, LocalConfiguration, PackageStates
      */
-    public function executeEnvironmentAndFoldersAction(): ResponseInterface
+    public function executeEnvironmentAndFoldersAction(ServerRequestInterface $request): ResponseInterface
     {
         $folderStructureFactory = GeneralUtility::makeInstance(DefaultFactory::class);
-        $structureFacade = $folderStructureFactory->getStructure();
+        $structureFacade = $folderStructureFactory->getStructure(WebserverType::fromRequest($request));
         $structureFixMessageQueue = $structureFacade->fix();
         $errorsFromStructure = $structureFixMessageQueue->getAllMessages(ContextualFeedbackSeverity::ERROR);
 
diff --git a/typo3/sysext/install/Classes/Controller/UpgradeController.php b/typo3/sysext/install/Classes/Controller/UpgradeController.php
index adfeb36c2dcd..f642b63ef428 100644
--- a/typo3/sysext/install/Classes/Controller/UpgradeController.php
+++ b/typo3/sysext/install/Classes/Controller/UpgradeController.php
@@ -73,6 +73,7 @@ use TYPO3\CMS\Install\Service\LateBootService;
 use TYPO3\CMS\Install\Service\LoadTcaService;
 use TYPO3\CMS\Install\Service\UpgradeWizardsService;
 use TYPO3\CMS\Install\UpgradeAnalysis\DocumentationFile;
+use TYPO3\CMS\Install\WebserverType;
 
 /**
  * Upgrade controller
@@ -230,7 +231,10 @@ class UpgradeController extends AbstractController
     {
         $this->coreUpdateInitialize();
         return new JsonResponse([
-            'success' => $this->coreUpdateService->checkPreConditions($this->coreUpdateGetVersionToHandle($request)),
+            'success' => $this->coreUpdateService->checkPreConditions(
+                $this->coreUpdateGetVersionToHandle($request),
+                WebserverType::fromRequest($request),
+            ),
             'status' => $this->coreUpdateService->getMessages(),
         ]);
     }
diff --git a/typo3/sysext/install/Classes/FolderStructure/DefaultFactory.php b/typo3/sysext/install/Classes/FolderStructure/DefaultFactory.php
index 4644f981ed93..50edb28a6e86 100644
--- a/typo3/sysext/install/Classes/FolderStructure/DefaultFactory.php
+++ b/typo3/sysext/install/Classes/FolderStructure/DefaultFactory.php
@@ -16,6 +16,7 @@
 namespace TYPO3\CMS\Install\FolderStructure;
 
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Install\WebserverType;
 
 /**
  * Factory returns default folder structure object hierarchy
@@ -30,9 +31,9 @@ class DefaultFactory
      *
      * @return StructureFacadeInterface
      */
-    public function getStructure()
+    public function getStructure(WebserverType $webserverType)
     {
-        $rootNode = new RootNode($this->getDefaultStructureDefinition(), null);
+        $rootNode = new RootNode($this->getDefaultStructureDefinition($webserverType), null);
         return new StructureFacade($rootNode);
     }
 
@@ -40,7 +41,7 @@ class DefaultFactory
      * Default definition of folder and file structure with dynamic
      * permission settings
      */
-    protected function getDefaultStructureDefinition(): array
+    protected function getDefaultStructureDefinition(WebserverType $webserverType): array
     {
         $filePermission = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'];
         $directoryPermission = $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'];
@@ -129,14 +130,14 @@ class DefaultFactory
             ];
 
             // Have a default .htaccess if running apache web server or a default web.config if running IIS
-            if ($this->isApacheServer()) {
+            if ($webserverType->isApacheServer()) {
                 $structure['children'][] = [
                     'name' => '.htaccess',
                     'type' => FileNode::class,
                     'targetPermission' => $filePermission,
                     'targetContentFile' => self::TEMPLATE_PATH . '/root-htaccess',
                 ];
-            } elseif ($this->isMicrosoftIisServer()) {
+            } elseif ($webserverType->isMicrosoftInternetInformationServer()) {
                 $structure['children'][] = [
                     'name' => 'web.config',
                     'type' => FileNode::class,
@@ -172,14 +173,14 @@ class DefaultFactory
             ];
 
             // Have a default .htaccess if running apache web server or a default web.config if running IIS
-            if ($this->isApacheServer()) {
+            if ($webserverType->isApacheServer()) {
                 $publicPathSubStructure[] = [
                     'name' => '.htaccess',
                     'type' => FileNode::class,
                     'targetPermission' => $filePermission,
                     'targetContentFile' => self::TEMPLATE_PATH . '/root-htaccess',
                 ];
-            } elseif ($this->isMicrosoftIisServer()) {
+            } elseif ($webserverType->isMicrosoftInternetInformationServer()) {
                 $publicPathSubStructure[] = [
                     'name' => 'web.config',
                     'type' => FileNode::class,
@@ -398,14 +399,4 @@ class DefaultFactory
             ],
         ];
     }
-
-    protected function isApacheServer(): bool
-    {
-        return isset($_SERVER['SERVER_SOFTWARE']) && str_starts_with($_SERVER['SERVER_SOFTWARE'], 'Apache');
-    }
-
-    protected function isMicrosoftIisServer(): bool
-    {
-        return isset($_SERVER['SERVER_SOFTWARE']) && str_starts_with($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS');
-    }
 }
diff --git a/typo3/sysext/install/Classes/Service/CoreUpdateService.php b/typo3/sysext/install/Classes/Service/CoreUpdateService.php
index 89fe248789c0..32dd02c51377 100644
--- a/typo3/sysext/install/Classes/Service/CoreUpdateService.php
+++ b/typo3/sysext/install/Classes/Service/CoreUpdateService.php
@@ -25,6 +25,7 @@ use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
 use TYPO3\CMS\Install\CoreVersion\CoreRelease;
 use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
+use TYPO3\CMS\Install\WebserverType;
 
 /**
  * Core update service.
@@ -126,14 +127,15 @@ class CoreUpdateService
      * Check if an update is possible at all
      *
      * @param CoreRelease $coreRelease The target core release
+     * @param WebserverType $webserverType The webserver type.
      * @return bool TRUE on success
      */
-    public function checkPreConditions(CoreRelease $coreRelease)
+    public function checkPreConditions(CoreRelease $coreRelease, WebserverType $webserverType)
     {
         $success = true;
 
         // Folder structure test: Update can be done only if folder structure returns no errors
-        $folderStructureFacade = GeneralUtility::makeInstance(DefaultFactory::class)->getStructure();
+        $folderStructureFacade = GeneralUtility::makeInstance(DefaultFactory::class)->getStructure($webserverType);
         $folderStructureMessageQueue = $folderStructureFacade->getStatus();
         $folderStructureErrors = $folderStructureMessageQueue->getAllMessages(ContextualFeedbackSeverity::ERROR);
         $folderStructureWarnings = $folderStructureMessageQueue->getAllMessages(ContextualFeedbackSeverity::WARNING);
diff --git a/typo3/sysext/install/Classes/WebserverType.php b/typo3/sysext/install/Classes/WebserverType.php
new file mode 100644
index 000000000000..58946d69da3b
--- /dev/null
+++ b/typo3/sysext/install/Classes/WebserverType.php
@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Install;
+
+use Psr\Http\Message\ServerRequestInterface;
+
+/**
+ * @internal This enum is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
+ */
+enum WebserverType: string
+{
+    case Apache = 'apache';
+    case MicrosoftInternetInformationServer = 'iis';
+    case Other = 'other';
+
+    public static function fromRequest(ServerRequestInterface $request): self
+    {
+        return self::fromType((string)($request->getServerParams()['SERVER_SOFTWARE'] ?? ''));
+    }
+
+    public static function fromType(string $type): self
+    {
+        if ($type === 'apache' || str_starts_with($type, 'Apache')) {
+            return self::Apache;
+        }
+        if ($type === 'iis' || str_starts_with($type, 'Microsoft-IIS')) {
+            return self::MicrosoftInternetInformationServer;
+        }
+
+        return self::Other;
+    }
+
+    /**
+     * @return array<string, non-empty-string>
+     */
+    public static function getDescriptions(): array
+    {
+        return [
+            self::Apache->value => 'Apache',
+            self::MicrosoftInternetInformationServer->value => 'Microsoft IIS',
+            self::Other->value => 'Other (use for anything else)',
+        ];
+    }
+
+    public function isApacheServer(): bool
+    {
+        return $this === self::Apache;
+    }
+
+    public function isMicrosoftInternetInformationServer(): bool
+    {
+        return $this === self::MicrosoftInternetInformationServer;
+    }
+}
diff --git a/typo3/sysext/install/Tests/Unit/FolderStructure/DefaultFactoryTest.php b/typo3/sysext/install/Tests/Unit/FolderStructure/DefaultFactoryTest.php
index f5fb42734e3f..92ecad0b40bd 100644
--- a/typo3/sysext/install/Tests/Unit/FolderStructure/DefaultFactoryTest.php
+++ b/typo3/sysext/install/Tests/Unit/FolderStructure/DefaultFactoryTest.php
@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Install\Tests\Unit\FolderStructure;
 
 use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
 use TYPO3\CMS\Install\FolderStructure\StructureFacadeInterface;
+use TYPO3\CMS\Install\WebserverType;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 final class DefaultFactoryTest extends UnitTestCase
@@ -28,7 +29,8 @@ final class DefaultFactoryTest extends UnitTestCase
      */
     public function getStructureReturnsInstanceOfStructureFacadeInterface(): void
     {
+        $webserverType = WebserverType::fromType('i-dont-care');
         $object = new DefaultFactory();
-        self::assertInstanceOf(StructureFacadeInterface::class, $object->getStructure());
+        self::assertInstanceOf(StructureFacadeInterface::class, $object->getStructure($webserverType));
     }
 }
-- 
GitLab