From 1c797cc6d0b2197ff43d6c3330061f75a0dbc9cd Mon Sep 17 00:00:00 2001
From: Helmut Hummel <typo3@helhum.io>
Date: Wed, 29 Apr 2020 12:30:31 +0200
Subject: [PATCH] [BUGFIX] Allow LocalDriver baseUri to be set to any value

LocalDriver evaluates a configuration option baseUri,
to use this URI instead of a derived one from the absolute
base path of files.

However there are currently limitations that need to be fixed
to allow more complex setups:

1. baseUri is not defined in flexform, thus cannot be set from UI
2. baseUri isn't evaluated, when the absolute file path
   is within TYPO3's public path
3. baseUri is currently required to be a fully qualified URI (with scheme and host)

This change addresses all three of them to cover the following use cases:

1. Provide a different hostname to serve the files from, even
   when they are located on the same machine.
2. Allow base URI without host name, for files that are on the same host,
   but in a folder not within TYPO3's root directory (e.g. due to special
   web server configuration)

Resolves: #91232
Releases: master, 10.4
Change-Id: I7694661f9892c612489c68ef271f965df65744b6
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64345
Reviewed-by: Markus Klein <markus.klein@typo3.org>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Markus Klein <markus.klein@typo3.org>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
---
 .../Resource/Driver/AbstractDriver.php        |   2 +-
 .../Classes/Resource/Driver/LocalDriver.php   |   7 +-
 .../Resource/Driver/LocalDriverFlexForm.xml   |  10 ++
 .../Private/Language/locallang_mod_file.xlf   |   6 +
 .../Unit/Resource/Driver/LocalDriverTest.php  | 103 ++++++++++++++++++
 5 files changed, 123 insertions(+), 5 deletions(-)

diff --git a/typo3/sysext/core/Classes/Resource/Driver/AbstractDriver.php b/typo3/sysext/core/Classes/Resource/Driver/AbstractDriver.php
index c5d8bfdd9d5e..487399b8b50c 100644
--- a/typo3/sysext/core/Classes/Resource/Driver/AbstractDriver.php
+++ b/typo3/sysext/core/Classes/Resource/Driver/AbstractDriver.php
@@ -117,7 +117,7 @@ abstract class AbstractDriver implements DriverInterface
      */
     public function hasCapability($capability)
     {
-        return ($this->capabilities & $capability) == $capability;
+        return ($this->capabilities & $capability) === $capability;
     }
 
     /*******************
diff --git a/typo3/sysext/core/Classes/Resource/Driver/LocalDriver.php b/typo3/sysext/core/Classes/Resource/Driver/LocalDriver.php
index 0114b94b181c..afb2ee6c9f33 100644
--- a/typo3/sysext/core/Classes/Resource/Driver/LocalDriver.php
+++ b/typo3/sysext/core/Classes/Resource/Driver/LocalDriver.php
@@ -131,7 +131,9 @@ class LocalDriver extends AbstractHierarchicalFilesystemDriver implements Stream
     {
         // only calculate baseURI if the storage does not enforce jumpUrl Script
         if ($this->hasCapability(ResourceStorage::CAPABILITY_PUBLIC)) {
-            if (GeneralUtility::isFirstPartOfStr($this->absoluteBasePath, Environment::getPublicPath())) {
+            if (!empty($this->configuration['baseUri'])) {
+                $this->baseUri = rtrim($this->configuration['baseUri'], '/') . '/';
+            } elseif (GeneralUtility::isFirstPartOfStr($this->absoluteBasePath, Environment::getPublicPath())) {
                 // use site-relative URLs
                 $temporaryBaseUri = rtrim(PathUtility::stripPathSitePrefix($this->absoluteBasePath), '/');
                 if ($temporaryBaseUri !== '') {
@@ -140,8 +142,6 @@ class LocalDriver extends AbstractHierarchicalFilesystemDriver implements Stream
                     $temporaryBaseUri = implode('/', $uriParts) . '/';
                 }
                 $this->baseUri = $temporaryBaseUri;
-            } elseif (isset($this->configuration['baseUri']) && GeneralUtility::isValidUrl($this->configuration['baseUri'])) {
-                $this->baseUri = rtrim($this->configuration['baseUri'], '/') . '/';
             }
         }
     }
@@ -186,7 +186,6 @@ class LocalDriver extends AbstractHierarchicalFilesystemDriver implements Stream
      *
      * @param string $identifier
      * @return string|null NULL if file is missing or deleted, the generated url otherwise
-     * @throws \TYPO3\CMS\Core\Resource\Exception
      */
     public function getPublicUrl($identifier)
     {
diff --git a/typo3/sysext/core/Configuration/Resource/Driver/LocalDriverFlexForm.xml b/typo3/sysext/core/Configuration/Resource/Driver/LocalDriverFlexForm.xml
index d14d8da65a36..3dca8eb70246 100644
--- a/typo3/sysext/core/Configuration/Resource/Driver/LocalDriverFlexForm.xml
+++ b/typo3/sysext/core/Configuration/Resource/Driver/LocalDriverFlexForm.xml
@@ -33,6 +33,16 @@
 					</config>
 				</TCEforms>
 			</pathType>
+			<baseUri>
+				<TCEforms>
+					<label>LLL:EXT:core/Resources/Private/Language/locallang_mod_file.xlf:sys_file_storage.localDriverFlexform_baseUri</label>
+					<config>
+						<type>input</type>
+						<placeholder>LLL:EXT:core/Resources/Private/Language/locallang_mod_file.xlf:sys_file_storage.localDriverFlexform_baseUri_placeholder</placeholder>
+						<size>30</size>
+					</config>
+				</TCEforms>
+			</baseUri>
 			<caseSensitive>
 				<TCEforms>
 					<label>LLL:EXT:core/Resources/Private/Language/locallang_mod_file.xlf:sys_file_storage.localDriverFlexform_caseSensitive</label>
diff --git a/typo3/sysext/core/Resources/Private/Language/locallang_mod_file.xlf b/typo3/sysext/core/Resources/Private/Language/locallang_mod_file.xlf
index c232af1fc3da..ec84426eab96 100644
--- a/typo3/sysext/core/Resources/Private/Language/locallang_mod_file.xlf
+++ b/typo3/sysext/core/Resources/Private/Language/locallang_mod_file.xlf
@@ -18,6 +18,12 @@
 			<trans-unit id="sys_file_storage.localDriverFlexform_basePath_placeholder" resname="sys_file_storage.localDriverFlexform_basePath_placeholder">
 				<source>Add storage path here. For example: myPath/folder/</source>
 			</trans-unit>
+			<trans-unit id="sys_file_storage.localDriverFlexform_baseUri" resname="sys_file_storage.localDriverFlexform_baseUri">
+				<source>Base URI</source>
+			</trans-unit>
+			<trans-unit id="sys_file_storage.localDriverFlexform_baseUri_placeholder" resname="sys_file_storage.localDriverFlexform_baseUri_placeholder">
+				<source>Use this URI to build the public url of files, instead of deriving the URI from base path</source>
+			</trans-unit>
 			<trans-unit id="sys_file_storage.localDriverFlexform_pathType" resname="sys_file_storage.localDriverFlexform_pathType">
 				<source>Path type</source>
 			</trans-unit>
diff --git a/typo3/sysext/core/Tests/Unit/Resource/Driver/LocalDriverTest.php b/typo3/sysext/core/Tests/Unit/Resource/Driver/LocalDriverTest.php
index 5b7ae29a3ff5..4ea00e03b52a 100644
--- a/typo3/sysext/core/Tests/Unit/Resource/Driver/LocalDriverTest.php
+++ b/typo3/sysext/core/Tests/Unit/Resource/Driver/LocalDriverTest.php
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException;
 use TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException;
 use TYPO3\CMS\Core\Resource\Exception\InvalidFileNameException;
 use TYPO3\CMS\Core\Resource\ResourceStorage;
+use TYPO3\CMS\Core\Resource\ResourceStorageInterface;
 use TYPO3\CMS\Core\Tests\Unit\Resource\BaseTestCase;
 use TYPO3\CMS\Core\Tests\Unit\Resource\Driver\Fixtures\LocalDriverFilenameFilter;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -41,6 +42,11 @@ class LocalDriverTest extends BaseTestCase
      */
     protected $resetSingletonInstances = true;
 
+    /**
+     * @var bool Reset changed Environment
+     */
+    protected $backupEnvironment = true;
+
     /**
      * @var LocalDriver
      */
@@ -181,6 +187,103 @@ class LocalDriverTest extends BaseTestCase
         self::assertStringNotContainsString('/../', $basePath);
     }
 
+    public function publicUrlIsCalculatedCorrectlyWithDifferentBasePathsAndBasUrisDataProvider(): array
+    {
+        return [
+            'no base uri, within public' => [
+                '/files/',
+                '',
+                '/foo.txt',
+                true,
+                'files/foo.txt',
+            ],
+            'no base uri, within project' => [
+                '/../files/',
+                '',
+                '/foo.txt',
+                false,
+                null,
+            ],
+            'base uri with host, within public' => [
+                '/files/',
+                'https://host.tld/',
+                '/foo.txt',
+                true,
+                'https://host.tld/foo.txt',
+            ],
+            'base uri with host, within project' => [
+                '/../files/',
+                'https://host.tld/',
+                '/foo.txt',
+                true,
+                'https://host.tld/foo.txt',
+            ],
+            'base uri with path only, within public' => [
+                '/files/',
+                'assets/',
+                '/foo.txt',
+                true,
+                'assets/foo.txt',
+            ],
+            'base uri with path only, within project' => [
+                '/../files/',
+                'assets/',
+                '/foo.txt',
+                true,
+                'assets/foo.txt',
+            ],
+            'base uri with path only, within other public dir' => [
+                '/../public/assets/',
+                'assets/',
+                '/foo.txt',
+                true,
+                'assets/foo.txt',
+            ],
+        ];
+    }
+
+    /**
+     * @param string $basePath
+     * @param string $baseUri
+     * @param string $fileName
+     * @param bool $expectedIsPublic
+     * @param string|null $expectedPublicUrl
+     * @test
+     * @dataProvider publicUrlIsCalculatedCorrectlyWithDifferentBasePathsAndBasUrisDataProvider
+     */
+    public function publicUrlIsCalculatedCorrectlyWithDifferentBasePathsAndBasUris(string $basePath, string $baseUri, string $fileName, bool $expectedIsPublic, ?string $expectedPublicUrl): void
+    {
+        $testDir = $this->createRealTestdir();
+        $projectPath = $testDir . '/app';
+        $publicPath = $projectPath . '/public';
+        $absoluteBaseDir = $publicPath . $basePath;
+        mkdir($projectPath);
+        mkdir($publicPath);
+        mkdir($absoluteBaseDir, 0777, true);
+        Environment::initialize(
+            Environment::getContext(),
+            true,
+            false,
+            $projectPath,
+            $publicPath,
+            Environment::getVarPath(),
+            Environment::getConfigPath(),
+            Environment::getCurrentScript(),
+            Environment::isUnix() ? 'UNIX' : 'WINDOWS'
+        );
+        $driverConfiguration = [
+            'pathType' => 'relative',
+            'basePath' => $basePath,
+            'baseUri' => $baseUri,
+        ];
+
+        $subject = $this->createDriver($driverConfiguration);
+
+        self::assertSame($expectedIsPublic, $subject->hasCapability(ResourceStorageInterface::CAPABILITY_PUBLIC));
+        self::assertSame($fileName, $subject->createFile($fileName, '/'));
+        self::assertSame($expectedPublicUrl, $subject->getPublicUrl($fileName));
+    }
+
     /**
      * @test
      */
-- 
GitLab