From d86d79b7d2c4c8addfb783ac14d667c9da36de34 Mon Sep 17 00:00:00 2001 From: Steve Clay Date: Sat, 1 Mar 2008 20:23:50 +0000 Subject: [PATCH] + URI rewriter in CSS min, fix for protocol-relative URIs, + partial minify.php compatibility --- lib/Minify.php | 30 +---------- lib/Minify/CSS.php | 38 +++++++++---- lib/Minify/Controller/Base.php | 29 +++++++++- lib/Minify/Source.php | 6 +-- web/test/css/paths.css | 13 ++--- web/test/css/paths.min.css | 2 +- web/test/test_CSS.php | 4 +- web/version1/minify.php | 99 ++++++++++++++++++++++++++++++++++ 8 files changed, 169 insertions(+), 52 deletions(-) create mode 100644 web/version1/minify.php diff --git a/lib/Minify.php b/lib/Minify.php index d2b9976..1f9fa34 100644 --- a/lib/Minify.php +++ b/lib/Minify.php @@ -55,34 +55,6 @@ class Minify { : $path; } - /** - * Create a controller instance and handle the request - * - * @param string type This should be the filename of the controller without - * extension. e.g. 'Group' - * - * @param array $ctrlOptions options for the controller's constructor - * - * @param array $minOptions options passed on to Minify - * - * @return mixed false on failure or array of content and headers sent - */ - public static function serveold($type, $ctrlOptions = array(), $minOptions = array()) { - $class = 'Minify_Controller_' . $type; - if (! class_exists($class, false)) { - require_once "Minify/Controller/{$type}.php"; - } - $ctrl = new $class($ctrlOptions, $minOptions); - $ret = self::handleRequest($ctrl); - if (false === $ret) { - if (! isset($ctrl->minOptions['quiet']) || ! $ctrl->minOptions['quiet']) { - header("HTTP/1.0 400 Bad Request"); - exit('400 Bad Request'); - } - } - return $ret; - } - /** * Serve a request for a minified file. * @@ -219,7 +191,7 @@ class Minify { // add headers to those from ConditionalGet //$headers['Content-Length'] = strlen($content); $headers['Content-Type'] = (null !== self::$_options['contentTypeCharset']) - ? self::$_options['contentType'] . ';charset=' . self::$_options['contentTypeCharset'] + ? self::$_options['contentType'] . '; charset=' . self::$_options['contentTypeCharset'] : self::$_options['contentType']; if (self::$_options['encodeMethod'] !== '') { $headers['Content-Encoding'] = $contentEncoding; diff --git a/lib/Minify/CSS.php b/lib/Minify/CSS.php index a000cfc..fb930e5 100644 --- a/lib/Minify/CSS.php +++ b/lib/Minify/CSS.php @@ -64,15 +64,21 @@ class Minify_CSS { $css = preg_replace('/#([a-f\\d])\\1([a-f\\d])\\2([a-f\\d])\\3([\\s;\\}])/i' , '#$1$2$3$4', $css); + $rewrite = false; if (isset($options['prependRelativePath'])) { self::$_tempPrepend = $options['prependRelativePath']; + $rewrite = true; + } elseif (isset($options['currentPath'])) { + self::$_tempCurrentPath = $options['currentPath']; + $rewrite = true; + } + if ($rewrite) { $css = preg_replace_callback('/@import ([\'"])(.*?)[\'"]\\s*;/' ,array('Minify_CSS', '_urlCB'), $css); - $css = preg_replace_callback('/url\\(([^\\)]+)\\)/' ,array('Minify_CSS', '_urlCB'), $css); } - + self::$_tempPrepend = self::$_tempCurrentPath = ''; return trim($css); } @@ -88,6 +94,11 @@ class Minify_CSS { */ private static $_tempPrepend = ''; + /** + * @var string path of this stylesheet for rewriting purposes + */ + private static $_tempCurrentPath = ''; + /** * Process what looks like a comment and return a replacement * @@ -157,17 +168,24 @@ class Minify_CSS { ? $m[1] : substr($m[1], 1, strlen($m[1]) - 2); } - if ('/' === $url[0]) { - if ('/' === $url[1]) { - // protocol relative URI! - $url = '//' . self::$_tempPrepend . substr($url, 2); - } - } else { + if ('/' !== $url[0]) { if (strpos($url, '//') > 0) { // probably starts with protocol, do not alter } else { - // relative URI - $url = self::$_tempPrepend . $url; + // relative URI, rewrite! + if (self::$_tempPrepend) { + $url = self::$_tempPrepend . $url; + } else { + // rewrite absolute url from scratch! + // prepend path with current dir separator (OS-independent) + $path = self::$_tempCurrentPath + . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR); + // strip doc root + $path = substr($path, strlen($_SERVER['DOCUMENT_ROOT'])); + // fix to absolute URL + $url = strtr($path, DIRECTORY_SEPARATOR, '/'); + $url = str_replace('/./', '/', $url); + } } } if ($isImport) { diff --git a/lib/Minify/Controller/Base.php b/lib/Minify/Controller/Base.php index f468a76..b55a0b7 100644 --- a/lib/Minify/Controller/Base.php +++ b/lib/Minify/Controller/Base.php @@ -88,6 +88,32 @@ abstract class Minify_Controller_Base { } } + /** + * Is a user-given file within document root, existing, + * and having an extension js/css/html/txt + * + * This is a convenience function for controllers that have to accept + * user-given paths + * + * @param string $file full file path (already processed by realpath()) + * @param string $docRoot root where files are safe to serve + * @return bool file is safe + */ + public static function _fileIsSafe($file, $docRoot) + { + if (strpos($file, $docRoot) !== 0 || ! file_exists($file)) { + return false; + } + $base = basename($file); + if ($base[0] === '.') { + return false; + } + list($revExt) = explode('.', strrev($base)); + return in_array(strrev($revExt), array('js', 'css', 'html', 'txt')); + } + + /*public static function _haveSameExt + /** * @var array instances of Minify_Source, which provide content and * any individual minification needs. @@ -125,7 +151,8 @@ abstract class Minify_Controller_Base { * * @return array options for Minify */ - public final function analyzeSources($options = array()) { + public final function analyzeSources($options = array()) + { if ($this->sources) { if (! isset($options['contentType'])) { $options['contentType'] = Minify_Source::getContentType($this->sources); diff --git a/lib/Minify/Source.php b/lib/Minify/Source.php index 9d4f24a..b37f3b2 100644 --- a/lib/Minify/Source.php +++ b/lib/Minify/Source.php @@ -113,9 +113,9 @@ class Minify_Source { public static function getContentType($sources) { $exts = array( - 'css' => 'text/css' - ,'js' => 'application/x-javascript' - ,'html' => 'text/html' + 'css' => Minify::TYPE_CSS + ,'js' => Minify::TYPE_JS + ,'html' => Minify::TYPE_HTML ); foreach ($sources as $source) { if (null !== $source->_filepath) { diff --git a/web/test/css/paths.css b/web/test/css/paths.css index 19d248e..0c7904b 100644 --- a/web/test/css/paths.css +++ b/web/test/css/paths.css @@ -1,9 +1,10 @@ @import "foo.css"; @import 'bar/foo.css'; -@import '/css/foo.css'; -@import 'http://foo.com/css/foo.css'; -@import url(./foo.css); -@import url("/css/foo.css"); -@import url(/css2/foo.css); +@import '/css/foo.css'; /* abs, should not alter */ +@import 'http://foo.com/css/foo.css'; /* abs, should not alter */ +@import url(../foo.css); +@import url("/css/foo.css"); /* abs, should not alter */ +@import url(/css2/foo.css); /* abs, should not alter */ foo {background:url('bar/foo.png')} -foo {background:url('http://foo.com/css/foo.css');} \ No newline at end of file +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 */ \ No newline at end of file diff --git a/web/test/css/paths.min.css b/web/test/css/paths.min.css index b887891..562de38 100644 --- a/web/test/css/paths.min.css +++ b/web/test/css/paths.min.css @@ -1 +1 @@ -@import "../foo.css";@import '../bar/foo.css';@import '/css/foo.css';@import 'http://foo.com/css/foo.css';@import url(.././foo.css);@import url("/css/foo.css");@import url(/css2/foo.css);foo{background:url('../bar/foo.png')}foo{background:url('http://foo.com/css/foo.css')} \ No newline at end of file +@import "../foo.css";@import '../bar/foo.css';@import '/css/foo.css';@import 'http://foo.com/css/foo.css';@import url(../../foo.css);@import url("/css/foo.css");@import url(/css2/foo.css);foo{background:url('../bar/foo.png')}foo{background:url('http://foo.com/css/foo.css')}foo{background:url("//foo.com/css/foo.css")} \ No newline at end of file diff --git a/web/test/test_CSS.php b/web/test/test_CSS.php index c7284a3..969c95e 100644 --- a/web/test/test_CSS.php +++ b/web/test/test_CSS.php @@ -25,8 +25,8 @@ foreach ($list as $item) { if ($minExpected !== $minOutput) { echo "\n---Source\n\n{$src}"; - echo "\n---Expected\n\n{$minExpected}"; - echo "\n---Output\n\n{$minOutput}\n\n\n\n"; + echo "\n\n---Expected\n\n{$minExpected}"; + echo "\n\n---Output\n\n{$minOutput}\n\n\n\n"; } } diff --git a/web/version1/minify.php b/web/version1/minify.php new file mode 100644 index 0000000..2175bbd --- /dev/null +++ b/web/version1/minify.php @@ -0,0 +1,99 @@ +sources and return $options + public function setupSources($options) { + $options['badRequestHeader'] = 'HTTP/1.0 404 Not Found'; + $options['contentTypeCharset'] = MINIFY_ENCODING; + + // The following restrictions are to limit the URLs that minify will + // respond to. Ideally there should be only one way to reference a file. + if (! isset($_GET['files']) + // verify at least one file, files are single comma separated, + // and are all same extension + || ! preg_match('/^[^,]+\\.(css|js)(,[^,]+\\.\\1)*$/', $_GET['files'], $m) + // no "//" (makes URL rewriting easier) + || strpos($_GET['files'], '//') !== false + // no "\" + || strpos($_GET['files'], '\\') !== false + // no "./" + || preg_match('/(?:^|[^\\.])\\.\\//', $_GET['files']) + ) { + return $options; + } + $extension = $m[1]; + + $files = explode(',', $_GET['files']); + if (count($files) > MINIFY_MAX_FILES) { + return $options; + } + + // strings for prepending to relative/absolute paths + $prependRelPaths = dirname($_SERVER['SCRIPT_FILENAME']) + . DIRECTORY_SEPARATOR; + $prependAbsPaths = $_SERVER['DOCUMENT_ROOT']; + + $sources = array(); + $goodFiles = array(); + $hasBadSource = false; + foreach ($files as $file) { + // prepend appropriate string for abs/rel paths + $file = ($file[0] === '/' ? $prependAbsPaths : $prependRelPaths) . $file; + // make sure a real file! + $file = realpath($file); + // don't allow unsafe or duplicate files + if (parent::_fileIsSafe($file, MINIFY_BASE_DIR) + && !in_array($file, $goodFiles)) + { + $goodFiles[] = $file; + $srcOptions = array( + 'filepath' => $file + ); + if ('css' === $extension && MINIFY_REWRITE_CSS_URLS) { + $srcOptions['minifyOptions']['currentPath'] = dirname($file); + } + $this->sources[] = new Minify_Source($srcOptions); + } else { + $hasBadSource = true; + break; + } + } + if ($hasBadSource) { + $this->sources = array(); + } + return $options; + } +} + +$v1 = new V1Controller(); +if (MINIFY_USE_CACHE) { + Minify::useServerCache(MINIFY_CACHE_DIR); +} +Minify::serve($v1); \ No newline at end of file