diff --git a/HISTORY.txt b/HISTORY.txt
index 95a46c8..94606ed 100644
--- a/HISTORY.txt
+++ b/HISTORY.txt
@@ -1,5 +1,21 @@
Minify Release History
+Version 2.1.4
+ * Option to minify JS with Closure Compiler API w/ JSMin failover
+ * Cookie/bookmarklet-based debug mode. No HTML editing!
+ * Allows 1 file to be missing w/o complete failure
+ * Combine multiple groups and files in single URI
+ * More useful HTML helpers for writing versioned URIs
+ * More detailed error logging, including minifier exceptions
+ * Builder offers more helpful messages/PHP environment warnings
+ * Bypass minification based on filename pattern. e.g. foo.min.js / foo-min.css
+ * JSMin won't choke on common Closure compiler syntaxes (i+ ++j)
+ * Better caching in IE6
+ * Cache ids are influenced by group/file names
+ * Debug mode for Javascript doesn't break on common XPath strings (Prototype 1.6)
+ * Removed annoying maxFiles limit
+ * mbstring.func_overload usage is safer
+
Version 2.1.3
* HTTP fixes
* ETag generation now valid (different when gzipped)
diff --git a/README.txt b/README.txt
index 3899b99..5d2e232 100644
--- a/README.txt
+++ b/README.txt
@@ -8,6 +8,14 @@ and tell clients to cache the file for a period of time.
More info: http://code.google.com/p/minify/
+WORDPRESS USER?
+
+These WP plugins integrate Minify into WordPress's style and script hooks to
+get you set up faster.
+ http://wordpress.org/extend/plugins/wp-minify/
+ http://wordpress.org/extend/plugins/w3-total-cache/
+
+
UPGRADING
See UPGRADING.txt for instructions.
@@ -16,12 +24,14 @@ See UPGRADING.txt for instructions.
INSTALLATION AND USAGE:
1. Place the /min/ directory as a child of your DOCUMENT_ROOT
-directory: i.e. you will have: /home/user/www/public_html/min
+directory: i.e. you will have: /home/user/www/min
2. Open http://yourdomain/min/ in a web browser. This will forward
you to the Minify URI Builder application, which will help you
quickly start using Minify to serve content on your site.
+See the User Guide: http://code.google.com/p/minify/wiki/UserGuide
+
UNIT TESTING:
@@ -36,12 +46,6 @@ components with more verbose output.)
3. Remove /min_unit_tests/ from your DOCUMENT_ROOT when you are done.
-EXTRAS:
-
-The min_extras folder contains files for benchmarking using Apache ab on Windows
-and a couple single-use tools. DO NOT place this on your production server.
-
-
FILE ENCODINGS
Minify *should* work fine with files encoded in UTF-8 or other 8-bit
diff --git a/min/.htaccess b/min/.htaccess
index 42f13eb..06c1161 100644
--- a/min/.htaccess
+++ b/min/.htaccess
@@ -1,4 +1,13 @@
Note: Please set $min_cachePath
-in /min/config.php to improve performance.
Uh Oh. Minify was unable to + serve Javascript for this app. To troubleshoot this, + enable FirePHP debugging + and request the Minify URL directly. Hopefully the + FirePHP console will report the cause of the error. +
+ + +Note: was discovered as a usable temp directory.
To
+ slightly improve performance you can hardcode this in /min/config.php:
+
Note: Your webserver does not seem to @@ -107,20 +160,33 @@ in your list, and move any others to the top of the first file in your list
If you desire, you can use Minify URIs in imports and they will not be touched
by Minify. E.g. @import "/min/?g=css2";
When /min/config.php has $min_allowDebugFlag = true;
+ you can get debug output by appending &debug
to a Minify URL, or
+ by sending the cookie minDebug=<match>
, where <match>
+ should be a string in the Minify URIs you'd like to debug. This bookmarklet will allow you to
+ set this cookie.
Minify Debug (right-click, add to bookmarks)
+Need help? Search or post to the Minify discussion list.
-This app is minified :) view -source
+Need help? Check the wiki, + or post to the discussion + list.
+Powered by Minify
- - - + - - + ob_get_contents() - ,'id' => __FILE__ - ,'lastModifiedTime' => max( - // regenerate cache if either of these change - filemtime(__FILE__) - ,filemtime(dirname(__FILE__) . '/../config.php') - ) - ,'minifyAll' => true - ,'encodeOutput' => $encodeOutput -); -ob_end_clean(); - -set_include_path(dirname(__FILE__) . '/../lib' . PATH_SEPARATOR . get_include_path()); - -require 'Minify.php'; - +// setup Minify if (0 === stripos(PHP_OS, 'win')) { Minify::setDocRoot(); // we may be on IIS } -Minify::setCache(isset($min_cachePath) ? $min_cachePath : null); +Minify::setCache( + isset($min_cachePath) ? $min_cachePath : '' + ,$min_cacheFileLocking +); Minify::$uploaderHoursBehind = $min_uploaderHoursBehind; -Minify::serve('Page', $serveOpts); +Minify::serve('Page', array( + 'content' => $content + ,'id' => __FILE__ + ,'lastModifiedTime' => max( + // regenerate cache if any of these change + filemtime(__FILE__) + ,filemtime(dirname(__FILE__) . '/../config.php') + ,filemtime(dirname(__FILE__) . '/../lib/Minify.php') + ) + ,'minifyAll' => true + ,'encodeOutput' => $encodeOutput +)); diff --git a/min/builder/test.php b/min/builder/test.php new file mode 100644 index 0000000..08fb498 --- /dev/null +++ b/min/builder/test.php @@ -0,0 +1,43 @@ +realpath(DOCUMENT_ROOT) failed. You may need " + . "to set \$min_documentRoot manually (hopefully realpath() is not " + . "broken in your environment)."; + } + if (0 !== strpos(realpath(__FILE__), realpath($_SERVER['DOCUMENT_ROOT']))) { + echo "DOCUMENT_ROOT doesn't contain this file. You may " + . " need to set \$min_documentRoot manually
"; + } + if (isset($_SERVER['SUBDOMAIN_DOCUMENT_ROOT'])) { + echo "\$_SERVER['SUBDOMAIN_DOCUMENT_ROOT'] is set. " + . "You may need to set \$min_documentRoot to this in config.php
"; + } + +} + +//*/ \ No newline at end of file diff --git a/min/config.php b/min/config.php index 76199a0..8a3a890 100644 --- a/min/config.php +++ b/min/config.php @@ -1,33 +1,38 @@ array('//js/file1.js', '//js/file2.js'), // 'css' => array('//css/file1.css', '//css/file2.css'), - - // custom source example - /*'js2' => array( - dirname(__FILE__) . '/../min_unit_tests/_test_files/js/before.js', - // do NOT process this file - new Minify_Source(array( - 'filepath' => dirname(__FILE__) . '/../min_unit_tests/_test_files/js/before.js', - 'minifier' => create_function('$a', 'return $a;') - )) - ),//*/ - - /*'js3' => array( - dirname(__FILE__) . '/../min_unit_tests/_test_files/js/before.js', - // do NOT process this file - new Minify_Source(array( - 'filepath' => dirname(__FILE__) . '/../min_unit_tests/_test_files/js/before.js', - 'minifier' => array('Minify_Packer', 'minify') - )) - ),//*/ ); \ No newline at end of file diff --git a/min/index.php b/min/index.php index 51c3525..165c8d5 100644 --- a/min/index.php +++ b/min/index.php @@ -30,19 +30,33 @@ if ($min_documentRoot) { } $min_serveOptions['minifierOptions']['text/css']['symlinks'] = $min_symlinks; +// auto-add targets to allowDirs +foreach ($min_symlinks as $uri => $target) { + $min_serveOptions['minApp']['allowDirs'][] = $target; +} -if ($min_allowDebugFlag && isset($_GET['debug'])) { - $min_serveOptions['debug'] = true; +if ($min_allowDebugFlag) { + if (! empty($_COOKIE['minDebug'])) { + foreach (preg_split('/\\s+/', $_COOKIE['minDebug']) as $debugUri) { + if (false !== strpos($_SERVER['REQUEST_URI'], $debugUri)) { + $min_serveOptions['debug'] = true; + break; + } + } + } + // allow GET to override + if (isset($_GET['debug'])) { + $min_serveOptions['debug'] = true; + } } if ($min_errorLogger) { require_once 'Minify/Logger.php'; if (true === $min_errorLogger) { require_once 'FirePHP.php'; - Minify_Logger::setLogger(FirePHP::getInstance(true)); - } else { - Minify_Logger::setLogger($min_errorLogger); + $min_errorLogger = FirePHP::getInstance(true); } + Minify_Logger::setLogger($min_errorLogger); } // check for URI versioning @@ -55,7 +69,12 @@ if (isset($_GET['g'])) { } if (isset($_GET['f']) || isset($_GET['g'])) { // serve! - Minify::serve('MinApp', $min_serveOptions); + + if (! isset($min_serveController)) { + require 'Minify/Controller/MinApp.php'; + $min_serveController = new Minify_Controller_MinApp(); + } + Minify::serve($min_serveController, $min_serveOptions); } elseif ($min_enableBuilder) { header('Location: builder/'); @@ -63,4 +82,4 @@ if (isset($_GET['f']) || isset($_GET['g'])) { } else { header("Location: /"); exit(); -} +} \ No newline at end of file diff --git a/min/lib/HTTP/ConditionalGet.php b/min/lib/HTTP/ConditionalGet.php index 823db05..158f73a 100644 --- a/min/lib/HTTP/ConditionalGet.php +++ b/min/lib/HTTP/ConditionalGet.php @@ -75,9 +75,8 @@ class HTTP_ConditionalGet { /** * @param array $spec options * - * 'isPublic': (bool) if true, the Cache-Control header will contain - * "public", allowing proxies to cache the content. Otherwise "private" will - * be sent, allowing only browser caching. (default false) + * 'isPublic': (bool) if false, the Cache-Control header will contain + * "private", allowing only browser caching. (default false) * * 'lastModifiedTime': (int) if given, both ETag AND Last-Modified headers * will be sent with content. This is recommended. @@ -150,7 +149,10 @@ class HTTP_ConditionalGet { } elseif (isset($spec['contentHash'])) { // Use the hash as the ETag $this->_setEtag($spec['contentHash'] . $etagAppend, $scope); } - $this->_headers['Cache-Control'] = "max-age={$maxAge}, {$scope}"; + $privacy = ($scope === 'private') + ? ', private' + : ''; + $this->_headers['Cache-Control'] = "max-age={$maxAge}{$privacy}"; // invalidate cache if disabled, otherwise check $this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate']) ? false @@ -209,7 +211,9 @@ class HTTP_ConditionalGet { { $headers = $this->_headers; if (array_key_exists('_responseCode', $headers)) { - header($headers['_responseCode']); + // FastCGI environments require 3rd arg to header() to be set + list(, $code) = explode(' ', $headers['_responseCode'], 3); + header($headers['_responseCode'], true, $code); unset($headers['_responseCode']); } foreach ($headers as $name => $val) { @@ -332,12 +336,9 @@ class HTTP_ConditionalGet { if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { return false; } - $ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE']; - if (false !== ($semicolon = strrpos($ifModifiedSince, ';'))) { - // IE has tacked on extra data to this header, strip it - $ifModifiedSince = substr($ifModifiedSince, 0, $semicolon); - } - if ($ifModifiedSince == self::gmtDate($this->_lmTime)) { + // strip off IE's extra data (semicolon) + list($ifModifiedSince) = explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE'], 2); + if (strtotime($ifModifiedSince) >= $this->_lmTime) { // Apache 2.2's behavior. If there was no ETag match, send the // non-encoded version of the ETag value. $this->_headers['ETag'] = $this->normalizeEtag($this->_etag); diff --git a/min/lib/HTTP/Encoder.php b/min/lib/HTTP/Encoder.php index 66c2678..49bedd7 100644 --- a/min/lib/HTTP/Encoder.php +++ b/min/lib/HTTP/Encoder.php @@ -33,11 +33,11 @@ * * * For more control over headers, use getHeaders() and getData() and send your - * own output. - * - * Note: If you don't need header mgmt, use PHP's native gzencode, gzdeflate, - * and gzcompress functions for gzip, deflate, and compress-encoding - * respectively. + * own output. + * + * Note: If you don't need header mgmt, use PHP's native gzencode, gzdeflate, + * and gzcompress functions for gzip, deflate, and compress-encoding + * respectively. * * @package Minify * @subpackage HTTP @@ -59,7 +59,7 @@ class HTTP_Encoder { * * @var bool */ - public static $encodeToIe6 = false; + public static $encodeToIe6 = true; /** @@ -88,10 +88,15 @@ class HTTP_Encoder { * * @return null */ - public function __construct($spec) + public function __construct($spec) { + $this->_useMbStrlen = (function_exists('mb_strlen') + && (ini_get('mbstring.func_overload') !== '') + && ((int)ini_get('mbstring.func_overload') & 2)); $this->_content = $spec['content']; - $this->_headers['Content-Length'] = (string)strlen($this->_content); + $this->_headers['Content-Length'] = $this->_useMbStrlen + ? (string)mb_strlen($this->_content, '8bit') + : (string)strlen($this->_content); if (isset($spec['type'])) { $this->_headers['Content-Type'] = $spec['type']; } @@ -111,7 +116,7 @@ class HTTP_Encoder { * * return string */ - public function getContent() + public function getContent() { return $this->_content; } @@ -130,7 +135,7 @@ class HTTP_Encoder { * * @return array */ - public function getHeaders() + public function getHeaders() { return $this->_headers; } @@ -146,7 +151,7 @@ class HTTP_Encoder { * * @return null */ - public function sendHeaders() + public function sendHeaders() { foreach ($this->_headers as $name => $val) { header($name . ': ' . $val); @@ -164,7 +169,7 @@ class HTTP_Encoder { * * @return null */ - public function sendAll() + public function sendAll() { $this->sendHeaders(); echo $this->_content; @@ -181,21 +186,21 @@ class HTTP_Encoder { * be non 0. The methods are favored in order of gzip, deflate, then * compress. Deflate is always smallest and generally faster, but is * rarely sent by servers, so client support could be buggier. - * + * * @param bool $allowCompress allow the older compress encoding * - * @param bool $allowDeflate allow the more recent deflate encoding + * @param bool $allowDeflate allow the more recent deflate encoding * * @return array two values, 1st is the actual encoding method, 2nd is the * alias of that method to use in the Content-Encoding header (some browsers * call gzip "x-gzip" etc.) */ - public static function getAcceptedEncoding($allowCompress = true, $allowDeflate = true) + public static function getAcceptedEncoding($allowCompress = true, $allowDeflate = true) { // @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html if (! isset($_SERVER['HTTP_ACCEPT_ENCODING']) - || self::_isBuggyIe()) + || self::isBuggyIe()) { return array('', ''); } @@ -244,16 +249,18 @@ class HTTP_Encoder { * this fails, false is returned. * * The header "Vary: Accept-Encoding" is added. If encoding is successful, - * the Content-Length header is updated, and Content-Encoding is also added. - * + * the Content-Length header is updated, and Content-Encoding is also added. + * * @param int $compressionLevel given to zlib functions. If not given, the * class default will be used. * * @return bool success true if the content was actually compressed */ - public function encode($compressionLevel = null) + public function encode($compressionLevel = null) { - $this->_headers['Vary'] = 'Accept-Encoding'; + if (! self::isBuggyIe()) { + $this->_headers['Vary'] = 'Accept-Encoding'; + } if (null === $compressionLevel) { $compressionLevel = self::$compressionLevel; } @@ -262,9 +269,9 @@ class HTTP_Encoder { || !extension_loaded('zlib')) { return false; - } - if ($this->_encodeMethod[0] === 'deflate') { - $encoded = gzdeflate($this->_content, $compressionLevel); + } + if ($this->_encodeMethod[0] === 'deflate') { + $encoded = gzdeflate($this->_content, $compressionLevel); } elseif ($this->_encodeMethod[0] === 'gzip') { $encoded = gzencode($this->_content, $compressionLevel); } else { @@ -273,7 +280,9 @@ class HTTP_Encoder { if (false === $encoded) { return false; } - $this->_headers['Content-Length'] = strlen($encoded); + $this->_headers['Content-Length'] = $this->_useMbStrlen + ? (string)mb_strlen($encoded, '8bit') + : (string)strlen($encoded); $this->_headers['Content-Encoding'] = $this->_encodeMethod[1]; $this->_content = $encoded; return true; @@ -285,7 +294,7 @@ class HTTP_Encoder { * This is a convenience method for common use of the class * * @param string $content - * + * * @param int $compressionLevel given to zlib functions. If not given, the * class default will be used. * @@ -296,20 +305,18 @@ class HTTP_Encoder { if (null === $compressionLevel) { $compressionLevel = self::$compressionLevel; } - $he = new HTTP_Encoder(array('content' => $content)); - $ret = $he->encode($compressionLevel); + $he = new HTTP_Encoder(array('content' => $content)); + $ret = $he->encode($compressionLevel); $he->sendAll(); return $ret; - } - - protected $_content = ''; - protected $_headers = array(); - protected $_encodeMethod = array('', ''); + } /** - * Is the browser an IE version earlier than 6 SP2? + * Is the browser an IE version earlier than 6 SP2? + * + * @return bool */ - protected static function _isBuggyIe() + public static function isBuggyIe() { $ua = $_SERVER['HTTP_USER_AGENT']; // quick escape for non-IEs @@ -318,9 +325,14 @@ class HTTP_Encoder { return false; } // no regex = faaast - $version = (float)substr($ua, 30); + $version = (float)substr($ua, 30); return self::$encodeToIe6 ? ($version < 6 || ($version == 6 && false === strpos($ua, 'SV1'))) : ($version < 7); } + + protected $_content = ''; + protected $_headers = array(); + protected $_encodeMethod = array('', ''); + protected $_useMbStrlen = false; } diff --git a/min/lib/JSMin.php b/min/lib/JSMin.php index 770e1c6..011eca3 100644 --- a/min/lib/JSMin.php +++ b/min/lib/JSMin.php @@ -1,12 +1,16 @@ + * $minifiedJs = JSMin::minify($js); + * * * This is a direct port of jsmin.c to PHP with a few PHP performance tweaks and * modifications to preserve some comments (see below). Also, rather than using * stdin/stdout, JSMin::minify() accepts a string as input and returns another * string as output. - * + * * Comments containing IE conditional compilation are preserved, as are multi-line * comments that begin with "/*!" (for documentation purposes). In the latter case * newlines are inserted around the comment to enhance readability. @@ -56,7 +60,7 @@ class JSMin { const ACTION_KEEP_A = 1; const ACTION_DELETE_A = 2; const ACTION_DELETE_A_B = 3; - + protected $a = "\n"; protected $b = ''; protected $input = ''; @@ -64,9 +68,9 @@ class JSMin { protected $inputLength = 0; protected $lookAhead = null; protected $output = ''; - + /** - * Minify Javascript + * Minify Javascript. * * @param string $js Javascript to be minified * @return string @@ -76,16 +80,22 @@ class JSMin { $jsmin = new JSMin($js); return $jsmin->min(); } - + /** - * Setup process + * @param string $input */ public function __construct($input) { - $this->input = str_replace("\r\n", "\n", $input); - $this->inputLength = strlen($this->input); + $this->input = $input; + // look out for syntax like "++ +" and "- ++" + $p = '\\+'; + $m = '\\-'; + if (preg_match("/([$p$m])(?:\\1 [$p$m]| (?:$p$p|$m$m))/", $input)) { + // likely pre-minified and would be broken by JSMin + $this->output = $input; + } } - + /** * Perform minification, return result */ @@ -94,8 +104,17 @@ class JSMin { if ($this->output !== '') { // min already run return $this->output; } + + $mbIntEnc = null; + if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) { + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('8bit'); + } + $this->input = str_replace("\r\n", "\n", $this->input); + $this->inputLength = strlen($this->input); + $this->action(self::ACTION_DELETE_A_B); - + while ($this->a !== null) { // determine next command $command = self::ACTION_KEEP_A; // default @@ -106,13 +125,16 @@ class JSMin { } elseif ($this->a === "\n") { if ($this->b === ' ') { $command = self::ACTION_DELETE_A_B; - } elseif (false === strpos('{[(+-', $this->b) - && ! $this->isAlphaNum($this->b)) { + // in case of mbstring.func_overload & 2, must check for null b, + // otherwise mb_strpos will give WARNING + } elseif ($this->b === null + || (false === strpos('{[(+-', $this->b) + && ! $this->isAlphaNum($this->b))) { $command = self::ACTION_DELETE_A; } } elseif (! $this->isAlphaNum($this->a)) { if ($this->b === ' ' - || ($this->b === "\n" + || ($this->b === "\n" && (false === strpos('}])+-"\'', $this->a)))) { $command = self::ACTION_DELETE_A_B; } @@ -120,9 +142,13 @@ class JSMin { $this->action($command); } $this->output = trim($this->output); + + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } return $this->output; } - + /** * ACTION_KEEP_A = Output A. Copy B to A. Get the next B. * ACTION_DELETE_A = Copy B to A. Get the next B. @@ -146,7 +172,8 @@ class JSMin { } if (ord($this->a) <= self::ORD_LF) { throw new JSMin_UnterminatedStringException( - 'Unterminated String: ' . var_export($str, true)); + "JSMin: Unterminated String at byte " + . $this->inputIndex . ": {$str}"); } $str .= $this->a; if ($this->a === '\\') { @@ -173,7 +200,8 @@ class JSMin { $pattern .= $this->a; } elseif (ord($this->a) <= self::ORD_LF) { throw new JSMin_UnterminatedRegExpException( - 'Unterminated RegExp: '. var_export($pattern, true)); + "JSMin: Unterminated RegExp at byte " + . $this->inputIndex .": {$pattern}"); } $this->output .= $this->a; } @@ -182,7 +210,7 @@ class JSMin { // end case ACTION_DELETE_A_B } } - + protected function isRegexpLiteral() { if (false !== strpos("\n{;(,=:[!&|?", $this->a)) { // we aren't dividing @@ -207,7 +235,7 @@ class JSMin { } return false; } - + /** * Get next char. Convert ctrl char to space. */ @@ -231,7 +259,7 @@ class JSMin { } return $c; } - + /** * Get next char. If is ctrl character, translate to a space or newline. */ @@ -240,7 +268,7 @@ class JSMin { $this->lookAhead = $this->get(); return $this->lookAhead; } - + /** * Is $c a letter, digit, underscore, dollar sign, escape, or non-ASCII? */ @@ -248,7 +276,7 @@ class JSMin { { return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126); } - + protected function singleLineComment() { $comment = ''; @@ -264,7 +292,7 @@ class JSMin { } } } - + protected function multipleLineComment() { $this->get(); @@ -276,7 +304,7 @@ class JSMin { $this->get(); // if comment preserved by YUI Compressor if (0 === strpos($comment, '!')) { - return "\n/*" . substr($comment, 1) . "*/\n"; + return "\n/*!" . substr($comment, 1) . "*/\n"; } // if IE conditional comment if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) { @@ -285,12 +313,14 @@ class JSMin { return ' '; } } elseif ($get === null) { - throw new JSMin_UnterminatedCommentException('Unterminated Comment: ' . var_export('/*' . $comment, true)); + throw new JSMin_UnterminatedCommentException( + "JSMin: Unterminated comment at byte " + . $this->inputIndex . ": /*{$comment}"); } $comment .= $get; } } - + /** * Get the next character, skipping over comments. * Some comments may be preserved. diff --git a/min/lib/Minify.php b/min/lib/Minify.php index 2c0ca34..d70e32d 100644 --- a/min/lib/Minify.php +++ b/min/lib/Minify.php @@ -29,12 +29,13 @@ require_once 'Minify/Source.php'; */ class Minify { - const VERSION = '2.1.3'; + const VERSION = '2.1.4'; const TYPE_CSS = 'text/css'; const TYPE_HTML = 'text/html'; // there is some debate over the ideal JS Content-Type, but this is the // Apache default and what Yahoo! uses.. const TYPE_JS = 'application/x-javascript'; + const URL_DEBUG = 'http://code.google.com/p/minify/wiki/Debugging'; /** * How many hours behind are the file modification times of uploaded files? @@ -179,9 +180,7 @@ class Minify { if (! $controller->sources) { // invalid request! if (! self::$_options['quiet']) { - header(self::$_options['badRequestHeader']); - echo self::$_options['badRequestHeader']; - return; + self::_errorExit(self::$_options['badRequestHeader'], self::URL_DEBUG); } else { list(,$statusCode) = explode(' ', self::$_options['badRequestHeader']); return array( @@ -202,6 +201,7 @@ class Minify { // determine encoding if (self::$_options['encodeOutput']) { + $sendVary = true; if (self::$_options['encodeMethod'] !== null) { // controller specifically requested this $contentEncoding = self::$_options['encodeMethod']; @@ -212,6 +212,7 @@ class Minify { // 'x-gzip' while our internal encodeMethod is 'gzip'. Calling // getAcceptedEncoding(false, false) leaves out compress and deflate as options. list(self::$_options['encodeMethod'], $contentEncoding) = HTTP_Encoder::getAcceptedEncoding(false, false); + $sendVary = ! HTTP_Encoder::isBuggyIe(); } } else { self::$_options['encodeMethod'] = ''; // identity (no encoding) @@ -226,6 +227,8 @@ class Minify { ); if (self::$_options['maxAge'] > 0) { $cgOptions['maxAge'] = self::$_options['maxAge']; + } elseif (self::$_options['debug']) { + $cgOptions['invalidate'] = true; } $cg = new HTTP_ConditionalGet($cgOptions); if ($cg->cacheIsValid) { @@ -250,7 +253,7 @@ class Minify { if (self::$_options['contentType'] === self::TYPE_CSS && self::$_options['rewriteCssUris']) { reset($controller->sources); - while (list($key, $source) = each($controller->sources)) { + foreach($controller->sources as $key => $source) { if ($source->filepath && !isset($source->minifyOptions['currentDir']) && !isset($source->minifyOptions['prependRelativePath']) @@ -261,12 +264,12 @@ class Minify { } // check server cache - if (null !== self::$_cache) { + if (null !== self::$_cache && ! self::$_options['debug']) { // using cache // the goal is to use only the cache methods to sniff the length and // output the content, as they do not require ever loading the file into // memory. - $cacheId = 'minify_' . self::_getCacheId(); + $cacheId = self::_getCacheId(); $fullCacheId = (self::$_options['encodeMethod']) ? $cacheId . '.gz' : $cacheId; @@ -276,7 +279,15 @@ class Minify { $cacheContentLength = self::$_cache->getSize($fullCacheId); } else { // generate & cache content - $content = self::_combineMinify(); + try { + $content = self::_combineMinify(); + } catch (Exception $e) { + self::$_controller->log($e->getMessage()); + if (! self::$_options['quiet']) { + self::_errorExit(self::$_options['errorHeader'], self::URL_DEBUG); + } + throw $e; + } self::$_cache->store($cacheId, $content); if (function_exists('gzencode')) { self::$_cache->store($cacheId . '.gz', gzencode($content, self::$_options['encodeLevel'])); @@ -285,7 +296,15 @@ class Minify { } else { // no cache $cacheIsReady = false; - $content = self::_combineMinify(); + try { + $content = self::_combineMinify(); + } catch (Exception $e) { + self::$_controller->log($e->getMessage()); + if (! self::$_options['quiet']) { + self::_errorExit(self::$_options['errorHeader'], self::URL_DEBUG); + } + throw $e; + } } if (! $cacheIsReady && self::$_options['encodeMethod']) { // still need to encode @@ -295,14 +314,17 @@ class Minify { // add headers $headers['Content-Length'] = $cacheIsReady ? $cacheContentLength - : strlen($content); + : ((function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($content, '8bit') + : strlen($content) + ); $headers['Content-Type'] = self::$_options['contentTypeCharset'] ? self::$_options['contentType'] . '; charset=' . self::$_options['contentTypeCharset'] : self::$_options['contentType']; if (self::$_options['encodeMethod'] !== '') { $headers['Content-Encoding'] = $contentEncoding; } - if (self::$_options['encodeOutput']) { + if (self::$_options['encodeOutput'] && $sendVary) { $headers['Vary'] = 'Accept-Encoding'; } @@ -369,9 +391,9 @@ class Minify { && 0 === strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/') ) { $_SERVER['DOCUMENT_ROOT'] = rtrim(substr( - $_SERVER['PATH_TRANSLATED'] + $_SERVER['SCRIPT_FILENAME'] ,0 - ,strlen($_SERVER['PATH_TRANSLATED']) - strlen($_SERVER['SCRIPT_NAME']) + ,strlen($_SERVER['SCRIPT_FILENAME']) - strlen($_SERVER['SCRIPT_NAME']) ), '\\'); if ($unsetPathInfo) { unset($_SERVER['PATH_INFO']); @@ -396,6 +418,20 @@ class Minify { */ protected static $_options = null; + protected static function _errorExit($header, $url) + { + $url = htmlspecialchars($url, ENT_QUOTES); + list(,$h1) = explode(' ', $header, 2); + $h1 = htmlspecialchars($h1); + // FastCGI environments require 3rd arg to header() to be set + list(, $code) = explode(' ', $header, 3); + header($header, true, $code); + header('Content-Type: text/html; charset=utf-8'); + echo "Please see $url.
"; + exit(); + } + /** * Set up sources to use Minify_Lines * @@ -440,37 +476,31 @@ class Minify { ? self::$_options['minifiers'][$type] : false; - if (Minify_Source::haveNoMinifyPrefs(self::$_controller->sources)) { - // all source have same options/minifier, better performance - // to combine, then minify once - foreach (self::$_controller->sources as $source) { + // minify each source with its own options and minifier, then combine. + // Here we used to combine all first but this was probably + // bad for PCRE performance, esp. in CSS. + foreach (self::$_controller->sources as $source) { + // allow the source to override our minifier and options + $minifier = (null !== $source->minifier) + ? $source->minifier + : $defaultMinifier; + $options = (null !== $source->minifyOptions) + ? array_merge($defaultOptions, $source->minifyOptions) + : $defaultOptions; + if ($minifier) { + self::$_controller->loadMinifier($minifier); + // get source content and minify it + try { + $pieces[] = call_user_func($minifier, $source->getContent(), $options); + } catch (Exception $e) { + throw new Exception("Exception in " . $source->getId() . + ": " . $e->getMessage()); + } + } else { $pieces[] = $source->getContent(); } - $content = implode($implodeSeparator, $pieces); - if ($defaultMinifier) { - self::$_controller->loadMinifier($defaultMinifier); - $content = call_user_func($defaultMinifier, $content, $defaultOptions); - } - } else { - // minify each source with its own options and minifier, then combine - foreach (self::$_controller->sources as $source) { - // allow the source to override our minifier and options - $minifier = (null !== $source->minifier) - ? $source->minifier - : $defaultMinifier; - $options = (null !== $source->minifyOptions) - ? array_merge($defaultOptions, $source->minifyOptions) - : $defaultOptions; - if ($minifier) { - self::$_controller->loadMinifier($minifier); - // get source content and minify it - $pieces[] = call_user_func($minifier, $source->getContent(), $options); - } else { - $pieces[] = $source->getContent(); - } - } - $content = implode($implodeSeparator, $pieces); } + $content = implode($implodeSeparator, $pieces); if ($type === self::TYPE_CSS && false !== strpos($content, '@import')) { $content = self::_handleCssImports($content); @@ -491,17 +521,23 @@ class Minify { * * Any settings that could affect output are taken into consideration * + * @param string $prefix + * * @return string */ - protected static function _getCacheId() + protected static function _getCacheId($prefix = 'minify') { - return md5(serialize(array( + $name = preg_replace('/[^a-zA-Z0-9\\.=_,]/', '', self::$_controller->selectionId); + $name = preg_replace('/\\.+/', '.', $name); + $name = substr($name, 0, 200 - 34 - strlen($prefix)); + $md5 = md5(serialize(array( Minify_Source::getDigest(self::$_controller->sources) ,self::$_options['minifiers'] ,self::$_options['minifierOptions'] ,self::$_options['postprocessor'] ,self::$_options['bubbleCssImports'] ))); + return "{$prefix}_{$name}_{$md5}"; } /** @@ -512,7 +548,7 @@ class Minify { { if (self::$_options['bubbleCssImports']) { // bubble CSS imports - preg_match_all('/@import.*?;/', $css, $imports); + preg_match_all('/@import.*?;/', $css, $imports); $css = implode('', $imports[0]) . preg_replace('/@import.*?;/', '', $css); } else if ('' !== self::$importWarning) { // remove comments so we don't mistake { in a comment as a block diff --git a/min/lib/Minify/CSS.php b/min/lib/Minify/CSS.php index 2220cf2..49882e9 100644 --- a/min/lib/Minify/CSS.php +++ b/min/lib/Minify/CSS.php @@ -26,6 +26,8 @@ class Minify_CSS { * 'preserveComments': (default true) multi-line comments that begin * with "/*!" will be preserved with newlines before and after to * enhance readability. + * + * 'removeCharsets': (default true) remove all @charset at-rules * * 'prependRelativePath': (default null) if given, this string will be * prepended to all relative URIs in import/url declarations @@ -36,23 +38,37 @@ class Minify_CSS { * the desired files. For this to work, the files *must* exist and be * visible by the PHP process. * - * 'symlinks': (default = array()) If the CSS file is stored in - * a symlink-ed directory, provide an array of link paths to - * target paths, where the link paths are within the document root. Because - * paths need to be normalized for this to work, use "//" to substitute - * the doc root in the link paths (the array keys). E.g.: - *
- * array('//symlink' => '/real/target/path') // unix
- * array('//static' => 'D:\\staticStorage') // Windows
+ * 'symlinks': (default = array()) If the CSS file is stored in
+ * a symlink-ed directory, provide an array of link paths to
+ * target paths, where the link paths are within the document root. Because
+ * paths need to be normalized for this to work, use "//" to substitute
+ * the doc root in the link paths (the array keys). E.g.:
+ *
+ * array('//symlink' => '/real/target/path') // unix
+ * array('//static' => 'D:\\staticStorage') // Windows
*
+ *
+ * 'docRoot': (default = $_SERVER['DOCUMENT_ROOT'])
+ * see Minify_CSS_UriRewriter::rewrite
*
* @return string
*/
public static function minify($css, $options = array())
{
+ $options = array_merge(array(
+ 'removeCharsets' => true,
+ 'preserveComments' => true,
+ 'currentDir' => null,
+ 'docRoot' => $_SERVER['DOCUMENT_ROOT'],
+ 'prependRelativePath' => null,
+ 'symlinks' => array(),
+ ), $options);
+
+ if ($options['removeCharsets']) {
+ $css = preg_replace('/@charset[^;]+;\\s*/', '', $css);
+ }
require_once 'Minify/CSS/Compressor.php';
- if (isset($options['preserveComments'])
- && !$options['preserveComments']) {
+ if (! $options['preserveComments']) {
$css = Minify_CSS_Compressor::process($css, $options);
} else {
require_once 'Minify/CommentPreserver.php';
@@ -62,16 +78,16 @@ class Minify_CSS {
,array($options)
);
}
- if (! isset($options['currentDir']) && ! isset($options['prependRelativePath'])) {
+ if (! $options['currentDir'] && ! $options['prependRelativePath']) {
return $css;
}
require_once 'Minify/CSS/UriRewriter.php';
- if (isset($options['currentDir'])) {
+ if ($options['currentDir']) {
return Minify_CSS_UriRewriter::rewrite(
$css
,$options['currentDir']
- ,isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT']
- ,isset($options['symlinks']) ? $options['symlinks'] : array()
+ ,$options['docRoot']
+ ,$options['symlinks']
);
} else {
return Minify_CSS_UriRewriter::prepend(
diff --git a/min/lib/Minify/CSS/Compressor.php b/min/lib/Minify/CSS/Compressor.php
index a348286..f6fb034 100644
--- a/min/lib/Minify/CSS/Compressor.php
+++ b/min/lib/Minify/CSS/Compressor.php
@@ -108,7 +108,7 @@ class Minify_CSS_Compressor {
\\s*
:
\\s*
- (\\b|[#\'"]) # 3 = first character of a value
+ (\\b|[#\'"-]) # 3 = first character of a value
/x', '$1$2:$3', $css);
// remove ws in selectors
@@ -236,15 +236,16 @@ class Minify_CSS_Compressor {
*/
protected function _fontFamilyCB($m)
{
- $m[1] = preg_replace('/
- \\s*
- (
- "[^"]+" # 1 = family in double qutoes
- |\'[^\']+\' # or 1 = family in single quotes
- |[\\w\\-]+ # or 1 = unquoted family
- )
- \\s*
- /x', '$1', $m[1]);
- return 'font-family:' . $m[1] . $m[2];
+ // Issue 210: must not eliminate WS between words in unquoted families
+ $pieces = preg_split('/(\'[^\']+\'|"[^"]+")/', $m[1], null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+ $out = 'font-family:';
+ while (null !== ($piece = array_shift($pieces))) {
+ if ($piece[0] !== '"' && $piece[0] !== "'") {
+ $piece = preg_replace('/\\s+/', ' ', $piece);
+ $piece = preg_replace('/\\s?,\\s?/', ',', $piece);
+ }
+ $out .= $piece;
+ }
+ return $out . $m[2];
}
}
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/lib/Minify/Cache/APC.php b/min/lib/Minify/Cache/APC.php
index ca84d29..24ab046 100644
--- a/min/lib/Minify/Cache/APC.php
+++ b/min/lib/Minify/Cache/APC.php
@@ -54,9 +54,12 @@ class Minify_Cache_APC {
*/
public function getSize($id)
{
- return $this->_fetch($id)
- ? strlen($this->_data)
- : false;
+ if (! $this->_fetch($id)) {
+ return false;
+ }
+ return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2))
+ ? mb_strlen($this->_data, '8bit')
+ : strlen($this->_data);
}
/**
diff --git a/min/lib/Minify/Cache/File.php b/min/lib/Minify/Cache/File.php
index 8744a7e..a45ba85 100644
--- a/min/lib/Minify/Cache/File.php
+++ b/min/lib/Minify/Cache/File.php
@@ -9,13 +9,12 @@ class Minify_Cache_File {
public function __construct($path = '', $fileLocking = false)
{
if (! $path) {
- require_once 'Solar/Dir.php';
- $path = rtrim(Solar_Dir::tmp(), DIRECTORY_SEPARATOR);
- }
+ $path = self::tmp();
+ }
$this->_locking = $fileLocking;
$this->_path = $path;
}
-
+
/**
* Write data to cache.
*
@@ -27,20 +26,23 @@ class Minify_Cache_File {
*/
public function store($id, $data)
{
- $flag = $this->_locking
- ? LOCK_EX
- : null;
- if (is_file($this->_path . '/' . $id)) {
- @unlink($this->_path . '/' . $id);
- }
- if (! @file_put_contents($this->_path . '/' . $id, $data, $flag)) {
- return false;
+ $flag = $this->_locking
+ ? LOCK_EX
+ : null;
+ $file = $this->_path . '/' . $id;
+ if (is_file($file)) {
+ @unlink($file);
+ }
+ if (! @file_put_contents($file, $data, $flag)) {
+ $this->_log("Minify_Cache_File: Write failed to '$file'");
+ return false;
+ }
+ // write control
+ if ($data !== $this->fetch($id)) {
+ @unlink($file);
+ $this->_log("Minify_Cache_File: Post-write read failed for '$file'");
+ return false;
}
- // write control
- if ($data !== $this->fetch($id)) {
- @unlink($file);
- return false;
- }
return true;
}
@@ -78,15 +80,15 @@ class Minify_Cache_File {
*/
public function display($id)
{
- if ($this->_locking) {
- $fp = fopen($this->_path . '/' . $id, 'rb');
- flock($fp, LOCK_SH);
- fpassthru($fp);
- flock($fp, LOCK_UN);
- fclose($fp);
- } else {
- readfile($this->_path . '/' . $id);
- }
+ if ($this->_locking) {
+ $fp = fopen($this->_path . '/' . $id, 'rb');
+ flock($fp, LOCK_SH);
+ fpassthru($fp);
+ flock($fp, LOCK_UN);
+ fclose($fp);
+ } else {
+ readfile($this->_path . '/' . $id);
+ }
}
/**
@@ -98,15 +100,15 @@ class Minify_Cache_File {
*/
public function fetch($id)
{
- if ($this->_locking) {
- $fp = fopen($this->_path . '/' . $id, 'rb');
- flock($fp, LOCK_SH);
- $ret = stream_get_contents($fp);
- flock($fp, LOCK_UN);
- fclose($fp);
- return $ret;
- } else {
- return file_get_contents($this->_path . '/' . $id);
+ if ($this->_locking) {
+ $fp = fopen($this->_path . '/' . $id, 'rb');
+ flock($fp, LOCK_SH);
+ $ret = stream_get_contents($fp);
+ flock($fp, LOCK_UN);
+ fclose($fp);
+ return $ret;
+ } else {
+ return file_get_contents($this->_path . '/' . $id);
}
}
@@ -119,7 +121,79 @@ class Minify_Cache_File {
{
return $this->_path;
}
+
+ /**
+ * Get a usable temp directory
+ *
+ * Adapted from Solar/Dir.php
+ * @author Paul M. Jones
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @link http://solarphp.com/trac/core/browser/trunk/Solar/Dir.php
+ *
+ * @return string
+ */
+ public static function tmp()
+ {
+ static $tmp = null;
+ if (! $tmp) {
+ $tmp = function_exists('sys_get_temp_dir')
+ ? sys_get_temp_dir()
+ : self::_tmp();
+ $tmp = rtrim($tmp, DIRECTORY_SEPARATOR);
+ }
+ return $tmp;
+ }
+
+ /**
+ * Returns the OS-specific directory for temporary files
+ *
+ * @author Paul M. Jones
+ * @license http://opensource.org/licenses/bsd-license.php BSD
+ * @link http://solarphp.com/trac/core/browser/trunk/Solar/Dir.php
+ *
+ * @return string
+ */
+ protected static function _tmp()
+ {
+ // non-Windows system?
+ if (strtolower(substr(PHP_OS, 0, 3)) != 'win') {
+ $tmp = empty($_ENV['TMPDIR']) ? getenv('TMPDIR') : $_ENV['TMPDIR'];
+ if ($tmp) {
+ return $tmp;
+ } else {
+ return '/tmp';
+ }
+ }
+ // Windows 'TEMP'
+ $tmp = empty($_ENV['TEMP']) ? getenv('TEMP') : $_ENV['TEMP'];
+ if ($tmp) {
+ return $tmp;
+ }
+ // Windows 'TMP'
+ $tmp = empty($_ENV['TMP']) ? getenv('TMP') : $_ENV['TMP'];
+ if ($tmp) {
+ return $tmp;
+ }
+ // Windows 'windir'
+ $tmp = empty($_ENV['windir']) ? getenv('windir') : $_ENV['windir'];
+ if ($tmp) {
+ return $tmp;
+ }
+ // final fallback for Windows
+ return getenv('SystemRoot') . '\\temp';
+ }
+
+ /**
+ * Send message to the Minify logger
+ * @param string $msg
+ * @return null
+ */
+ protected function _log($msg)
+ {
+ require_once 'Minify/Logger.php';
+ Minify_Logger::log($msg);
+ }
private $_path = null;
- private $_locking = null;
+ private $_locking = null;
}
diff --git a/min/lib/Minify/Cache/Memcache.php b/min/lib/Minify/Cache/Memcache.php
index 2b81e7a..72bf454 100644
--- a/min/lib/Minify/Cache/Memcache.php
+++ b/min/lib/Minify/Cache/Memcache.php
@@ -60,9 +60,12 @@ class Minify_Cache_Memcache {
*/
public function getSize($id)
{
- return $this->_fetch($id)
- ? strlen($this->_data)
- : false;
+ if (! $this->_fetch($id)) {
+ return false;
+ }
+ return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2))
+ ? mb_strlen($this->_data, '8bit')
+ : strlen($this->_data);
}
/**
diff --git a/min/lib/Minify/Cache/ZendPlatform.php b/min/lib/Minify/Cache/ZendPlatform.php
new file mode 100644
index 0000000..3130d69
--- /dev/null
+++ b/min/lib/Minify/Cache/ZendPlatform.php
@@ -0,0 +1,142 @@
+
+ * Minify::setCache(new Minify_Cache_ZendPlatform());
+ *
+ *
+ * @package Minify
+ * @author Patrick van Dissel
+ */
+class Minify_Cache_ZendPlatform {
+
+
+ /**
+ * Create a Minify_Cache_ZendPlatform object, to be passed to
+ * Minify::setCache().
+ *
+ * @param int $expire seconds until expiration (default = 0
+ * meaning the item will not get an expiration date)
+ *
+ * @return null
+ */
+ public function __construct($expire = 0)
+ {
+ $this->_exp = $expire;
+ }
+
+
+ /**
+ * Write data to cache.
+ *
+ * @param string $id cache id
+ *
+ * @param string $data
+ *
+ * @return bool success
+ */
+ public function store($id, $data)
+ {
+ return output_cache_put($id, "{$_SERVER['REQUEST_TIME']}|{$data}");
+ }
+
+
+ /**
+ * Get the size of a cache entry
+ *
+ * @param string $id cache id
+ *
+ * @return int size in bytes
+ */
+ public function getSize($id)
+ {
+ return $this->_fetch($id)
+ ? strlen($this->_data)
+ : false;
+ }
+
+
+ /**
+ * Does a valid cache entry exist?
+ *
+ * @param string $id cache id
+ *
+ * @param int $srcMtime mtime of the original source file(s)
+ *
+ * @return bool exists
+ */
+ public function isValid($id, $srcMtime)
+ {
+ $ret = ($this->_fetch($id) && ($this->_lm >= $srcMtime));
+ return $ret;
+ }
+
+
+ /**
+ * Send the cached content to output
+ *
+ * @param string $id cache id
+ */
+ public function display($id)
+ {
+ echo $this->_fetch($id)
+ ? $this->_data
+ : '';
+ }
+
+
+ /**
+ * Fetch the cached content
+ *
+ * @param string $id cache id
+ *
+ * @return string
+ */
+ public function fetch($id)
+ {
+ return $this->_fetch($id)
+ ? $this->_data
+ : '';
+ }
+
+
+ private $_exp = null;
+
+
+ // cache of most recently fetched id
+ private $_lm = null;
+ private $_data = null;
+ private $_id = null;
+
+
+ /**
+ * Fetch data and timestamp from ZendPlatform, store in instance
+ *
+ * @param string $id
+ *
+ * @return bool success
+ */
+ private function _fetch($id)
+ {
+ if ($this->_id === $id) {
+ return true;
+ }
+ $ret = output_cache_get($id, $this->_exp);
+ if (false === $ret) {
+ $this->_id = null;
+ return false;
+ }
+ list($this->_lm, $this->_data) = explode('|', $ret, 2);
+ $this->_id = $id;
+ return true;
+ }
+}
diff --git a/min/lib/Minify/CommentPreserver.php b/min/lib/Minify/CommentPreserver.php
index f56eb34..7a359bf 100644
--- a/min/lib/Minify/CommentPreserver.php
+++ b/min/lib/Minify/CommentPreserver.php
@@ -30,8 +30,7 @@ class Minify_CommentPreserver {
* Process a string outside of C-style comments that begin with "/*!"
*
* On each non-empty string outside these comments, the given processor
- * function will be called. The first "!" will be removed from the
- * preserved comments, and the comments will be surrounded by
+ * function will be called. The comments will be surrounded by
* Minify_CommentPreserver::$preprend and Minify_CommentPreserver::$append.
*
* @param string $content
@@ -65,7 +64,7 @@ class Minify_CommentPreserver {
* @param string $in input
*
* @return array 3 elements are returned. If a YUI comment is found, the
- * 2nd element is the comment and the 1st and 2nd are the surrounding
+ * 2nd element is the comment and the 1st and 3rd are the surrounding
* strings. If no comment is found, the entire string is returned as the
* 1st element and the other two are false.
*/
@@ -79,7 +78,7 @@ class Minify_CommentPreserver {
}
$ret = array(
substr($in, 0, $start)
- ,self::$prepend . '/*' . substr($in, $start + 3, $end - $start - 1) . self::$append
+ ,self::$prepend . '/*!' . substr($in, $start + 3, $end - $start - 1) . self::$append
);
$endChars = (strlen($in) - $end - 2);
$ret[] = (0 === $endChars)
diff --git a/min/lib/Minify/Controller/Base.php b/min/lib/Minify/Controller/Base.php
index 84889b3..724dd8d 100644
--- a/min/lib/Minify/Controller/Base.php
+++ b/min/lib/Minify/Controller/Base.php
@@ -52,9 +52,10 @@ abstract class Minify_Controller_Base {
,'quiet' => false // serve() will send headers and output
,'debug' => false
- // if you override this, the response code MUST be directly after
+ // if you override these, the response codes MUST be directly after
// the first space.
,'badRequestHeader' => 'HTTP/1.0 400 Bad Request'
+ ,'errorHeader' => 'HTTP/1.0 500 Internal Server Error'
// callback function to see/modify content of all sources
,'postprocessor' => null
@@ -117,6 +118,8 @@ abstract class Minify_Controller_Base {
* be in subdirectories of these directories.
*
* @return bool file is safe
+ *
+ * @deprecated use checkAllowDirs, checkNotHidden instead
*/
public static function _fileIsSafe($file, $safeDirs)
{
@@ -134,7 +137,28 @@ abstract class Minify_Controller_Base {
list($revExt) = explode('.', strrev($base));
return in_array(strrev($revExt), array('js', 'css', 'html', 'txt'));
}
+
+ public static function checkAllowDirs($file, $allowDirs, $uri)
+ {
+ foreach ((array)$allowDirs as $allowDir) {
+ if (strpos($file, $allowDir) === 0) {
+ return true;
+ }
+ }
+ throw new Exception("File '$file' is outside \$allowDirs. If the path is"
+ . " resolved via an alias/symlink, look into the \$min_symlinks option."
+ . " E.g. \$min_symlinks['/" . dirname($uri) . "'] = '" . dirname($file) . "';");
+ }
+
+ public static function checkNotHidden($file)
+ {
+ $b = basename($file);
+ if (0 === strpos($b, '.')) {
+ throw new Exception("Filename '$b' starts with period (may be hidden)");
+ }
+ }
+
/**
* @var array instances of Minify_Source, which provide content and
* any individual minification needs.
@@ -143,6 +167,15 @@ abstract class Minify_Controller_Base {
*/
public $sources = array();
+ /**
+ * The setupSources() method may choose to set this, making it easier to
+ * recognize a particular set of sources/settings in the cache folder. It
+ * will be filtered and truncated to make the final cache id <= 250 bytes.
+ *
+ * @var string short name to place inside cache id
+ */
+ public $selectionId = '';
+
/**
* Mix in default controller options with user-given options
*
@@ -195,7 +228,7 @@ abstract class Minify_Controller_Base {
* @param string $msg
* @return null
*/
- protected function log($msg) {
+ public function log($msg) {
require_once 'Minify/Logger.php';
Minify_Logger::log($msg);
}
diff --git a/min/lib/Minify/Controller/MinApp.php b/min/lib/Minify/Controller/MinApp.php
index 9582d29..986ef2c 100644
--- a/min/lib/Minify/Controller/MinApp.php
+++ b/min/lib/Minify/Controller/MinApp.php
@@ -28,64 +28,86 @@ class Minify_Controller_MinApp extends Minify_Controller_Base {
'allowDirs' => '//'
,'groupsOnly' => false
,'groups' => array()
- ,'maxFiles' => 10
+ ,'noMinPattern' => '@[-\\.]min\\.(?:js|css)$@i' // matched against basename
)
,(isset($options['minApp']) ? $options['minApp'] : array())
);
unset($options['minApp']);
$sources = array();
+ $this->selectionId = '';
+ $missingUri = '';
+
if (isset($_GET['g'])) {
- // try groups
- if (! isset($cOptions['groups'][$_GET['g']])) {
- $this->log("A group configuration for \"{$_GET['g']}\" was not set");
+ // add group(s)
+ $this->selectionId .= 'g=' . $_GET['g'];
+ $keys = explode(',', $_GET['g']);
+ if ($keys != array_unique($keys)) {
+ $this->log("Duplicate group key found.");
return $options;
}
-
- $files = $cOptions['groups'][$_GET['g']];
- // if $files is a single object, casting will break it
- if (is_object($files)) {
- $files = array($files);
- } elseif (! is_array($files)) {
- $files = (array)$files;
- }
- foreach ($files as $file) {
- if ($file instanceof Minify_Source) {
- $sources[] = $file;
- continue;
- }
- if (0 === strpos($file, '//')) {
- $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1);
- }
- $file = realpath($file);
- if (is_file($file)) {
- $sources[] = new Minify_Source(array(
- 'filepath' => $file
- ));
- } else {
- $this->log("The path \"{$file}\" could not be found (or was not a file)");
+ foreach (explode(',', $_GET['g']) as $key) {
+ if (! isset($cOptions['groups'][$key])) {
+ $this->log("A group configuration for \"{$key}\" was not found");
return $options;
}
+ $files = $cOptions['groups'][$key];
+ // if $files is a single object, casting will break it
+ if (is_object($files)) {
+ $files = array($files);
+ } elseif (! is_array($files)) {
+ $files = (array)$files;
+ }
+ foreach ($files as $file) {
+ if ($file instanceof Minify_Source) {
+ $sources[] = $file;
+ continue;
+ }
+ if (0 === strpos($file, '//')) {
+ $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1);
+ }
+ $realpath = realpath($file);
+ if ($realpath && is_file($realpath)) {
+ $sources[] = $this->_getFileSource($realpath, $cOptions);
+ } else {
+ $this->log("The path \"{$file}\" (realpath \"{$realpath}\") could not be found (or was not a file)");
+ return $options;
+ }
+ }
+ if ($sources) {
+ try {
+ $this->checkType($sources[0]);
+ } catch (Exception $e) {
+ $this->log($e->getMessage());
+ return $options;
+ }
+ }
}
- } elseif (! $cOptions['groupsOnly'] && isset($_GET['f'])) {
+ }
+ if (! $cOptions['groupsOnly'] && isset($_GET['f'])) {
// try user files
// The following restrictions are to limit the URLs that minify will
- // respond to. Ideally there should be only one way to reference a file.
+ // respond to.
if (// verify at least one file, files are single comma separated,
// and are all same extension
- ! preg_match('/^[^,]+\\.(css|js)(?:,[^,]+\\.\\1)*$/', $_GET['f'])
+ ! preg_match('/^[^,]+\\.(css|js)(?:,[^,]+\\.\\1)*$/', $_GET['f'], $m)
// no "//"
|| strpos($_GET['f'], '//') !== false
// no "\"
|| strpos($_GET['f'], '\\') !== false
- // no "./"
- || preg_match('/(?:^|[^\\.])\\.\\//', $_GET['f'])
) {
- $this->log("GET param 'f' invalid (see MinApp.php line 63)");
+ $this->log("GET param 'f' was invalid");
+ return $options;
+ }
+ $ext = ".{$m[1]}";
+ try {
+ $this->checkType($m[1]);
+ } catch (Exception $e) {
+ $this->log($e->getMessage());
return $options;
}
$files = explode(',', $_GET['f']);
- if (count($files) > $cOptions['maxFiles'] || $files != array_unique($files)) {
- $this->log("Too many or duplicate files specified");
+ if ($files != array_unique($files)) {
+ $this->log("Duplicate files were specified");
return $options;
}
if (isset($_GET['b'])) {
@@ -96,7 +118,7 @@ class Minify_Controller_MinApp extends Minify_Controller_Base {
// valid base
$base = "/{$_GET['b']}/";
} else {
- $this->log("GET param 'b' invalid (see MinApp.php line 84)");
+ $this->log("GET param 'b' was invalid");
return $options;
}
} else {
@@ -106,27 +128,82 @@ class Minify_Controller_MinApp extends Minify_Controller_Base {
foreach ((array)$cOptions['allowDirs'] as $allowDir) {
$allowDirs[] = realpath(str_replace('//', $_SERVER['DOCUMENT_ROOT'] . '/', $allowDir));
}
+ $basenames = array(); // just for cache id
foreach ($files as $file) {
- $path = $_SERVER['DOCUMENT_ROOT'] . $base . $file;
- $file = realpath($path);
- if (false === $file) {
- $this->log("Path \"{$path}\" failed realpath()");
- return $options;
- } elseif (! parent::_fileIsSafe($file, $allowDirs)) {
- $this->log("Path \"{$path}\" failed Minify_Controller_Base::_fileIsSafe()");
- return $options;
- } else {
- $sources[] = new Minify_Source(array(
- 'filepath' => $file
- ));
+ $uri = $base . $file;
+ $path = $_SERVER['DOCUMENT_ROOT'] . $uri;
+ $realpath = realpath($path);
+ if (false === $realpath || ! is_file($realpath)) {
+ $this->log("The path \"{$path}\" (realpath \"{$realpath}\") could not be found (or was not a file)");
+ if (! $missingUri) {
+ $missingUri = $uri;
+ continue;
+ } else {
+ $this->log("More than one file was missing: '$missingUri', '$uri'");
+ return $options;
+ }
}
+ try {
+ parent::checkNotHidden($realpath);
+ parent::checkAllowDirs($realpath, $allowDirs, $uri);
+ } catch (Exception $e) {
+ $this->log($e->getMessage());
+ return $options;
+ }
+ $sources[] = $this->_getFileSource($realpath, $cOptions);
+ $basenames[] = basename($realpath, $ext);
}
+ if ($this->selectionId) {
+ $this->selectionId .= '_f=';
+ }
+ $this->selectionId .= implode(',', $basenames) . $ext;
}
if ($sources) {
+ if ($missingUri) {
+ array_unshift($sources, new Minify_Source(array(
+ 'id' => 'missingFile'
+ ,'lastModified' => 0
+ ,'content' => "/* Minify: missing file '" . ltrim($missingUri, '/') . "' */\n"
+ ,'minifier' => ''
+ )));
+ }
$this->sources = $sources;
} else {
$this->log("No sources to serve");
}
return $options;
}
+
+ protected function _getFileSource($file, $cOptions)
+ {
+ $spec['filepath'] = $file;
+ if ($cOptions['noMinPattern']
+ && preg_match($cOptions['noMinPattern'], basename($file))) {
+ $spec['minifier'] = '';
+ }
+ return new Minify_Source($spec);
+ }
+
+ protected $_type = null;
+
+ /*
+ * Make sure that only source files of a single type are registered
+ */
+ public function checkType($sourceOrExt)
+ {
+ if ($sourceOrExt === 'js') {
+ $type = Minify::TYPE_JS;
+ } elseif ($sourceOrExt === 'css') {
+ $type = Minify::TYPE_CSS;
+ } elseif ($sourceOrExt->contentType !== null) {
+ $type = $sourceOrExt->contentType;
+ } else {
+ return;
+ }
+ if ($this->_type === null) {
+ $this->_type = $type;
+ } elseif ($this->_type !== $type) {
+ throw new Exception('Content-Type mismatch');
+ }
+ }
}
diff --git a/min/lib/Minify/Controller/Page.php b/min/lib/Minify/Controller/Page.php
index fa4599a..de471e1 100644
--- a/min/lib/Minify/Controller/Page.php
+++ b/min/lib/Minify/Controller/Page.php
@@ -40,14 +40,19 @@ class Minify_Controller_Page extends Minify_Controller_Base {
$sourceSpec = array(
'filepath' => $options['file']
);
+ $f = $options['file'];
} else {
// strip controller options
$sourceSpec = array(
'content' => $options['content']
,'id' => $options['id']
);
+ $f = $options['id'];
unset($options['content'], $options['id']);
}
+ // something like "builder,index.php" or "directory,file.html"
+ $this->selectionId = strtr(substr($f, 1 + strlen(dirname(dirname($f)))), '/\\', ',,');
+
if (isset($options['minifyAll'])) {
// this will be the 2nd argument passed to Minify_HTML::minify()
$sourceSpec['minifyOptions'] = array(
diff --git a/min/lib/Minify/Controller/Version1.php b/min/lib/Minify/Controller/Version1.php
index 1861aab..5279d36 100644
--- a/min/lib/Minify/Controller/Version1.php
+++ b/min/lib/Minify/Controller/Version1.php
@@ -7,7 +7,7 @@
require_once 'Minify/Controller/Base.php';
/**
- * Controller class for emulating version 1 of minify.php
+ * Controller class for emulating version 1 of minify.php (mostly a proof-of-concept)
*
*
* Minify::serve('Version1');
diff --git a/min/lib/Minify/HTML.php b/min/lib/Minify/HTML.php
index fb5c1e9..e9453ff 100644
--- a/min/lib/Minify/HTML.php
+++ b/min/lib/Minify/HTML.php
@@ -1,245 +1,246 @@
-
- */
-class Minify_HTML {
-
- /**
- * "Minify" an HTML page
- *
- * @param string $html
- *
- * @param array $options
- *
- * 'cssMinifier' : (optional) callback function to process content of STYLE
- * elements.
- *
- * 'jsMinifier' : (optional) callback function to process content of SCRIPT
- * elements. Note: the type attribute is ignored.
- *
- * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If
- * unset, minify will sniff for an XHTML doctype.
- *
- * @return string
- */
- public static function minify($html, $options = array()) {
- $min = new Minify_HTML($html, $options);
- return $min->process();
- }
-
-
- /**
- * Create a minifier object
- *
- * @param string $html
- *
- * @param array $options
- *
- * 'cssMinifier' : (optional) callback function to process content of STYLE
- * elements.
- *
- * 'jsMinifier' : (optional) callback function to process content of SCRIPT
- * elements. Note: the type attribute is ignored.
- *
- * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If
- * unset, minify will sniff for an XHTML doctype.
- *
- * @return null
- */
- public function __construct($html, $options = array())
- {
- $this->_html = str_replace("\r\n", "\n", trim($html));
- if (isset($options['xhtml'])) {
- $this->_isXhtml = (bool)$options['xhtml'];
- }
- if (isset($options['cssMinifier'])) {
- $this->_cssMinifier = $options['cssMinifier'];
- }
- if (isset($options['jsMinifier'])) {
- $this->_jsMinifier = $options['jsMinifier'];
- }
- }
-
-
- /**
- * Minify the markeup given in the constructor
- *
- * @return string
- */
- public function process()
- {
- if ($this->_isXhtml === null) {
- $this->_isXhtml = (false !== strpos($this->_html, '_html);
-
- // fill placeholders
- $this->_html = str_replace(
- array_keys($this->_placeholders)
- ,array_values($this->_placeholders)
- ,$this->_html
- );
- return $this->_html;
- }
-
- protected function _commentCB($m)
- {
- return (0 === strpos($m[1], '[') || false !== strpos($m[1], '_replacementHash . count($this->_placeholders) . '%';
- $this->_placeholders[$placeholder] = $content;
- return $placeholder;
- }
-
- protected $_isXhtml = null;
- protected $_replacementHash = null;
- protected $_placeholders = array();
- protected $_cssMinifier = null;
- protected $_jsMinifier = null;
-
- protected function _outsideTagCB($m)
- {
- return '>' . preg_replace('/^\\s+|\\s+$/', ' ', $m[1]) . '<';
- }
-
- protected function _removePreCB($m)
- {
- return $this->_reservePlace($m[1]);
- }
-
- protected function _removeTextareaCB($m)
- {
- return $this->_reservePlace($m[1]);
- }
-
- protected function _removeStyleCB($m)
- {
- $openStyle = $m[1];
- $css = $m[2];
- // remove HTML comments
- $css = preg_replace('/(?:^\\s*\\s*$)/', '', $css);
-
- // remove CDATA section markers
- $css = $this->_removeCdata($css);
-
- // minify
- $minifier = $this->_cssMinifier
- ? $this->_cssMinifier
- : 'trim';
- $css = call_user_func($minifier, $css);
-
- return $this->_reservePlace($this->_needsCdata($css)
- ? "{$openStyle}/**/"
- : "{$openStyle}{$css}"
- );
- }
-
- protected function _removeScriptCB($m)
- {
- $openScript = $m[2];
- $js = $m[3];
-
- // whitespace surrounding? preserve at least one space
- $ws1 = ($m[1] === '') ? '' : ' ';
- $ws2 = ($m[4] === '') ? '' : ' ';
-
- // remove HTML comments (and ending "//" if present)
- $js = preg_replace('/(?:^\\s*\\s*$)/', '', $js);
-
- // remove CDATA section markers
- $js = $this->_removeCdata($js);
-
- // minify
- $minifier = $this->_jsMinifier
- ? $this->_jsMinifier
- : 'trim';
- $js = call_user_func($minifier, $js);
-
- return $this->_reservePlace($this->_needsCdata($js)
- ? "{$ws1}{$openScript}/**/{$ws2}"
- : "{$ws1}{$openScript}{$js}{$ws2}"
- );
- }
-
- protected function _removeCdata($str)
- {
- return (false !== strpos($str, ''), '', $str)
- : $str;
- }
-
- protected function _needsCdata($str)
- {
- return ($this->_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str));
- }
-}
+
+ */
+class Minify_HTML {
+
+ /**
+ * "Minify" an HTML page
+ *
+ * @param string $html
+ *
+ * @param array $options
+ *
+ * 'cssMinifier' : (optional) callback function to process content of STYLE
+ * elements.
+ *
+ * 'jsMinifier' : (optional) callback function to process content of SCRIPT
+ * elements. Note: the type attribute is ignored.
+ *
+ * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If
+ * unset, minify will sniff for an XHTML doctype.
+ *
+ * @return string
+ */
+ public static function minify($html, $options = array()) {
+ $min = new Minify_HTML($html, $options);
+ return $min->process();
+ }
+
+
+ /**
+ * Create a minifier object
+ *
+ * @param string $html
+ *
+ * @param array $options
+ *
+ * 'cssMinifier' : (optional) callback function to process content of STYLE
+ * elements.
+ *
+ * 'jsMinifier' : (optional) callback function to process content of SCRIPT
+ * elements. Note: the type attribute is ignored.
+ *
+ * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If
+ * unset, minify will sniff for an XHTML doctype.
+ *
+ * @return null
+ */
+ public function __construct($html, $options = array())
+ {
+ $this->_html = str_replace("\r\n", "\n", trim($html));
+ if (isset($options['xhtml'])) {
+ $this->_isXhtml = (bool)$options['xhtml'];
+ }
+ if (isset($options['cssMinifier'])) {
+ $this->_cssMinifier = $options['cssMinifier'];
+ }
+ if (isset($options['jsMinifier'])) {
+ $this->_jsMinifier = $options['jsMinifier'];
+ }
+ }
+
+
+ /**
+ * Minify the markeup given in the constructor
+ *
+ * @return string
+ */
+ public function process()
+ {
+ if ($this->_isXhtml === null) {
+ $this->_isXhtml = (false !== strpos($this->_html, '_html);
+
+ // fill placeholders
+ $this->_html = str_replace(
+ array_keys($this->_placeholders)
+ ,array_values($this->_placeholders)
+ ,$this->_html
+ );
+ // issue 229: multi-pass to catch scripts that didn't get replaced in textareas
+ $this->_html = str_replace(
+ array_keys($this->_placeholders)
+ ,array_values($this->_placeholders)
+ ,$this->_html
+ );
+ return $this->_html;
+ }
+
+ protected function _commentCB($m)
+ {
+ return (0 === strpos($m[1], '[') || false !== strpos($m[1], '_replacementHash . count($this->_placeholders) . '%';
+ $this->_placeholders[$placeholder] = $content;
+ return $placeholder;
+ }
+
+ protected $_isXhtml = null;
+ protected $_replacementHash = null;
+ protected $_placeholders = array();
+ protected $_cssMinifier = null;
+ protected $_jsMinifier = null;
+
+ protected function _removePreCB($m)
+ {
+ return $this->_reservePlace("_reservePlace("
*
- * If you do not want ampersands as HTML entities, set Minify_Build::$ampersand = "&"
- * before using this function.
- *
- * @param string $group a key from groupsConfig.php
- * @param boolean $forceAmpersand (default false) Set to true if the RewriteRule
- * directives in .htaccess are functional. This will remove the "?" from URIs, making them
- * more cacheable by proxies.
+ * @param mixed $keyOrFiles a group key or array of file paths/URIs
+ * @param array $opts options:
+ * 'farExpires' : (default true) append a modified timestamp for cache revving
+ * 'debug' : (default false) append debug flag
+ * 'charset' : (default 'UTF-8') for htmlspecialchars
+ * 'minAppUri' : (default '/min') URI of min directory
+ * 'rewriteWorks' : (default true) does mod_rewrite work in min app?
+ * 'groupsConfigFile' : specify if different
* @return string
- */
-function Minify_groupUri($group, $forceAmpersand = false)
-{
- $path = $forceAmpersand
- ? "/g={$group}"
- : "/?g={$group}";
- return _Minify_getBuild($group)->uri(
- '/' . basename(dirname(__FILE__)) . $path
- ,$forceAmpersand
- );
-}
-
-
-/**
- * Get the last modification time of the source js/css files used by Minify to
- * build the page.
- *
- * If you're caching the output of Minify_groupUri(), you'll want to rebuild
- * the cache if it's older than this timestamp.
- *
- *
- * // simplistic HTML cache system
- * $file = '/path/to/cache/file';
- * if (! file_exists($file) || filemtime($file) < Minify_groupsMtime(array('js', 'css'))) {
- * // (re)build cache
- * $page = buildPage(); // this calls Minify_groupUri() for js and css
- * file_put_contents($file, $page);
- * echo $page;
- * exit();
- * }
- * readfile($file);
- *
- *
- * @param array $groups an array of keys from groupsConfig.php
- * @return int Unix timestamp of the latest modification
- */
-function Minify_groupsMtime($groups)
-{
- $max = 0;
- foreach ((array)$groups as $group) {
- $max = max($max, _Minify_getBuild($group)->lastModified);
- }
- return $max;
-}
-
-/**
- * @param string $group a key from groupsConfig.php
- * @return Minify_Build
- * @private
*/
-function _Minify_getBuild($group)
+function Minify_getUri($keyOrFiles, $opts = array())
{
- static $builds = array();
- static $gc = false;
- if (false === $gc) {
- $gc = (require dirname(__FILE__) . '/groupsConfig.php');
- }
- if (! isset($builds[$group])) {
- $builds[$group] = new Minify_Build($gc[$group]);
- }
- return $builds[$group];
+ return Minify_HTML_Helper::getUri($keyOrFiles, $opts);
+}
+
+
+/**
+ * Get the last modification time of several source js/css files. If you're
+ * caching the output of Minify_getUri(), you might want to know if one of the
+ * dependent source files has changed so you can update the HTML.
+ *
+ * Since this makes a bunch of stat() calls, you might not want to check this
+ * on every request.
+ *
+ * @param array $keysAndFiles group keys and/or file paths/URIs.
+ * @return int latest modification time of all given keys/files
+ */
+function Minify_mtime($keysAndFiles, $groupsConfigFile = null)
+{
+ $gc = null;
+ if (! $groupsConfigFile) {
+ $groupsConfigFile = dirname(__FILE__) . '/groupsConfig.php';
+ }
+ $sources = array();
+ foreach ($keysAndFiles as $keyOrFile) {
+ if (is_object($keyOrFile)
+ || 0 === strpos($keyOrFile, '/')
+ || 1 === strpos($keyOrFile, ':\\')) {
+ // a file/source obj
+ $sources[] = $keyOrFile;
+ } else {
+ if (! $gc) {
+ $gc = (require $groupsConfigFile);
+ }
+ foreach ($gc[$keyOrFile] as $source) {
+ $sources[] = $source;
+ }
+ }
+ }
+ return Minify_HTML_Helper::getLastModified($sources);
}
diff --git a/min_unit_tests/.htaccess b/min_unit_tests/.htaccess
new file mode 100644
index 0000000..4e05c4c
--- /dev/null
+++ b/min_unit_tests/.htaccess
@@ -0,0 +1,4 @@
+
+# In case AddOutputFilterByType has been added
+SetEnv no-gzip
+
diff --git a/min_unit_tests/_inc.php b/min_unit_tests/_inc.php
index bf416d4..711009a 100644
--- a/min_unit_tests/_inc.php
+++ b/min_unit_tests/_inc.php
@@ -24,7 +24,7 @@ if ($min_errorLogger && true !== $min_errorLogger) { // custom logger
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', 1);
-header('Content-Type: text/plain');
+header('Content-Type: text/plain;charset=utf-8');
$thisDir = dirname(__FILE__);
@@ -47,4 +47,17 @@ function assertTrue($test, $message)
return (bool)$test;
}
+/**
+ * Get number of bytes in a string regardless of mbstring.func_overload
+ *
+ * @param string $str
+ * @return int
+ */
+function countBytes($str)
+{
+ return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2))
+ ? mb_strlen($str, '8bit')
+ : strlen($str);
+}
+
ob_start();
\ No newline at end of file
diff --git a/min_unit_tests/_test_files/css/comments.min.css b/min_unit_tests/_test_files/css/comments.min.css
index 3ea5c61..e0414a7 100644
--- a/min_unit_tests/_test_files/css/comments.min.css
+++ b/min_unit_tests/_test_files/css/comments.min.css
@@ -1,3 +1,3 @@
-/* YUI Compressor style comments are preserved */
+/*! YUI Compressor style comments are preserved */
body{background:#fff url(/path/to/image.gif) repeat-y}
\ No newline at end of file
diff --git a/min_unit_tests/_test_files/css/issue210.css b/min_unit_tests/_test_files/css/issue210.css
new file mode 100644
index 0000000..842195b
--- /dev/null
+++ b/min_unit_tests/_test_files/css/issue210.css
@@ -0,0 +1 @@
+.ui-widget { font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; }
\ No newline at end of file
diff --git a/min_unit_tests/_test_files/css/issue210.min.css b/min_unit_tests/_test_files/css/issue210.min.css
new file mode 100644
index 0000000..8a1869d
--- /dev/null
+++ b/min_unit_tests/_test_files/css/issue210.min.css
@@ -0,0 +1 @@
+.ui-widget{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif}
\ No newline at end of file
diff --git a/min_unit_tests/_test_files/css/styles.css b/min_unit_tests/_test_files/css/styles.css
index bf46c0a..ed0bcd4 100644
--- a/min_unit_tests/_test_files/css/styles.css
+++ b/min_unit_tests/_test_files/css/styles.css
@@ -1,9 +1,11 @@
+@charset "utf-8";
+
/* some CSS to try to exercise things in general */
@import url( /more.css );
body, td, th {
- font-family: Verdana , "Bitstream Vera Sans" , sans-serif ;
+ font-family: Verdana , "Bitstream Vera Sans" , Arial Narrow, sans-serif ;
font-size : 12px;
}
diff --git a/min_unit_tests/_test_files/css/styles.min.css b/min_unit_tests/_test_files/css/styles.min.css
index a823b05..a71063d 100644
--- a/min_unit_tests/_test_files/css/styles.min.css
+++ b/min_unit_tests/_test_files/css/styles.min.css
@@ -1,3 +1,3 @@
-@import url(/more.css);body,td,th{font-family:Verdana,"Bitstream Vera Sans",sans-serif;font-size:12px}.nav{margin-left:20%}#main-nav{background-color:red;border:1px
+@import url(/more.css);body,td,th{font-family:Verdana,"Bitstream Vera Sans",Arial Narrow,sans-serif;font-size:12px}.nav{margin-left:20%}#main-nav{background-color:red;border:1px
solid #0f7}div#content
h1+p{padding-top:0;margin-top:0}@media all and (min-width: 640px){#media-queries-1{background-color:#0f0}}@media screen and (max-width: 2000px){#media-queries-2{background-color:#0f0}}
\ No newline at end of file
diff --git a/min_unit_tests/_test_files/css/vladmirated.min.css b/min_unit_tests/_test_files/css/vladmirated.min.css
index eaffaee..a59e500 100644
--- a/min_unit_tests/_test_files/css/vladmirated.min.css
+++ b/min_unit_tests/_test_files/css/vladmirated.min.css
@@ -25,7 +25,7 @@ p{padding:0px;margin:0px;padding-bottom:5px}#post_nav{text-align:left;padding-bo
a{text-decoration:none;font-family:Verdana;font-size:12px;color:#108eed}#post_nav a:hover, .post_nav_2 p a:hover{text-decoration:underline;color:#FF8000}#rightcol
#about{padding-bottom:10px}#rightcol #r_news
input{color:#333;font-size:12px}#header{height:200px;width:100%;background-image:url('images/header_bg.jpg');background-repeat:x-repeat}#header
-img{float:right;margin-right: -3px;z-index:100}.tags{text-transform:lowercase;color:#333;font-family:arial;font-size:12px;border-top:2px dotted #EEE;width:300px;padding-top:20px;padding-bottom:0px;margin-top:0px;padding-left:20px;padding-right:20px}.tags
+img{float:right;margin-right:-3px;z-index:100}.tags{text-transform:lowercase;color:#333;font-family:arial;font-size:12px;border-top:2px dotted #EEE;width:300px;padding-top:20px;padding-bottom:0px;margin-top:0px;padding-left:20px;padding-right:20px}.tags
a{color:#108eed}.tags
p{text-align:left;margin:0px;padding:0px}blockquote
strong{font-family:verdana;display:block;margin-top:10px;color:#F00;font-style:italic;text-align:right}blockquote{margin:0px;background-color:#eee;border:2px
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_files/html/before.html b/min_unit_tests/_test_files/html/before.html
index c22cc8f..3b06d18 100644
--- a/min_unit_tests/_test_files/html/before.html
+++ b/min_unit_tests/_test_files/html/before.html
@@ -78,7 +78,7 @@ css hack {
css Zen Garden
- The Beauty of CSS
+ The Beauty of CSS
Design
@@ -90,7 +90,7 @@ Design
+1234567890