Skip to content

Commit

Permalink
[BUGFIX] Adjust CSS import paths in inline CSS
Browse files Browse the repository at this point in the history
Extracts CSS path fixing functionality of ResourceCompressor
to a separate class in order to be used in other contexts.
PageRenderer uses the new object.

Resolves: #91935
Releases: master, 10.4
Change-Id: Icba71dcd0ceb110fb2398b2839310fb1952d0601
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/65720
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
  • Loading branch information
Alexander Künzl authored and bmack committed Sep 14, 2020
1 parent 39e75ef commit 3cbdf32
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 53 deletions.
10 changes: 8 additions & 2 deletions typo3/sysext/core/Classes/Page/PageRenderer.php
Expand Up @@ -24,6 +24,7 @@
use TYPO3\CMS\Core\Localization\LocalizationFactory;
use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Resource\RelativeCssPathFixer;
use TYPO3\CMS\Core\Resource\ResourceCompressor;
use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
use TYPO3\CMS\Core\SingletonInterface;
Expand Down Expand Up @@ -2694,13 +2695,18 @@ protected function executePostRenderHook(&$jsLibs, &$jsFiles, &$jsFooterFiles, &
protected function createInlineCssTagFromFile(string $file, array $properties): string
{
$cssInline = file_get_contents($file);

$cssInlineFix = $this->getPathFixer()->fixRelativeUrlPaths($cssInline, '/' . PathUtility::dirname($file) . '/');
return '<style type="text/css"'
. ' media="' . htmlspecialchars($properties['media']) . '"'
. ($properties['title'] ? ' title="' . htmlspecialchars($properties['title']) . '"' : '')
. '>' . LF
. '/*<![CDATA[*/' . LF . '<!-- ' . LF
. $cssInline
. $cssInlineFix
. '-->' . LF . '/*]]>*/' . LF . '</style>' . LF;
}

protected function getPathFixer(): RelativeCssPathFixer
{
return GeneralUtility::makeInstance(RelativeCssPathFixer::class);
}
}
82 changes: 82 additions & 0 deletions typo3/sysext/core/Classes/Resource/RelativeCssPathFixer.php
@@ -0,0 +1,82 @@
<?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\Core\Resource;

use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
* This fixes import paths in CSS files if their location changes,
* e.g. when inlining or compressing css
*
* @internal This class is not part of the TYPO3 API.
*/
class RelativeCssPathFixer
{
/**
* Fixes the relative paths inside of url() references in CSS files
*
* @param string $contents Data to process
* @param string $newDir directory referenced from current location
* @return string Processed data
*/
public function fixRelativeUrlPaths(string $contents, string $newDir): string
{
// Replace "url()" paths
if (stripos($contents, 'url') !== false) {
$regex = '/url(\\(\\s*["\']?(?!\\/)([^"\']+)["\']?\\s*\\))/iU';
$contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '(\'|\')');
}
// Replace "@import" paths
if (stripos($contents, '@import') !== false) {
$regex = '/@import\\s*(["\']?(?!\\/)([^"\']+)["\']?)/i';
$contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '"|"');
}
return $contents;
}

/**
* Finds and replaces all URLs by using a given regex
*
* @param string $contents Data to process
* @param string $regex Regex used to find URLs in content
* @param string $newDir Path to prepend to the original file
* @param string $wrap Wrap around replaced values
* @return string Processed data
*/
protected function findAndReplaceUrlPathsByRegex(string $contents, string $regex, string $newDir, string $wrap = '|'): string
{
$matches = [];
$replacements = [];
$wrapParts = explode('|', $wrap);
preg_match_all($regex, $contents, $matches);
foreach ($matches[2] as $matchCount => $match) {
// remove '," or white-spaces around
$match = trim($match, '\'" ');
// we must not rewrite paths containing ":" or "url(", e.g. data URIs (see RFC 2397)
if (strpos($match, ':') === false && !preg_match('/url\\s*\\(/i', $match)) {
$newPath = GeneralUtility::resolveBackPath($newDir . $match);
$replacements[$matches[1][$matchCount]] = $wrapParts[0] . $newPath . $wrapParts[1];
}
}
// replace URL paths in content
if (!empty($replacements)) {
$contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
}
return $contents;
}
}
64 changes: 13 additions & 51 deletions typo3/sysext/core/Classes/Resource/ResourceCompressor.php
Expand Up @@ -288,7 +288,7 @@ protected function createMergedFile(array $filesToInclude, $type = 'css')
}
// only fix paths if files aren't already in typo3temp (already processed)
if ($type === 'css' && !GeneralUtility::isFirstPartOfStr($filename, $this->targetDirectory)) {
$contents = $this->cssFixRelativeUrlPaths($contents, PathUtility::dirname($filename) . '/');
$contents = $this->cssFixRelativeUrlPaths($contents, $filename);
}
$concatenated .= LF . $contents;
}
Expand Down Expand Up @@ -355,7 +355,7 @@ public function compressCssFile($filename)
if (!file_exists(Environment::getPublicPath() . '/' . $targetFile) || $this->createGzipped && !file_exists(Environment::getPublicPath() . '/' . $targetFile . '.gzip')) {
$contents = $this->compressCssString(file_get_contents($filenameAbsolute));
if (strpos($filename, $this->targetDirectory) === false) {
$contents = $this->cssFixRelativeUrlPaths($contents, PathUtility::dirname($filename) . '/');
$contents = $this->cssFixRelativeUrlPaths($contents, $filename);
}
$this->writeFileAndCompressed($targetFile, $contents);
}
Expand Down Expand Up @@ -494,57 +494,14 @@ protected function checkBaseDirectory($filename, array $baseDirectories)
}

/**
* Fixes the relative paths inside of url() references in CSS files
*
* @param string $contents Data to process
* @param string $oldDir Directory of the original file, relative to TYPO3_mainDir
* @return string Processed data
*/
protected function cssFixRelativeUrlPaths($contents, $oldDir)
{
$newDir = '../../../' . $oldDir;
// Replace "url()" paths
if (stripos($contents, 'url') !== false) {
$regex = '/url(\\(\\s*["\']?(?!\\/)([^"\']+)["\']?\\s*\\))/iU';
$contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '(\'|\')');
}
// Replace "@import" paths
if (stripos($contents, '@import') !== false) {
$regex = '/@import\\s*(["\']?(?!\\/)([^"\']+)["\']?)/i';
$contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '"|"');
}
return $contents;
}

/**
* Finds and replaces all URLs by using a given regex
*
* @param string $contents Data to process
* @param string $regex Regex used to find URLs in content
* @param string $newDir Path to prepend to the original file
* @param string $wrap Wrap around replaced values
* @return string Processed data
* @param string $contents
* @param string $filename
* @return string
*/
protected function findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, $wrap = '|')
protected function cssFixRelativeUrlPaths(string $contents, string $filename): string
{
$matches = [];
$replacements = [];
$wrap = explode('|', $wrap);
preg_match_all($regex, $contents, $matches);
foreach ($matches[2] as $matchCount => $match) {
// remove '," or white-spaces around
$match = trim($match, '\'" ');
// we must not rewrite paths containing ":" or "url(", e.g. data URIs (see RFC 2397)
if (strpos($match, ':') === false && !preg_match('/url\\s*\\(/i', $match)) {
$newPath = GeneralUtility::resolveBackPath($newDir . $match);
$replacements[$matches[1][$matchCount]] = $wrap[0] . $newPath . $wrap[1];
}
}
// replace URL paths in content
if (!empty($replacements)) {
$contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
}
return $contents;
$newDir = '../../../' . PathUtility::dirname($filename) . '/';
return $this->getPathFixer()->fixRelativeUrlPaths($contents, $newDir);
}

/**
Expand Down Expand Up @@ -721,4 +678,9 @@ protected function getJavaScriptFileType(): string
}
return 'text/javascript';
}

protected function getPathFixer(): RelativeCssPathFixer
{
return GeneralUtility::makeInstance(RelativeCssPathFixer::class);
}
}
95 changes: 95 additions & 0 deletions typo3/sysext/core/Tests/Unit/Resource/RelativeCssPathFixerTest.php
@@ -0,0 +1,95 @@
<?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\Core\Tests\Unit\Resource;

use TYPO3\CMS\Core\Resource\RelativeCssPathFixer;

/**
* Testcase for the RelativeCssPathFixer class
*/
class RelativeCssPathFixerTest extends BaseTestCase
{
/**
* @var bool Restore Environment after tests
*/
protected $backupEnvironment = true;

/**
* @return array
*/
public function fixRelativeUrlPathsDataProvider(): array
{
return [
'@import from fileadmin with relative' => [
'@import url(../tests/test.css); body { background: #ffffff; }',
'/fileadmin/css/',
'@import url(\'/fileadmin/tests/test.css\'); body { background: #ffffff; }',
],
'@import from fileadmin with no relative' => [
'@import url(test.css); body { background: #ffffff; }',
'fileadmin/css/',
'@import url(\'fileadmin/css/test.css\'); body { background: #ffffff; }',
],
'@import from sitepackage with no relative' => [
'@import url(test.css); body { background: #ffffff; }',
'typo3conf/ext/sitepackage/Resources/Public/Css/',
'@import url(\'typo3conf/ext/sitepackage/Resources/Public/Css/test.css\'); body { background: #ffffff; }',
],
'url() from sitepackage with relative' => [
'@font-face {
font-family: "Testfont"
src: url("../fonts/testfont.woff2") format("woff2"),
url("../fonts/testfont.woff") format("woff");
}',
'../../../typo3conf/ext/sitepackage/Resources/Public/Css/',
'@font-face {
font-family: "Testfont"
src: url(\'../../../typo3conf/ext/sitepackage/Resources/Public/fonts/testfont.woff2\') format("woff2"),
url(\'../../../typo3conf/ext/sitepackage/Resources/Public/fonts/testfont.woff\') format("woff");
}',
],
'url() from fileadmin with no relative' => [
'@font-face {
font-family: "Testfont"
src: url("../fonts/testfont.woff2") format("woff2"),
url("../fonts/testfont.woff") format("woff");
}',
'fileadmin/css/',
'@font-face {
font-family: "Testfont"
src: url(\'fileadmin/fonts/testfont.woff2\') format("woff2"),
url(\'fileadmin/fonts/testfont.woff\') format("woff");
}',
],
];
}

/**
* @test
* @dataProvider fixRelativeUrlPathsDataProvider
* @param string $css css input
* @param string $newDir new Directory of css file
* @param string $expected expected adjusted import path
*/
public function fixRelativeUrlPaths(string $css, string $newDir, string $expected): void
{
$subject = new RelativeCssPathFixer();
$fixedCssPath = $subject->fixRelativeUrlPaths($css, $newDir);
self::assertSame($expected, $fixedCssPath);
}
}

0 comments on commit 3cbdf32

Please sign in to comment.