From 5bff334aa53f701521aa5c58b861f177966ef9c2 Mon Sep 17 00:00:00 2001
From: Helmut Hummel <typo3@helhum.io>
Date: Tue, 31 Aug 2021 15:33:55 +0200
Subject: [PATCH] [TASK] Add a PathUtility method to check for host-absolute
 URLs

This replaces a lot of string-based comparisons spread around
the code, which have been even wrong.
Having a single place for this kind of checks is beneficial.

The method is added to PathUtility as similar methods are already
present there.

Releases: master
Resolves: #95121
Change-Id: Ic73aac5d8c758dee5e0424bf2d54cd62b4b09e0c
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/70913
Tested-by: core-ci <typo3@b13.com>
Tested-by: Markus Klein <markus.klein@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Markus Klein <markus.klein@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 .../Classes/Resource/PublicUrlPrefixer.php    |  3 +-
 .../Classes/Template/ModuleTemplate.php       |  2 +-
 .../Cache/Backend/SimpleFileBackend.php       |  2 +-
 .../core/Classes/LinkHandling/LinkService.php |  4 +--
 .../core/Classes/Log/Writer/FileWriter.php    |  2 +-
 .../core/Classes/Page/AssetRenderer.php       |  2 +-
 .../core/Classes/Utility/PathUtility.php      | 20 ++++++++++++-
 .../Tests/Unit/Utility/PathUtilityTest.php    | 28 +++++++++++++++++++
 .../Classes/Evaluation/SourceHost.php         |  5 +++-
 9 files changed, 59 insertions(+), 9 deletions(-)

diff --git a/typo3/sysext/backend/Classes/Resource/PublicUrlPrefixer.php b/typo3/sysext/backend/Classes/Resource/PublicUrlPrefixer.php
index 4f21c9b42024..2dfeeef90022 100644
--- a/typo3/sysext/backend/Classes/Resource/PublicUrlPrefixer.php
+++ b/typo3/sysext/backend/Classes/Resource/PublicUrlPrefixer.php
@@ -20,6 +20,7 @@ namespace TYPO3\CMS\Backend\Resource;
 use TYPO3\CMS\Core\Resource\Event\GeneratePublicUrlForResourceEvent;
 use TYPO3\CMS\Core\Resource\ResourceInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\PathUtility;
 
 class PublicUrlPrefixer
 {
@@ -39,7 +40,7 @@ class PublicUrlPrefixer
         try {
             $resource = $event->getResource();
             $originalUrl = $event->getStorage()->getPublicUrl($resource);
-            if (!$originalUrl || strpos($originalUrl, '://') !== false) {
+            if (!$originalUrl || PathUtility::hasProtocolAndScheme($originalUrl)) {
                 return;
             }
             $event->setPublicUrl(GeneralUtility::getIndpEnv('TYPO3_SITE_PATH') . $originalUrl);
diff --git a/typo3/sysext/backend/Classes/Template/ModuleTemplate.php b/typo3/sysext/backend/Classes/Template/ModuleTemplate.php
index 7efe52a0cdde..d94fa4facda9 100644
--- a/typo3/sysext/backend/Classes/Template/ModuleTemplate.php
+++ b/typo3/sysext/backend/Classes/Template/ModuleTemplate.php
@@ -676,7 +676,7 @@ class ModuleTemplate
      */
     protected function getUriForFileName($filename)
     {
-        if (strpos($filename, '://')) {
+        if (PathUtility::hasProtocolAndScheme($filename)) {
             return $filename;
         }
         $urlPrefix = '';
diff --git a/typo3/sysext/core/Classes/Cache/Backend/SimpleFileBackend.php b/typo3/sysext/core/Classes/Cache/Backend/SimpleFileBackend.php
index 8bd48d302fd7..87a26ebc7336 100644
--- a/typo3/sysext/core/Classes/Cache/Backend/SimpleFileBackend.php
+++ b/typo3/sysext/core/Classes/Cache/Backend/SimpleFileBackend.php
@@ -116,7 +116,7 @@ class SimpleFileBackend extends AbstractBackend implements PhpCapableBackendInte
     {
         // Skip handling if directory is a stream resource
         // This is used by unit tests with vfs:// directories
-        if (strpos($cacheDirectory, '://')) {
+        if (PathUtility::hasProtocolAndScheme($cacheDirectory)) {
             $this->temporaryCacheDirectory = $cacheDirectory;
             return;
         }
diff --git a/typo3/sysext/core/Classes/LinkHandling/LinkService.php b/typo3/sysext/core/Classes/LinkHandling/LinkService.php
index 79e68a9f0bf6..0f55a6054f27 100644
--- a/typo3/sysext/core/Classes/LinkHandling/LinkService.php
+++ b/typo3/sysext/core/Classes/LinkHandling/LinkService.php
@@ -21,7 +21,7 @@ use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
 use TYPO3\CMS\Core\LinkHandling\Exception\UnknownUrnException;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Core\Utility\PathUtility;
 
 /**
  * Class LinkService, responsible to find what kind of resource (type) is used
@@ -126,7 +126,7 @@ class LinkService implements SingletonInterface
             if ($fragment) {
                 $result['fragment'] = $fragment;
             }
-        } elseif ((strpos($urn, '://') || StringUtility::beginsWith($urn, '//')) && $this->handlers[self::TYPE_URL]) {
+        } elseif ($this->handlers[self::TYPE_URL] && PathUtility::hasProtocolAndScheme($urn)) {
             $result = $this->handlers[self::TYPE_URL]->resolveHandlerData(['url' => $urn]);
             $result['type'] = self::TYPE_URL;
         } elseif (stripos($urn, 'mailto:') === 0 && $this->handlers[self::TYPE_EMAIL]) {
diff --git a/typo3/sysext/core/Classes/Log/Writer/FileWriter.php b/typo3/sysext/core/Classes/Log/Writer/FileWriter.php
index 54adfd0217d6..4447f0eb018d 100644
--- a/typo3/sysext/core/Classes/Log/Writer/FileWriter.php
+++ b/typo3/sysext/core/Classes/Log/Writer/FileWriter.php
@@ -100,7 +100,7 @@ class FileWriter extends AbstractWriter
     {
         $logFile = $relativeLogFile;
         // Skip handling if logFile is a stream resource. This is used by unit tests with vfs:// directories
-        if (false === strpos($logFile, '://') && !PathUtility::isAbsolutePath($logFile)) {
+        if (!PathUtility::hasProtocolAndScheme($logFile) && !PathUtility::isAbsolutePath($logFile)) {
             $logFile = GeneralUtility::getFileAbsFileName($logFile);
             if (empty($logFile)) {
                 throw new InvalidLogWriterConfigurationException(
diff --git a/typo3/sysext/core/Classes/Page/AssetRenderer.php b/typo3/sysext/core/Classes/Page/AssetRenderer.php
index 79cbcc3ecd31..ddf39a27129c 100644
--- a/typo3/sysext/core/Classes/Page/AssetRenderer.php
+++ b/typo3/sysext/core/Classes/Page/AssetRenderer.php
@@ -113,7 +113,7 @@ class AssetRenderer
 
     private function getAbsoluteWebPath(string $file): string
     {
-        if (strpos($file, '://') !== false || strpos($file, '//') === 0) {
+        if (PathUtility::hasProtocolAndScheme($file)) {
             return $file;
         }
         $file = PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName($file));
diff --git a/typo3/sysext/core/Classes/Utility/PathUtility.php b/typo3/sysext/core/Classes/Utility/PathUtility.php
index 0af7baba9225..b7ef423b489b 100644
--- a/typo3/sysext/core/Classes/Utility/PathUtility.php
+++ b/typo3/sysext/core/Classes/Utility/PathUtility.php
@@ -49,7 +49,7 @@ class PathUtility
                     $targetPath = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH') . $targetPath;
                 }
             }
-        } elseif (strpos($targetPath, '://') !== false) {
+        } elseif (static::hasProtocolAndScheme($targetPath)) {
             return $targetPath;
         } elseif (file_exists(Environment::getPublicPath() . '/' . $targetPath)) {
             if (!Environment::isCli()) {
@@ -416,4 +416,22 @@ class PathUtility
     {
         return substr($path, strlen(Environment::getPublicPath() . '/'));
     }
+
+    /**
+     * Tries to guess whether a given URL hast protocol and (optional) scheme.
+     * Scheme relative URLs match as well.
+     * Current implementation is two simple string operations.
+     *
+     * This is just a guess. For a more detailed validation and parsing,
+     * use \TYPO3\CMS\Core\Utility\GeneralUtility::isValidUrl()
+     *
+     * @param string $path
+     * @return bool
+     *
+     * @internal
+     */
+    public static function hasProtocolAndScheme(string $path): bool
+    {
+        return strpos($path, '//') === 0 || strpos($path, '://') > 0;
+    }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Utility/PathUtilityTest.php b/typo3/sysext/core/Tests/Unit/Utility/PathUtilityTest.php
index 5fbd871c7b6e..86a4e48bfd2e 100644
--- a/typo3/sysext/core/Tests/Unit/Utility/PathUtilityTest.php
+++ b/typo3/sysext/core/Tests/Unit/Utility/PathUtilityTest.php
@@ -555,4 +555,32 @@ class PathUtilityTest extends UnitTestCase
 
         self::assertSame($expectedResult, PathUtility::isAbsolutePath($inputPath));
     }
+
+    public function hasProtocolAndSchemeDataProvider(): array
+    {
+        return [
+            ['//example.com/demo.html', true],
+            ['http://example.com/demo.html', true],
+            ['http://example.com/demo.html', true],
+            ['https://example.com/demo.html', true],
+            ['f://example.com/demo.html', true],
+            ['f:/example.com/demo.html', false],
+            ['://example.com/demo.html', false],
+            [':/example.com/demo.html', false],
+            ['/example.com/demo.html', false],
+            ['example.com/demo.html', false],
+            ['demo.html', false],
+            ['\\\\server\\unc-path\\demo.html', false],
+            ['\\\\example.com\\demo.html', false],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider hasProtocolAndSchemeDataProvider
+     */
+    public function hasProtocolAndScheme(string $url, bool $result)
+    {
+        self::assertSame($result, PathUtility::hasProtocolAndScheme($url));
+    }
 }
diff --git a/typo3/sysext/redirects/Classes/Evaluation/SourceHost.php b/typo3/sysext/redirects/Classes/Evaluation/SourceHost.php
index f1845b1b9e75..ffe706c243ea 100644
--- a/typo3/sysext/redirects/Classes/Evaluation/SourceHost.php
+++ b/typo3/sysext/redirects/Classes/Evaluation/SourceHost.php
@@ -17,9 +17,12 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Redirects\Evaluation;
 
+use TYPO3\CMS\Core\Utility\PathUtility;
+
 /**
  * Class SourceHost
  * Triggered from DataHandler as TCA formevals hook for validation / sanitation of domain values.
+ *
  * @internal
  */
 class SourceHost
@@ -54,7 +57,7 @@ class SourceHost
         }
 
         // 2) Check if value contains a protocol like http:// https:// etc...
-        if (strpos($value, '://') !== false) {
+        if (PathUtility::hasProtocolAndScheme($value)) {
             $tmp = $this->parseUrl($value);
             if (!empty($tmp)) {
                 return $tmp;
-- 
GitLab