From a25eb69eef5abb14e637a5429d74081f1630b145 Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Mon, 7 Nov 2022 23:34:00 +0100
Subject: [PATCH] [FEATURE] Add TypoScript option to create absolute links and
 resource/image URLs

A new TypoScript option config.forceAbsoluteUrls = 1
is added, which allows to create ANY link to be absolute (pages
+ files etc), as well as all images, included assets
to have a full qualified domain name.

Setting this option will override any option in
config.absRefPrefix and any typolink.forceAbsoluteUrl = 0
options.

The main benefit is to create a fully static version of
the frontend. Ideally, config.absRefPrefix can then be phased
out and the config.absRefPrefix option
should be removed in favor of this new feature at a later stage.

Resolves: #87919
Releases: main
Change-Id: Ibbd3084a978c8a2a4ff803f9aef276679a1658f3
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/76463
Tested-by: core-ci <typo3@b13.com>
Tested-by: Susanne Moog <look@susi.dev>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Susanne Moog <look@susi.dev>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
---
 ...llowGenerationOfAbsoluteURLsCompletely.rst |  27 ++
 .../core/Tests/Functional/Fixtures/pages.csv  |  20 +-
 .../TypoScriptFrontendController.php          |   6 +
 .../Typolink/AbstractTypolinkBuilder.php      |  10 +-
 .../Classes/Typolink/PageLinkBuilder.php      |   7 +-
 .../AbsoluteUriPrefixRenderingTest.php        | 303 ++++++++++++++++++
 .../AbsoluteUriPrefixRenderingTest.typoscript |  63 ++++
 7 files changed, 423 insertions(+), 13 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.1/Feature-87919-AllowGenerationOfAbsoluteURLsCompletely.rst
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Rendering/AbsoluteUriPrefixRenderingTest.php
 create mode 100644 typo3/sysext/frontend/Tests/Functional/Rendering/Fixtures/AbsoluteUriPrefixRenderingTest.typoscript

diff --git a/typo3/sysext/core/Documentation/Changelog/12.1/Feature-87919-AllowGenerationOfAbsoluteURLsCompletely.rst b/typo3/sysext/core/Documentation/Changelog/12.1/Feature-87919-AllowGenerationOfAbsoluteURLsCompletely.rst
new file mode 100644
index 000000000000..4ca5e99019b2
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.1/Feature-87919-AllowGenerationOfAbsoluteURLsCompletely.rst
@@ -0,0 +1,27 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-87919-1667984808:
+
+==============================================================
+Feature: #87919 - Allow generation of absolute URLs completely
+==============================================================
+
+See :issue:`87919`
+
+Description
+===========
+
+A new TypoScript option :typoscript:`config.forceAbsoluteUrls = 1` is added.
+
+
+Impact
+======
+
+If set, all links, reference to images or assets, which previously were built with a relative
+or absolute path (e.g. :file:`/fileadmin/my-pdf.pdf`) are then rendered as absolute URLs
+with the site prefix / current domain. 
+
+Examples for such use-cases are the generation of a full static version of a TYPO3 site
+for sending a page via email.
+
+.. index:: TypoScript, ext:frontend
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/pages.csv b/typo3/sysext/core/Tests/Functional/Fixtures/pages.csv
index 82d90ae422e9..2a8fc1cbf145 100644
--- a/typo3/sysext/core/Tests/Functional/Fixtures/pages.csv
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/pages.csv
@@ -1,15 +1,15 @@
 pages,,,,,,,,,,,,,,,,
 ,uid,pid,title,deleted,slug,perms_everybody,fe_group,t3ver_oid,t3ver_wsid,t3ver_state,sys_language_uid,l10n_parent,doktype,mount_pid,mount_pid_ol,sorting
-,1,0,Root 1,0,/,15,0,0,0,0,0,0,0,0,0,16
-,2,1,Dummy 1-2,0,,15,0,0,0,0,0,0,0,0,0,16
-,3,1,Dummy 1-3,0,,15,0,0,0,0,0,0,0,0,0,32
-,4,1,Dummy 1-4,0,,15,0,0,0,0,0,0,0,0,0,64
-,5,2,Dummy 1-2-5,0,,15,0,0,0,0,0,0,0,0,0,16
-,6,2,Dummy 1-2-6,0,,15,-2,0,0,0,0,0,0,0,0,32
-,7,2,Dummy 1-2-7,0,,15,0,0,0,0,0,0,0,0,0,64
-,8,3,Dummy 1-3-8,0,,15,0,0,0,0,0,0,0,0,0,16
-,9,3,Dummy 1-3-9,0,,15,0,0,0,0,0,0,0,0,0,32
-,10,4,Dummy 1-4-10,0,,15,0,0,0,0,0,0,0,0,0,16
+,1,0,Root 1,0,"/",15,0,0,0,0,0,0,0,0,0,16
+,2,1,Dummy 1-2,0,"/dummy-1-2",15,0,0,0,0,0,0,0,0,0,16
+,3,1,Dummy 1-3,0,"/dummy-1-3",15,0,0,0,0,0,0,0,0,0,32
+,4,1,Dummy 1-4,0,"/dummy-1-4",15,0,0,0,0,0,0,0,0,0,64
+,5,2,Dummy 1-2-5,0,"/dummy-1-2-5",15,0,0,0,0,0,0,0,0,0,16
+,6,2,Dummy 1-2-6,0,"/dummy-1-2-6",15,-2,0,0,0,0,0,0,0,0,32
+,7,2,Dummy 1-2-7,0,"/dummy-1-2-7",15,0,0,0,0,0,0,0,0,0,64
+,8,3,Dummy 1-3-8,0,"/dummy-1-3-8",15,0,0,0,0,0,0,0,0,0,16
+,9,3,Dummy 1-3-9,0,"/dummy-1-2-9",15,0,0,0,0,0,0,0,0,0,32
+,10,4,Dummy 1-4-10,0,"/dummy-1-4-10",15,0,0,0,0,0,0,0,0,0,16
 ,11,0,Workspace Root,0,,15,0,0,987654321,1,0,0,0,0,0,16
 ,901,0,Wurzel 1,0,,0,0,0,0,0,1,1,0,0,0,16
 ,902,1,Attrappe 1-2,0,,0,0,0,0,0,1,2,0,0,0,16
diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
index bf16b0bd4de0..ee47832b6979 100644
--- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
+++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
@@ -2035,6 +2035,12 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                 $this->absRefPrefix = $normalizedParams->getSitePath();
             }
         }
+        // config.forceAbsoluteUrls will override absRefPrefix
+        if ($this->config['config']['forceAbsoluteUrls'] ?? false) {
+            $normalizedParams = $request->getAttribute('normalizedParams');
+            $this->absRefPrefix = $normalizedParams->getSiteUrl();
+        }
+
         // linkVars
         $this->calculateLinkVars($request->getQueryParams());
         // Setting XHTML-doctype from doctype
diff --git a/typo3/sysext/frontend/Classes/Typolink/AbstractTypolinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/AbstractTypolinkBuilder.php
index dc605717f6b4..38f84367456d 100644
--- a/typo3/sysext/frontend/Classes/Typolink/AbstractTypolinkBuilder.php
+++ b/typo3/sysext/frontend/Classes/Typolink/AbstractTypolinkBuilder.php
@@ -71,7 +71,13 @@ abstract class AbstractTypolinkBuilder
      */
     protected function forceAbsoluteUrl(string $url, array $configuration): string
     {
-        if (!empty($url) && !empty($configuration['forceAbsoluteUrl']) && preg_match('#^(?:([a-z]+)(://)([^/]*)/?)?(.*)$#', $url, $matches)) {
+        $tsfe = $this->getTypoScriptFrontendController();
+        if ($tsfe->config['config']['forceAbsoluteUrls'] ?? false) {
+            $forceAbsoluteUrl = true;
+        } else {
+            $forceAbsoluteUrl = !empty($configuration['forceAbsoluteUrl']);
+        }
+        if (!empty($url) && $forceAbsoluteUrl && preg_match('#^(?:([a-z]+)(://)([^/]*)/?)?(.*)$#', $url, $matches)) {
             $urlParts = [
                 'scheme' => $matches[1],
                 'delimiter' => '://',
@@ -92,7 +98,7 @@ abstract class AbstractTypolinkBuilder
                 // absRefPrefix has been prepended to $url beforehand
                 // so we only modify the path if no absRefPrefix has been set
                 // otherwise we would destroy the path
-                if ($this->getTypoScriptFrontendController()->absRefPrefix === '') {
+                if ($tsfe->absRefPrefix === '') {
                     $urlParts['path'] = $normalizedParams->getSitePath() . ltrim($urlParts['path'], '/');
                 }
                 $isUrlModified = true;
diff --git a/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php
index 215de7bcf22d..940a84fea1a7 100644
--- a/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php
+++ b/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php
@@ -469,7 +469,12 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
 
         // By default, it is assumed to ab an internal link or current domain's linking scheme should be used
         // Use the config option to override this.
-        $useAbsoluteUrl = $conf['forceAbsoluteUrl'] ?? false;
+        // Global option config.forceAbsoluteUrls = 1 overrides any setting for this specific link
+        if ($tsfe->config['config']['forceAbsoluteUrls'] ?? false) {
+            $useAbsoluteUrl = true;
+        } else {
+            $useAbsoluteUrl = $conf['forceAbsoluteUrl'] ?? false;
+        }
         // Check if the current page equal to the site of the target page, now only set the absolute URL
         // Always generate absolute URLs if no current site is set
         if (
diff --git a/typo3/sysext/frontend/Tests/Functional/Rendering/AbsoluteUriPrefixRenderingTest.php b/typo3/sysext/frontend/Tests/Functional/Rendering/AbsoluteUriPrefixRenderingTest.php
new file mode 100644
index 000000000000..00e335765ff5
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Rendering/AbsoluteUriPrefixRenderingTest.php
@@ -0,0 +1,303 @@
+<?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\Frontend\Tests\Functional\Rendering;
+
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class AbsoluteUriPrefixRenderingTest extends FunctionalTestCase
+{
+    use SiteBasedTestTrait;
+
+    /**
+     * @var string[]
+     */
+    private array $definedResources = [
+        'absoluteCSS' => '/typo3/sysext/backend/Resources/Public/Css/backend.css',
+        'relativeCSS' => 'typo3/sysext/backend/Resources/Public/Css/backend.css',
+        'extensionCSS' => 'EXT:rte_ckeditor/Resources/Public/Css/contents.css',
+        'externalCSS' => 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css',
+        'absoluteJS' => '/typo3/sysext/backend/Resources/Public/JavaScript/backend.js',
+        'relativeJS' => 'typo3/sysext/core/Resources/Public/JavaScript/Contrib/autosize.js',
+        'extensionJS' => 'EXT:core/Resources/Public/JavaScript/Contrib/jquery.js',
+        'externalJS' => 'https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js',
+        'localImage' => 'typo3/sysext/frontend/Resources/Public/Icons/Extension.svg',
+    ];
+
+    /**
+     * @var string[]
+     */
+    private array $resolvedResources = [
+        'relativeCSS' => 'typo3/sysext/backend/Resources/Public/Css/backend.css',
+        'extensionCSS' => 'typo3/sysext/rte_ckeditor/Resources/Public/Css/contents.css',
+        'externalCSS' => 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css',
+        'relativeJS' => 'typo3/sysext/core/Resources/Public/JavaScript/Contrib/autosize.js',
+        'extensionJS' => 'typo3/sysext/core/Resources/Public/JavaScript/Contrib/jquery.js',
+        'externalJS' => 'https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js',
+        'localImage' => 'typo3/sysext/frontend/Resources/Public/Icons/Extension.svg',
+        'link' => '/en/dummy-1-4-10',
+    ];
+
+    protected array $coreExtensionsToLoad = ['rte_ckeditor'];
+
+    protected const LANGUAGE_PRESETS = [
+        'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8', 'iso' => 'en', 'hrefLang' => 'en-US'],
+    ];
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->importCsvDataSet(__DIR__ . '/../../../../core/Tests/Functional/Fixtures/pages.csv');
+        $this->writeSiteConfiguration(
+            'test',
+            $this->buildSiteConfiguration(1, 'http://localhost/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', '/en/'),
+            ],
+            $this->buildErrorHandlingConfiguration('Fluid', [404]),
+        );
+        $this->setUpFrontendRootPage(
+            1,
+            ['EXT:frontend/Tests/Functional/Rendering/Fixtures/AbsoluteUriPrefixRenderingTest.typoscript']
+        );
+        $this->setTypoScriptConstantsToTemplateRecord(
+            1,
+            $this->compileTypoScriptConstants($this->definedResources)
+        );
+    }
+
+    public function urisAreRenderedUsingForceAbsoluteUrlsDataProvider(): \Generator
+    {
+        // no compression settings
+        yield 'none - none' => [
+            'none', 'none',
+            [
+                'absolute' => '"/{{CANDIDATE}}"',
+                'local' => '"/{{CANDIDATE}}"',
+                'relative' => '"/{{CANDIDATE}}\?\d+"',
+                'extension' => '"/{{CANDIDATE}}\?\d+"',
+                'external' => '"{{CANDIDATE}}"',
+                'link' => 'href="{{CANDIDATE}}"',
+            ],
+        ];
+        yield 'with-prefix - none' => [
+            '1', 'none',
+            [
+                'absolute' => '"http://localhost/{{CANDIDATE}}"',
+                'local' => '"http://localhost/{{CANDIDATE}}"',
+                'relative' => '"http://localhost/{{CANDIDATE}}\?\d+"',
+                'extension' => '"http://localhost/{{CANDIDATE}}\?\d+"',
+                'external' => '"{{CANDIDATE}}"',
+                'link' => 'href="http://localhost{{CANDIDATE}}"',
+            ],
+        ];
+        // concatenation
+        yield 'none - concatenate' => [
+            '0', 'concatenate',
+            [
+                '!absolute' => '{{CANDIDATE}}',
+                '!relative' => '{{CANDIDATE}}',
+                '!extension' => '{{CANDIDATE}}',
+                'absolute' => '"/typo3temp/assets/compressed/merged-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'local' => '"/{{CANDIDATE}}"',
+                'relative' => '"/typo3temp/assets/compressed/merged-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'extension' => '"/typo3temp/assets/compressed/merged-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'external' => '"{{CANDIDATE}}"',
+                'link' => 'href="{{CANDIDATE}}"',
+            ],
+        ];
+        yield 'with-prefix - concatenate' => [
+            '1', 'concatenate',
+            [
+                '!absolute' => 'http://localhost/{{CANDIDATE}}',
+                '!relative' => 'http://localhost/{{CANDIDATE}}',
+                '!extension' => 'http://localhost/{{CANDIDATE}}',
+                'absolute' => '"http://localhost/typo3temp/assets/compressed/merged-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'local' => '"http://localhost/{{CANDIDATE}}"',
+                'relative' => '"http://localhost/typo3temp/assets/compressed/merged-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'extension' => '"http://localhost/typo3temp/assets/compressed/merged-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'external' => '"{{CANDIDATE}}"',
+                'link' => 'href="http://localhost{{CANDIDATE}}"',
+            ],
+        ];
+        // compression
+        yield 'none - compress' => [
+            '0', 'compress',
+            [
+                '!absolute' => '{{CANDIDATE}}',
+                '!relative' => '/{{CANDIDATE}}',
+                '!extension' => '/{{CANDIDATE}}',
+                'absolute' => '"/typo3temp/assets/compressed/{{CANDIDATE-FILENAME}}-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'local' => '"/{{CANDIDATE}}"',
+                'relative' => '"/typo3temp/assets/compressed/{{CANDIDATE-FILENAME}}-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'extension' => '"/typo3temp/assets/compressed/{{CANDIDATE-FILENAME}}-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'external' => '"{{CANDIDATE}}"',
+                'link' => 'href="{{CANDIDATE}}"',
+            ],
+        ];
+        yield 'with-prefix - compress' => [
+            '1', 'compress',
+            [
+                '!absolute' => 'http://localhost/{{CANDIDATE}}',
+                '!relative' => 'http://localhost/{{CANDIDATE}}',
+                '!extension' => 'http://localhost/{{CANDIDATE}}',
+                'absolute' => '"http://localhost/typo3temp/assets/compressed/{{CANDIDATE-FILENAME}}-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'local' => '"http://localhost/{{CANDIDATE}}"',
+                'relative' => '"http://localhost/typo3temp/assets/compressed/{{CANDIDATE-FILENAME}}-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'extension' => '"http://localhost/typo3temp/assets/compressed/{{CANDIDATE-FILENAME}}-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'external' => '"{{CANDIDATE}}"',
+                'link' => 'href="http://localhost{{CANDIDATE}}"',
+            ],
+        ];
+        // concatenation & compression
+        yield 'no prefix - concatenate-and-compress' => [
+            '0', 'concatenate-and-compress',
+            [
+                '!absolute' => '{{CANDIDATE}}',
+                '!relative' => '/{{CANDIDATE}}',
+                '!extension' => '/{{CANDIDATE}}',
+                'absolute' => '"/typo3temp/assets/compressed/merged-[a-z0-9]+-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'local' => '"/{{CANDIDATE}}"',
+                'relative' => '"/typo3temp/assets/compressed/merged-[a-z0-9]+-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'extension' => '"/typo3temp/assets/compressed/merged-[a-z0-9]+-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'external' => '"{{CANDIDATE}}"',
+                'link' => 'href="{{CANDIDATE}}"',
+            ],
+        ];
+        yield 'with prefix - concatenate-and-compress' => [
+            '1', 'concatenate-and-compress',
+            [
+                '!absolute' => 'http://localhost/{{CANDIDATE}}',
+                '!relative' => 'http://localhost/{{CANDIDATE}}',
+                '!extension' => 'http://localhost/{{CANDIDATE}}',
+                'absolute' => '"http://localhost/typo3temp/assets/compressed/merged-[a-z0-9]+-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'local' => '"http://localhost/{{CANDIDATE}}"',
+                'relative' => '"http://localhost/typo3temp/assets/compressed/merged-[a-z0-9]+-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'extension' => '"http://localhost/typo3temp/assets/compressed/merged-[a-z0-9]+-[a-z0-9]+\.{{CANDIDATE-EXTENSION}}\?\d+"',
+                'external' => '"{{CANDIDATE}}"',
+                'link' => 'href="http://localhost{{CANDIDATE}}"',
+            ],
+        ];
+    }
+
+    /**
+     * @param string $useAbsoluteUrls
+     * @param string $compressorAspect
+     * @param array $expectations
+     * @test
+     * @dataProvider urisAreRenderedUsingForceAbsoluteUrlsDataProvider
+     */
+    public function urisAreRenderedUsingAbsRefPrefix(string $useAbsoluteUrls, string $compressorAspect, array $expectations): void
+    {
+        $response = $this->executeFrontendSubRequest(
+            (new InternalRequest())->withQueryParameters([
+                'id' => 1,
+                'useAbsoluteUrls' => $useAbsoluteUrls,
+                'testCompressor' => $compressorAspect,
+            ])
+        );
+        $content = (string)$response->getBody();
+
+        foreach ($expectations as $type => $expectation) {
+            $shallExist = true;
+            if (str_starts_with($type, '!')) {
+                $shallExist = false;
+                $type = substr($type, 1);
+            }
+            $candidates = array_map(
+                function (string $candidateKey) {
+                    return $this->resolvedResources[$candidateKey];
+                },
+                array_filter(
+                    array_keys($this->resolvedResources),
+                    static function (string $candidateKey) use ($type) {
+                        return str_starts_with($candidateKey, $type);
+                    }
+                )
+            );
+            foreach ($candidates as $candidate) {
+                $pathInfo = pathinfo($candidate);
+                $pattern = str_replace(
+                    [
+                        '{{CANDIDATE}}',
+                        '{{CANDIDATE-FILENAME}}',
+                        '{{CANDIDATE-EXTENSION}}',
+                    ],
+                    [
+                        preg_quote($candidate, '#'),
+                        preg_quote($pathInfo['filename'], '#'),
+                        preg_quote($pathInfo['extension'] ?? '', '#'),
+                    ],
+                    $expectation
+                );
+
+                if ($shallExist) {
+                    self::assertMatchesRegularExpression(
+                        '#' . $pattern . '#',
+                        $content
+                    );
+                } else {
+                    self::assertDoesNotMatchRegularExpression(
+                        '#' . $pattern . '#',
+                        $content
+                    );
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds TypoScript constants snippet to the existing template record
+     *
+     * @param int $pageId
+     * @param string $constants
+     * @param bool $append
+     */
+    protected function setTypoScriptConstantsToTemplateRecord(int $pageId, string $constants, bool $append = false): void
+    {
+        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_template');
+
+        $template = $connection->select(['uid', 'constants'], 'sys_template', ['pid' => $pageId, 'root' => 1])->fetchAssociative();
+        if (empty($template)) {
+            self::fail('Cannot find root template on page with id: "' . $pageId . '"');
+        }
+        $updateFields = [];
+        $updateFields['constants'] = ($append ? $template['constants'] . LF : '') . $constants;
+        $connection->update(
+            'sys_template',
+            $updateFields,
+            ['uid' => $template['uid']]
+        );
+    }
+
+    /**
+     * @param array $constants
+     * @return string
+     */
+    protected function compileTypoScriptConstants(array $constants): string
+    {
+        $lines = [];
+        foreach ($constants as $constantName => $constantValue) {
+            $lines[] = $constantName . ' = ' . $constantValue;
+        }
+        return implode(PHP_EOL, $lines);
+    }
+}
diff --git a/typo3/sysext/frontend/Tests/Functional/Rendering/Fixtures/AbsoluteUriPrefixRenderingTest.typoscript b/typo3/sysext/frontend/Tests/Functional/Rendering/Fixtures/AbsoluteUriPrefixRenderingTest.typoscript
new file mode 100644
index 000000000000..1825a4f4fb8c
--- /dev/null
+++ b/typo3/sysext/frontend/Tests/Functional/Rendering/Fixtures/AbsoluteUriPrefixRenderingTest.typoscript
@@ -0,0 +1,63 @@
+page = PAGE
+page {
+  includeCSS {
+    absoluteCSS = {$absoluteCSS}
+    relativeCSS = {$relativeCSS}
+    extensionCSS = {$extensionCSS}
+    externalCSS = {$externalCSS}
+    externalCSS.external = 1
+    externalCSS.excludeFromConcatenation = 1
+    externalCSS.disableCompression = 1
+  }
+
+  includeJS {
+    absoluteJS = {$absoluteJS}
+    relativeJS = {$relativeJS}
+    extensionJS = {$extensionJS}
+    externalJS = {$externalJS}
+    externalJS.external = 1
+    externalJS.excludeFromConcatenation = 1
+    externalJS.disableCompression = 1
+  }
+  # added so we can also test the inclusion of these types in FE
+  includeCSSLibs {
+    relativeCSS = {$relativeCSS}
+  }
+  includeJSLibs {
+    relativeJS = {$relativeJS}
+  }
+  includeJSFooterlibs {
+    relativeJS = {$relativeJS}
+  }
+  10 = TEXT
+  10.value = Link to page
+  10.typolink.parameter = 10
+  20 = IMAGE
+  20 {
+    file = {$localImage}
+  }
+}
+
+[request.getQueryParams()['useAbsoluteUrls'] == '1']
+  config.forceAbsoluteUrls = 1
+[GLOBAL]
+
+[request.getQueryParams()['testCompressor'] == 'concatenate' || request.getParsedBody()['testCompressor'] == 'concatenate']
+  config {
+    concatenateCss = 1
+    concatenateJs = 1
+  }
+[request.getQueryParams()['testCompressor'] == 'compress' || request.getParsedBody()['testCompressor'] == 'compress']
+  config {
+    compressCss = 1
+    compressJs = 1
+  }
+[request.getQueryParams()['testCompressor'] == 'concatenate-and-compress' || request.getParsedBody()['testCompressor'] == 'concatenate-and-compress']
+  config {
+    concatenateCss = 1
+    concatenateJs = 1
+    compressCss = 1
+    compressJs = 1
+    concatenateJsAndCss = 1
+  }
+[end]
-- 
GitLab