From d8d1f89ca05b030c44565173846704a7418de7bf Mon Sep 17 00:00:00 2001 From: Steve Clay Date: Sun, 19 Dec 2010 21:00:27 +0000 Subject: [PATCH] Issue 214 (remove traversals in prepended links) --- min/lib/Minify/CSS/UriRewriter.php | 196 ++++++++++-------- .../css_uriRewriter/exp_prepend.css | 14 ++ .../css_uriRewriter/exp_prepend2.css | 14 ++ .../test_Minify_CSS_UriRewriter.php | 41 +++- 4 files changed, 176 insertions(+), 89 deletions(-) create mode 100644 min_unit_tests/_test_files/css_uriRewriter/exp_prepend.css create mode 100644 min_unit_tests/_test_files/css_uriRewriter/exp_prepend2.css diff --git a/min/lib/Minify/CSS/UriRewriter.php b/min/lib/Minify/CSS/UriRewriter.php index 824c6bb..1404371 100644 --- a/min/lib/Minify/CSS/UriRewriter.php +++ b/min/lib/Minify/CSS/UriRewriter.php @@ -12,13 +12,6 @@ */ class Minify_CSS_UriRewriter { - /** - * Defines which class to call as part of callbacks, change this - * if you extend Minify_CSS_UriRewriter - * @var string - */ - protected static $className = 'Minify_CSS_UriRewriter'; - /** * rewrite() and rewriteRelative() append debugging information here * @var string @@ -26,7 +19,7 @@ class Minify_CSS_UriRewriter { public static $debugText = ''; /** - * Rewrite file relative URIs as root relative in CSS files + * In CSS content, rewrite file relative URIs as root relative * * @param string $css * @@ -83,7 +76,7 @@ class Minify_CSS_UriRewriter { } /** - * Prepend a path to relative URIs in CSS files + * In CSS content, prepend a path to relative URIs * * @param string $css * @@ -107,73 +100,8 @@ class Minify_CSS_UriRewriter { return $css; } - /** - * @var string directory of this stylesheet - */ - private static $_currentDir = ''; - - /** - * @var string DOC_ROOT - */ - private static $_docRoot = ''; - - /** - * @var array directory replacements to map symlink targets back to their - * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath' - */ - private static $_symlinks = array(); - - /** - * @var string path to prepend - */ - private static $_prependPath = null; - - private static function _trimUrls($css) - { - return preg_replace('/ - url\\( # url( - \\s* - ([^\\)]+?) # 1 = URI (assuming does not contain ")") - \\s* - \\) # ) - /x', 'url($1)', $css); - } - - private static function _processUriCB($m) - { - // $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' - $isImport = ($m[0][0] === '@'); - // determine URI and the quote character (if any) - if ($isImport) { - $quoteChar = $m[1]; - $uri = $m[2]; - } else { - // $m[1] is either quoted or not - $quoteChar = ($m[1][0] === "'" || $m[1][0] === '"') - ? $m[1][0] - : ''; - $uri = ($quoteChar === '') - ? $m[1] - : substr($m[1], 1, strlen($m[1]) - 2); - } - // analyze URI - if ('/' !== $uri[0] // root-relative - && false === strpos($uri, '//') // protocol (non-data) - && 0 !== strpos($uri, 'data:') // data protocol - ) { - // URI is file-relative: rewrite depending on options - $uri = (self::$_prependPath !== null) - ? (self::$_prependPath . $uri) - : self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks); - } - return $isImport - ? "@import {$quoteChar}{$uri}{$quoteChar}" - : "url({$quoteChar}{$uri}{$quoteChar})"; - } - - /** - * Rewrite a file relative URI as root relative + * Get a root relative URI from a file relative URI * * * Minify_CSS_UriRewriter::rewriteRelative( @@ -219,16 +147,16 @@ class Minify_CSS_UriRewriter { self::$debugText .= "file-relative URI : {$uri}\n" . "path prepended : {$path}\n"; - // "unresolve" a symlink back to doc root - foreach ($symlinks as $link => $target) { - if (0 === strpos($path, $target)) { - // replace $target with $link + // "unresolve" a symlink back to doc root + foreach ($symlinks as $link => $target) { + if (0 === strpos($path, $target)) { + // replace $target with $link $path = $link . substr($path, strlen($target)); - self::$debugText .= "symlink unresolved : {$path}\n"; + self::$debugText .= "symlink unresolved : {$path}\n"; - break; - } + break; + } } // strip doc root $path = substr($path, strlen($realDocRoot)); @@ -239,18 +167,35 @@ class Minify_CSS_UriRewriter { $uri = strtr($path, '/\\', '//'); - // remove /./ and /../ where possible - $uri = str_replace('/./', '/', $uri); - // inspired by patch from Oleg Cherniy - do { - $uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed); - } while ($changed); + $uri = self::removeDots($uri); self::$debugText .= "traversals removed : {$uri}\n\n"; return $uri; } + + /** + * Remove instances of "./" and "../" where possible from a root-relative URI + * @param string $uri + * @return string + */ + public static function removeDots($uri) + { + $uri = str_replace('/./', '/', $uri); + // inspired by patch from Oleg Cherniy + do { + $uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed); + } while ($changed); + return $uri; + } + /** + * Defines which class to call as part of callbacks, change this + * if you extend Minify_CSS_UriRewriter + * @var string + */ + protected static $className = 'Minify_CSS_UriRewriter'; + /** * Get realpath with any trailing slash removed. If realpath() fails, * just remove the trailing slash. @@ -267,4 +212,79 @@ class Minify_CSS_UriRewriter { } return rtrim($path, '/\\'); } + + /** + * @var string directory of this stylesheet + */ + private static $_currentDir = ''; + + /** + * @var string DOC_ROOT + */ + private static $_docRoot = ''; + + /** + * @var array directory replacements to map symlink targets back to their + * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath' + */ + private static $_symlinks = array(); + + /** + * @var string path to prepend + */ + private static $_prependPath = null; + + private static function _trimUrls($css) + { + return preg_replace('/ + url\\( # url( + \\s* + ([^\\)]+?) # 1 = URI (assuming does not contain ")") + \\s* + \\) # ) + /x', 'url($1)', $css); + } + + private static function _processUriCB($m) + { + // $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' + $isImport = ($m[0][0] === '@'); + // determine URI and the quote character (if any) + if ($isImport) { + $quoteChar = $m[1]; + $uri = $m[2]; + } else { + // $m[1] is either quoted or not + $quoteChar = ($m[1][0] === "'" || $m[1][0] === '"') + ? $m[1][0] + : ''; + $uri = ($quoteChar === '') + ? $m[1] + : substr($m[1], 1, strlen($m[1]) - 2); + } + // analyze URI + if ('/' !== $uri[0] // root-relative + && false === strpos($uri, '//') // protocol (non-data) + && 0 !== strpos($uri, 'data:') // data protocol + ) { + // URI is file-relative: rewrite depending on options + if (self::$_prependPath === null) { + $uri = self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks); + } else { + $uri = self::$_prependPath . $uri; + if ($uri[0] === '/') { + $root = ''; + $rootRelative = $uri; + $uri = $root . self::removeDots($rootRelative); + } elseif (preg_match('@^((https?\:)?//([^/]+))/@', $uri, $m) && (false !== strpos($m[3], '.'))) { + $root = $m[1]; + $rootRelative = substr($uri, strlen($root)); + $uri = $root . self::removeDots($rootRelative); + } + } + } + return $isImport + ? "@import {$quoteChar}{$uri}{$quoteChar}" + : "url({$quoteChar}{$uri}{$quoteChar})"; + } } diff --git a/min_unit_tests/_test_files/css_uriRewriter/exp_prepend.css b/min_unit_tests/_test_files/css_uriRewriter/exp_prepend.css new file mode 100644 index 0000000..87b7ecb --- /dev/null +++ b/min_unit_tests/_test_files/css_uriRewriter/exp_prepend.css @@ -0,0 +1,14 @@ +@import "http://cnd.com/A/B/foo.css"; +@import 'http://cnd.com/A/B/bar/foo.css' print; +@import 'http://cnd.com/A/bar/foo.css' print; +@import 'http://cnd.com/foo.css' print; +@import '/css/foo.css'; /* abs, should not alter */ +@import 'http://foo.com/css/foo.css'; /* abs, should not alter */ +@import url(http://cnd.com/A/foo.css) tv, projection; +@import url("/css/foo.css"); /* abs, should not alter */ +@import url(/css2/foo.css); /* abs, should not alter */ +@import url(data:image/gif;base64,AAAA); /* data, should not alter */ +foo {background:url('http://cnd.com/A/B/bar/foo.png')} +foo {background:url('http://foo.com/css/foo.css');} /* abs, should not alter */ +foo {background:url("//foo.com/css/foo.css");} /* protocol relative, should not alter */ +foo {background:url(data:image/gif;base64,AAAA);} /* data, should not alter */ \ No newline at end of file diff --git a/min_unit_tests/_test_files/css_uriRewriter/exp_prepend2.css b/min_unit_tests/_test_files/css_uriRewriter/exp_prepend2.css new file mode 100644 index 0000000..ceea4ec --- /dev/null +++ b/min_unit_tests/_test_files/css_uriRewriter/exp_prepend2.css @@ -0,0 +1,14 @@ +@import "//cnd.com/A/B/foo.css"; +@import '//cnd.com/A/B/bar/foo.css' print; +@import '//cnd.com/A/bar/foo.css' print; +@import '//cnd.com/foo.css' print; +@import '/css/foo.css'; /* abs, should not alter */ +@import 'http://foo.com/css/foo.css'; /* abs, should not alter */ +@import url(//cnd.com/A/foo.css) tv, projection; +@import url("/css/foo.css"); /* abs, should not alter */ +@import url(/css2/foo.css); /* abs, should not alter */ +@import url(data:image/gif;base64,AAAA); /* data, should not alter */ +foo {background:url('//cnd.com/A/B/bar/foo.png')} +foo {background:url('http://foo.com/css/foo.css');} /* abs, should not alter */ +foo {background:url("//foo.com/css/foo.css");} /* protocol relative, should not alter */ +foo {background:url(data:image/gif;base64,AAAA);} /* data, should not alter */ \ No newline at end of file diff --git a/min_unit_tests/test_Minify_CSS_UriRewriter.php b/min_unit_tests/test_Minify_CSS_UriRewriter.php index 7217a3d..40eba91 100644 --- a/min_unit_tests/test_Minify_CSS_UriRewriter.php +++ b/min_unit_tests/test_Minify_CSS_UriRewriter.php @@ -17,7 +17,7 @@ function test_Minify_CSS_UriRewriter() ,$thisDir // use DOCUMENT_ROOT = '/full/path/to/min_unit_tests' ); - $passed = assertTrue($expected === $actual, 'Minify_CSS_UriRewriter'); + $passed = assertTrue($expected === $actual, 'Minify_CSS_UriRewriter : rewrite'); if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { echo "\n---Input:\n\n{$in}\n"; echo "\n---Output: " .countBytes($actual). " bytes\n\n{$actual}\n\n"; @@ -29,6 +29,45 @@ function test_Minify_CSS_UriRewriter() echo "--- Minify_CSS_UriRewriter::\$debugText\n\n" , Minify_CSS_UriRewriter::$debugText; } + + + Minify_CSS_UriRewriter::$debugText = ''; + $in = file_get_contents($thisDir . '/_test_files/css_uriRewriter/in.css'); + $expected = file_get_contents($thisDir . '/_test_files/css_uriRewriter/exp_prepend.css'); + $actual = Minify_CSS_UriRewriter::prepend($in, 'http://cnd.com/A/B/'); + + $passed = assertTrue($expected === $actual, 'Minify_CSS_UriRewriter : prepend1'); + if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { + echo "\n---Input:\n\n{$in}\n"; + echo "\n---Output: " .countBytes($actual). " bytes\n\n{$actual}\n\n"; + if (!$passed) { + echo "---Expected: " .countBytes($expected). " bytes\n\n{$expected}\n\n\n"; + } + + // show debugging only when test run directly + echo "--- Minify_CSS_UriRewriter::\$debugText\n\n" + , Minify_CSS_UriRewriter::$debugText; + } + + + Minify_CSS_UriRewriter::$debugText = ''; + $in = file_get_contents($thisDir . '/_test_files/css_uriRewriter/in.css'); + $expected = file_get_contents($thisDir . '/_test_files/css_uriRewriter/exp_prepend2.css'); + $actual = Minify_CSS_UriRewriter::prepend($in, '//cnd.com/A/B/'); + + $passed = assertTrue($expected === $actual, 'Minify_CSS_UriRewriter : prepend2'); + if (__FILE__ === realpath($_SERVER['SCRIPT_FILENAME'])) { + echo "\n---Input:\n\n{$in}\n"; + echo "\n---Output: " .countBytes($actual). " bytes\n\n{$actual}\n\n"; + if (!$passed) { + echo "---Expected: " .countBytes($expected). " bytes\n\n{$expected}\n\n\n"; + } + + // show debugging only when test run directly + echo "--- Minify_CSS_UriRewriter::\$debugText\n\n" + , Minify_CSS_UriRewriter::$debugText; + } + Minify_CSS_UriRewriter::$debugText = ''; $in = '../../../../assets/skins/sam/sprite.png';